Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hearing impaired subtitle stream indicator #7379

Merged
merged 3 commits into from
Oct 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions Emby.Naming/Common/NamingOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ public NamingOptions()
"default"
};

MediaHearingImpairedFlags = new[]
{
"cc",
"hi",
"sdh"
};

EpisodeExpressions = new[]
{
// *** Begin Kodi Standard Naming
Expand Down Expand Up @@ -727,6 +734,11 @@ public NamingOptions()
/// </summary>
public string[] MediaDefaultFlags { get; set; }

/// <summary>
/// Gets or sets list of external media hearing impaired flags.
/// </summary>
public string[] MediaHearingImpairedFlags { get; set; }

/// <summary>
/// Gets or sets list of album stacking prefixes.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions Emby.Naming/ExternalFiles/ExternalPathParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager loca
pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (culture != null && pathInfo.Language == "hin")
{
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
pathInfo.IsHearingImpaired = true;
pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
{
pathInfo.IsHearingImpaired = true;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else
{
titleString = currentSlice + titleString;
Expand Down
10 changes: 9 additions & 1 deletion Emby.Naming/ExternalFiles/ExternalPathParserResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public class ExternalPathParserResult
/// <param name="path">Path to file.</param>
/// <param name="isDefault">Is default.</param>
/// <param name="isForced">Is forced.</param>
public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false)
/// <param name="isHearingImpaired">For the hearing impaired.</param>
public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false)
{
Path = path;
IsDefault = isDefault;
IsForced = isForced;
IsHearingImpaired = isHearingImpaired;
}

/// <summary>
Expand Down Expand Up @@ -47,5 +49,11 @@ public ExternalPathParserResult(string path, bool isDefault = false, bool isForc
/// </summary>
/// <value><c>true</c> if this instance is forced; otherwise, <c>false</c>.</value>
public bool IsForced { get; set; }

/// <summary>
/// Gets or sets a value indicating whether this instance is for the hearing impaired.
/// </summary>
/// <value><c>true</c> if this instance is for the hearing impaired; otherwise, <c>false</c>.</value>
public bool IsHearingImpaired { get; set; }
}
}
13 changes: 11 additions & 2 deletions Emby.Server.Implementations/Data/SqliteItemRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
"RpuPresentFlag",
"ElPresentFlag",
"BlPresentFlag",
"DvBlSignalCompatibilityId"
"DvBlSignalCompatibilityId",
"IsHearingImpaired"
};

private static readonly string _mediaStreamSaveColumnsInsertQuery =
Expand Down Expand Up @@ -349,7 +350,8 @@ static SqliteItemRepository()
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
{
const string CreateMediaStreamsTableCommand
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, PRIMARY KEY (ItemId, StreamIndex))";
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))";

const string CreateMediaAttachmentsTableCommand
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";

Expand Down Expand Up @@ -572,6 +574,8 @@ public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userM
AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);

AddColumn(db, "MediaStreams", "IsHearingImpaired", "TEXT", existingColumnNames);
},
TransactionMode);

Expand Down Expand Up @@ -5836,6 +5840,8 @@ private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> stream
statement.TryBind("@ElPresentFlag" + index, stream.ElPresentFlag);
statement.TryBind("@BlPresentFlag" + index, stream.BlPresentFlag);
statement.TryBind("@DvBlSignalCompatibilityId" + index, stream.DvBlSignalCompatibilityId);

statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
}

statement.Reset();
Expand Down Expand Up @@ -6047,12 +6053,15 @@ private MediaStream GetMediaStream(IReadOnlyList<ResultSetValue> reader)
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
}

item.IsHearingImpaired = reader.GetBoolean(43);

if (item.Type == MediaStreamType.Subtitle)
{
item.LocalizedUndefined = _localization.GetLocalizedString("Undefined");
item.LocalizedDefault = _localization.GetLocalizedString("Default");
item.LocalizedForced = _localization.GetLocalizedString("Forced");
item.LocalizedExternal = _localization.GetLocalizedString("External");
item.LocalizedHearingImpaired = _localization.GetLocalizedString("Hearing Impaired");
}

