Add hearing impaired subtitle stream indicator (#7379)

Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
This commit is contained in:
Joe Rogers 2022-10-07 06:38:05 -04:00 committed by GitHub
parent 3612b427c4
commit 2e4db18ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 102 additions and 12 deletions

View File

@ -280,6 +280,13 @@ namespace Emby.Naming.Common
"default"
};
MediaHearingImpairedFlags = new[]
{
"cc",
"hi",
"sdh"
};
EpisodeExpressions = new[]
{
// *** Begin Kodi Standard Naming
@ -727,6 +734,11 @@ namespace Emby.Naming.Common
/// </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>

View File

@ -99,6 +99,18 @@ namespace Emby.Naming.ExternalFiles
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;

View File

@ -11,11 +11,13 @@ namespace Emby.Naming.ExternalFiles
/// <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>
@ -47,5 +49,11 @@ namespace Emby.Naming.ExternalFiles
/// </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; }
}
}

View File

@ -178,7 +178,8 @@ namespace Emby.Server.Implementations.Data
"RpuPresentFlag",
"ElPresentFlag",
"BlPresentFlag",
"DvBlSignalCompatibilityId"
"DvBlSignalCompatibilityId",
"IsHearingImpaired"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
@ -349,7 +350,8 @@ namespace Emby.Server.Implementations.Data
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))";
@ -572,6 +574,8 @@ namespace Emby.Server.Implementations.Data
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);
@ -5836,6 +5840,8 @@ AND Type = @InternalPersonType)");
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();
@ -6047,12 +6053,15 @@ AND Type = @InternalPersonType)");
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;

View File

@ -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",

View File

@ -730,6 +730,7 @@ namespace MediaBrowser.MediaEncoding.Probing
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))
{
@ -955,6 +956,11 @@ namespace MediaBrowser.MediaEncoding.Probing
{
stream.IsForced = true;
}
if (disposition.GetValueOrDefault("hearing_impaired") == 1)
{
stream.IsHearingImpaired = true;
}
}
NormalizeStreamTitle(stream);

View File

@ -221,6 +221,8 @@ namespace MediaBrowser.Model.Entities
public string LocalizedExternal { get; set; }
public string LocalizedHearingImpaired { get; set; }
public string DisplayTitle
{
get
@ -345,6 +347,11 @@ namespace MediaBrowser.Model.Entities
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);
@ -453,6 +460,12 @@ namespace MediaBrowser.Model.Entities
/// <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>

View File

@ -120,6 +120,7 @@ namespace MediaBrowser.Providers.MediaInfo
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));
}

View File

@ -65,6 +65,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
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);
@ -142,16 +143,19 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
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]

View File

@ -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,

View File

@ -82,6 +82,19 @@ namespace Jellyfin.Model.Tests.Entities
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

View File

@ -17,12 +17,15 @@ public class 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);
@ -89,6 +92,7 @@ public class ExternalPathParserTests
[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")]
@ -96,7 +100,11 @@ public class ExternalPathParserTests
[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";
@ -107,5 +115,6 @@ public class ExternalPathParserTests
Assert.Equal(language, actual.Language);
Assert.Equal(isDefault, actual.IsDefault);
Assert.Equal(isForced, actual.IsForced);
Assert.Equal(isHearingImpaired, actual.IsHearingImpaired);
}
}

View File

@ -227,7 +227,7 @@ public class MediaInfoResolverTests
});
// filename has metadata
file = "My.Video.Title1.default.forced.en.srt";
file = "My.Video.Title1.default.forced.sdh.en.srt";
data.Add(
file,
new[]
@ -236,7 +236,7 @@ public class MediaInfoResolverTests
},
new[]
{
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true)
CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true)
});
// single stream with metadata
@ -245,15 +245,15 @@ public class MediaInfoResolverTests
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[]
@ -262,7 +262,7 @@ public class MediaInfoResolverTests
},
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
@ -324,6 +324,7 @@ public class MediaInfoResolverTests
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);
}
@ -396,7 +397,7 @@ public class MediaInfoResolverTests
}
}
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
{
@ -405,6 +406,7 @@ public class MediaInfoResolverTests
Path = path,
IsDefault = isDefault,
IsForced = isForced,
IsHearingImpaired = isHearingImpaired,
Language = language,
Title = title
};