return item;
Expand Down
1 change: 1 addition & 0 deletions Emby.Server.Implementations/Localization/Core/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HearingImpaired": "Hearing Impaired",
"HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
Expand Down
6 changes: 6 additions & 0 deletions MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, Med
stream.LocalizedDefault = _localization.GetLocalizedString("Default");
stream.LocalizedForced = _localization.GetLocalizedString("Forced");
stream.LocalizedExternal = _localization.GetLocalizedString("External");
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("Hearing Impaired");

if (string.IsNullOrEmpty(stream.Title))
{
Expand Down Expand Up @@ -955,6 +956,11 @@ private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, Med
{
stream.IsForced = true;
}

if (disposition.GetValueOrDefault("hearing_impaired") == 1)
{
stream.IsHearingImpaired = true;
}
}

NormalizeStreamTitle(stream);
Expand Down
13 changes: 13 additions & 0 deletions MediaBrowser.Model/Entities/MediaStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ public string VideoDoViTitle

public string LocalizedExternal { get; set; }

public string LocalizedHearingImpaired { get; set; }

public string DisplayTitle
{
get
Expand Down Expand Up @@ -345,6 +347,11 @@ public string DisplayTitle
attributes.Add(string.IsNullOrEmpty(LocalizedUndefined) ? "Und" : LocalizedUndefined);
}

if (IsHearingImpaired)
{
attributes.Add(string.IsNullOrEmpty(LocalizedHearingImpaired) ? "Hearing Impaired" : LocalizedHearingImpaired);
}

if (IsDefault)
{
attributes.Add(string.IsNullOrEmpty(LocalizedDefault) ? "Default" : LocalizedDefault);
Expand Down Expand Up @@ -453,6 +460,12 @@ public string DisplayTitle
/// <value><c>true</c> if this instance is forced; otherwise, <c>false</c>.</value>
public bool IsForced { get; set; }

/// <summary>
/// Gets or sets a value indicating whether this instance is for the hearing impaired.
/// </summary>
/// <value><c>true</c> if this instance is for the hearing impaired; otherwise, <c>false</c>.</value>
public bool IsHearingImpaired { get; set; }

/// <summary>
/// Gets or sets the height.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public abstract class MediaInfoResolver
mediaStream.Index = startIndex++;
mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault;
mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced;
mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired;

mediaStreams.Add(MergeMetadata(mediaStream, pathInfo));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public void GetMediaInfo_MetaData_Success()
Assert.True(res.VideoStream.IsDefault);
Assert.False(res.VideoStream.IsExternal);
Assert.False(res.VideoStream.IsForced);
Assert.False(res.VideoStream.IsHearingImpaired);
Assert.False(res.VideoStream.IsInterlaced);
Assert.False(res.VideoStream.IsTextSubtitleStream);
Assert.Equal(13d, res.VideoStream.Level);
Expand Down Expand Up @@ -142,16 +143,19 @@ public void GetMediaInfo_Mp4MetaData_Success()
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[3].Type);
Assert.Equal("DVDSUB", res.MediaStreams[3].Codec);
Assert.Null(res.MediaStreams[3].Title);
Assert.False(res.MediaStreams[3].IsHearingImpaired);

Assert.Equal("eng", res.MediaStreams[4].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[4].Type);
Assert.Equal("mov_text", res.MediaStreams[4].Codec);
Assert.Null(res.MediaStreams[4].Title);
Assert.True(res.MediaStreams[4].IsHearingImpaired);

Assert.Equal("eng", res.MediaStreams[5].Language);
Assert.Equal(MediaStreamType.Subtitle, res.MediaStreams[5].Type);
Assert.Equal("mov_text", res.MediaStreams[5].Codec);
Assert.Equal("Commentary", res.MediaStreams[5].Title);
Assert.False(res.MediaStreams[5].IsHearingImpaired);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"hearing_impaired": 1,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
Expand Down
13 changes: 13 additions & 0 deletions tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ public class MediaStreamTests
Codec = null
});

data.Add(
"Title - EN - Hearing Impaired - Default - Forced - SRT",
new MediaStream
{
Type = MediaStreamType.Subtitle,
Title = "Title",
Language = "EN",
IsForced = true,
IsDefault = true,
IsHearingImpaired = true,
Codec = "SRT"
});

data.Add(
"Title - AAC - Default - External",
new MediaStream
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ public ExternalPathParserTests()
{
var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" });
var frenchCultureDto = new CultureDto("French", "French", "fr", new[] { "fre", "fra" });
var hindiCultureDto = new CultureDto("Hindi", "Hindi", "hi", new[] { "hin" });

var localizationManager = new Mock<ILocalizationManager>(MockBehavior.Loose);
localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase)))
.Returns(englishCultureDto);
localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"fr.*", RegexOptions.IgnoreCase)))
.Returns(frenchCultureDto);
localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"hi.*", RegexOptions.IgnoreCase)))
.Returns(hindiCultureDto);

_audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio);
_subtitlePathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Subtitle);
Expand Down Expand Up @@ -89,14 +92,19 @@ public void ParseFile_SubtitleExtensionsMatched_ReturnsPath(string path)
[InlineData(".DEFAULT.FORCED", null, null, true, true)]
[InlineData(".en", null, "eng")]
[InlineData(".EN", null, "eng")]
[InlineData(".hi", null, "hin")]
[InlineData(".fr.en", "fr", "eng")]
[InlineData(".en.fr", "en", "fre")]
[InlineData(".title.en.fr", "title.en", "fre")]
[InlineData(".Title Goes Here", "Title Goes Here", null)]
[InlineData(".Title.with.Separator", "Title.with.Separator", null)]
[InlineData(".title.en.default.forced", "title", "eng", true, true)]
[InlineData(".forced.default.en.title", "title", "eng", true, true)]
public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false)
[InlineData(".sdh.en.title", "title", "eng", false, false, true)]
[InlineData(".en.cc.title", "title", "eng", false, false, true)]
[InlineData(".hi.en.title", "title", "eng", false, false, true)]
[InlineData(".en.hi.title", "title", "eng", false, false, true)]
public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, string? language, bool isDefault = false, bool isForced = false, bool isHearingImpaired = false)
{
var path = "My.Video" + tokens + ".srt";

Expand All @@ -107,5 +115,6 @@ public void ParseFile_ExtraTokens_ParseToValues(string tokens, string? title, st
Assert.Equal(language, actual.Language);
Assert.Equal(isDefault, actual.IsDefault);
Assert.Equal(isForced, actual.IsForced);
Assert.Equal(isHearingImpaired, actual.IsHearingImpaired);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
});

// filename has metadata
file = "My.Video.Title1.default.forced.en.srt";
file = "My.Video.Title1.default.forced.sdh.en.srt";
data.Add(
file,
new[]
Expand All @@ -236,7 +236,7 @@ public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
},
new[]
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true)
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true)
});

// single stream with metadata
Expand All @@ -245,15 +245,15 @@ public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
file,
new[]
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true)
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
},
new[]
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true)
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true)
});

// stream wins for title/language, filename wins for flags when conflicting
file = "My.Video.Title2.default.forced.en.srt";
file = "My.Video.Title2.default.forced.sdh.en.srt";
data.Add(
file,
new[]
Expand All @@ -262,7 +262,7 @@ public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
},
new[]
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true)
CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true, true)
});

// multiple stream with metadata - filename flags ignored but other data filled in when missing from stream
Expand Down Expand Up @@ -324,6 +324,7 @@ public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(str
Assert.Equal(expected.Path, actual.Path);
Assert.Equal(expected.IsDefault, actual.IsDefault);
Assert.Equal(expected.IsForced, actual.IsForced);
Assert.Equal(expected.IsHearingImpaired, actual.IsHearingImpaired);
Assert.Equal(expected.Language, actual.Language);
Assert.Equal(expected.Title, actual.Title);
}
Expand Down Expand Up @@ -396,7 +397,7 @@ List<MediaStream> GenerateMediaStreams()
}
}

private static MediaStream CreateMediaStream(string path, string? language, string? title, int index, bool isForced = false, bool isDefault = false)
private static MediaStream CreateMediaStream(string path, string? language, string? title, int index, bool isForced = false, bool isDefault = false, bool isHearingImpaired = false)
{
return new MediaStream
{
Expand All @@ -405,6 +406,7 @@ private static MediaStream CreateMediaStream(string path, string? language, stri
Path = path,
IsDefault = isDefault,
IsForced = isForced,
IsHearingImpaired = isHearingImpaired,
Language = language,
Title = title
};
Expand Down