diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index c527328583..0f62e8e1eb 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -887,7 +887,7 @@ namespace MediaBrowser.Controller.Entities return Name; } - public virtual string GetInternalMetadataPath() + public string GetInternalMetadataPath() { var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs index 9329978a84..ff90eeffbc 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.Providers.MediaInfo public class AudioResolver : MediaInfoResolver { /// - /// Initializes a new instance of the class for external audio file processing. + /// Initializes a new instance of the class for external audio file processing. /// /// The localization manager. /// The media encoder. diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs index 83d5e15e29..40b45faf52 100644 --- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -43,6 +43,11 @@ namespace MediaBrowser.Providers.MediaInfo /// private readonly IMediaEncoder _mediaEncoder; + /// + /// The instance. + /// + private readonly NamingOptions _namingOptions; + /// /// The of the files this resolver should resolve. /// @@ -62,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo DlnaProfileType type) { _mediaEncoder = mediaEncoder; + _namingOptions = namingOptions; _type = type; _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type); } @@ -102,7 +108,7 @@ namespace MediaBrowser.Providers.MediaInfo if (mediaInfo.MediaStreams.Count == 1) { - MediaStream mediaStream = mediaInfo.MediaStreams.First(); + MediaStream mediaStream = mediaInfo.MediaStreams[0]; mediaStream.Index = startIndex++; mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault; mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced; @@ -159,9 +165,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var file in files) { - if (_compareInfo.IsPrefix(Path.GetFileNameWithoutExtension(file), video.FileNameWithoutExtension, CompareOptions, out int matchLength)) + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); + if (_compareInfo.IsPrefix(fileNameWithoutExtension, video.FileNameWithoutExtension, CompareOptions, out int matchLength) + && (fileNameWithoutExtension.Length == matchLength || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[matchLength].ToString()))) { - var externalPathInfo = _externalPathParser.ParseFile(file, Path.GetFileNameWithoutExtension(file)[matchLength..]); + var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[matchLength..]); if (externalPathInfo != null) { diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index 78b3836e78..289036fdab 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -12,7 +12,7 @@ namespace MediaBrowser.Providers.MediaInfo public class SubtitleResolver : MediaInfoResolver { /// - /// Initializes a new instance of the class for external subtitle file processing. + /// Initializes a new instance of the class for external subtitle file processing. /// /// The localization manager. /// The media encoder. diff --git a/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs new file mode 100644 index 0000000000..b396b54400 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/ExternalFiles/ExternalPathParserTests.cs @@ -0,0 +1,111 @@ +using System.Text.RegularExpressions; +using Emby.Naming.Common; +using Emby.Naming.ExternalFiles; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using Moq; +using Xunit; + +namespace Jellyfin.Naming.Tests.ExternalFiles; + +public class ExternalPathParserTests +{ + private readonly ExternalPathParser _audioPathParser; + private readonly ExternalPathParser _subtitlePathParser; + + public ExternalPathParserTests() + { + var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" }); + var frenchCultureDto = new CultureDto("French", "French", "fr", new[] { "fre", "fra" }); + + var localizationManager = new Mock(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); + + _audioPathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Audio); + _subtitlePathParser = new ExternalPathParser(new NamingOptions(), localizationManager.Object, DlnaProfileType.Subtitle); + } + + [Theory] + [InlineData("")] + [InlineData("MyVideo.ass")] + [InlineData("MyVideo.mks")] + [InlineData("MyVideo.sami")] + [InlineData("MyVideo.srt")] + [InlineData("MyVideo.m4v")] + public void ParseFile_AudioExtensionsNotMatched_ReturnsNull(string path) + { + Assert.Null(_audioPathParser.ParseFile(path, string.Empty)); + } + + [Theory] + [InlineData("MyVideo.aa")] + [InlineData("MyVideo.aac")] + [InlineData("MyVideo.flac")] + [InlineData("MyVideo.m4a")] + [InlineData("MyVideo.mka")] + [InlineData("MyVideo.mp3")] + public void ParseFile_AudioExtensionsMatched_ReturnsPath(string path) + { + var actual = _audioPathParser.ParseFile(path, string.Empty); + Assert.NotNull(actual); + Assert.Equal(path, actual!.Path); + } + + [Theory] + [InlineData("")] + [InlineData("MyVideo.aa")] + [InlineData("MyVideo.aac")] + [InlineData("MyVideo.flac")] + [InlineData("MyVideo.mka")] + [InlineData("MyVideo.m4v")] + public void ParseFile_SubtitleExtensionsNotMatched_ReturnsNull(string path) + { + Assert.Null(_subtitlePathParser.ParseFile(path, string.Empty)); + } + + [Theory] + [InlineData("MyVideo.ass")] + [InlineData("MyVideo.mks")] + [InlineData("MyVideo.sami")] + [InlineData("MyVideo.srt")] + [InlineData("MyVideo.vtt")] + public void ParseFile_SubtitleExtensionsMatched_ReturnsPath(string path) + { + var actual = _subtitlePathParser.ParseFile(path, string.Empty); + Assert.NotNull(actual); + Assert.Equal(path, actual!.Path); + } + + [Theory] + [InlineData("", null, null)] + [InlineData(".default", null, null, true, false)] + [InlineData(".forced", null, null, false, true)] + [InlineData(".foreign", null, null, false, true)] + [InlineData(".default.forced", null, null, true, true)] + [InlineData(".forced.default", null, null, true, true)] + [InlineData(".DEFAULT.FORCED", null, null, true, true)] + [InlineData(".en", null, "eng")] + [InlineData(".EN", null, "eng")] + [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) + { + var path = "My.Video" + tokens + ".srt"; + + var actual = _subtitlePathParser.ParseFile(path, tokens); + + Assert.NotNull(actual); + Assert.Equal(title, actual!.Title); + Assert.Equal(language, actual.Language); + Assert.Equal(isDefault, actual.IsDefault); + Assert.Equal(isForced, actual.IsForced); + } +} diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index c4f01b2712..cc3d4faa03 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs index 69f10d6700..381d6c72d5 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs @@ -1,177 +1,79 @@ -using System; using System.Collections.Generic; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Emby.Naming.Common; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Providers.MediaInfo; using Moq; using Xunit; -namespace Jellyfin.Providers.Tests.MediaInfo +namespace Jellyfin.Providers.Tests.MediaInfo; + +public class AudioResolverTests { - public class AudioResolverTests + private readonly AudioResolver _audioResolver; + + public AudioResolverTests() { - private const string VideoDirectoryPath = "Test Data/Video"; - private const string MetadataDirectoryPath = "Test Data/Metadata"; - private readonly AudioResolver _audioResolver; + // prep BaseItem and Video for calls made that expect managers + Video.LiveTvManager = Mock.Of(); - public AudioResolverTests() - { - var englishCultureDto = new CultureDto("English", "English", "en", new[] { "eng" }); + var applicationPaths = new Mock().Object; + var serverConfig = new Mock(); + serverConfig.Setup(c => c.ApplicationPaths) + .Returns(applicationPaths); + BaseItem.ConfigurationManager = serverConfig.Object; - var localizationManager = new Mock(MockBehavior.Loose); - localizationManager.Setup(lm => lm.FindLanguageInfo(It.IsRegex(@"en.*", RegexOptions.IgnoreCase))) - .Returns(englishCultureDto); + // build resolver to test with + var localizationManager = Mock.Of(); - var mediaEncoder = new Mock(MockBehavior.Strict); - mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny(), It.IsAny())) - .Returns((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo + var mediaEncoder = new Mock(MockBehavior.Strict); + mediaEncoder.Setup(me => me.GetMediaInfo(It.IsAny(), It.IsAny())) + .Returns((_, _) => Task.FromResult(new MediaBrowser.Model.MediaInfo.MediaInfo + { + MediaStreams = new List { - MediaStreams = new List - { - new() - } - })); + new() + } + })); - _audioResolver = new AudioResolver(localizationManager.Object, mediaEncoder.Object, new NamingOptions()); - } + _audioResolver = new AudioResolver(localizationManager, mediaEncoder.Object, new NamingOptions()); + } - [Fact] - public async void AddExternalStreamsAsync_GivenMixedFilenames_ReturnsValidSubtitles() + [Theory] + [InlineData("My.Video.srt", false, false)] + [InlineData("My.Video.mp3", false, true)] + [InlineData("My.Video.srt", true, false)] + [InlineData("My.Video.mp3", true, true)] + public async void GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches) + { + BaseItem.MediaSourceManager = Mock.Of(); + + var video = new Movie { - var startIndex = 0; - var index = startIndex; - var files = new[] - { - VideoDirectoryPath + "/MyVideo.en.aac", - VideoDirectoryPath + "/MyVideo.en.forced.default.dts", - VideoDirectoryPath + "/My.Video.mp3", - VideoDirectoryPath + "/Some.Other.Video.mp3", - VideoDirectoryPath + "/My.Video.png", - VideoDirectoryPath + "/My.Video.srt", - VideoDirectoryPath + "/My.Video.txt", - VideoDirectoryPath + "/My.Video.vtt", - VideoDirectoryPath + "/My.Video.ass", - VideoDirectoryPath + "/My.Video.sub", - VideoDirectoryPath + "/My.Video.ssa", - VideoDirectoryPath + "/My.Video.smi", - VideoDirectoryPath + "/My.Video.sami", - VideoDirectoryPath + "/My.Video.en.mp3", - VideoDirectoryPath + "/My.Video.en.forced.mp3", - VideoDirectoryPath + "/My.Video.en.default.forced.aac", - VideoDirectoryPath + "/My.Video.Label.mp3", - VideoDirectoryPath + "/My.Video.With Additional Garbage.en.aac", - VideoDirectoryPath + "/My.Video.With.Additional.Garbage.en.mp3" - }; - var metadataFiles = new[] - { - MetadataDirectoryPath + "/My.Video.en.aac" - }; - var expectedResult = new[] - { - CreateMediaStream(VideoDirectoryPath + "/MyVideo.en.aac", "eng", null, index++), - CreateMediaStream(VideoDirectoryPath + "/MyVideo.en.forced.default.dts", "eng", null, index++, isDefault: true, isForced: true), - CreateMediaStream(VideoDirectoryPath + "/My.Video.mp3", null, null, index++), - CreateMediaStream(VideoDirectoryPath + "/My.Video.en.mp3", "eng", null, index++), - CreateMediaStream(VideoDirectoryPath + "/My.Video.en.forced.mp3", "eng", null, index++, isDefault: false, isForced: true), - CreateMediaStream(VideoDirectoryPath + "/My.Video.en.default.forced.aac", "eng", null, index++, isDefault: true, isForced: true), - CreateMediaStream(VideoDirectoryPath + "/My.Video.Label.mp3", null, "Label", index++), - CreateMediaStream(VideoDirectoryPath + "/My.Video.With Additional Garbage.en.aac", "eng", "With Additional Garbage", index++), - CreateMediaStream(VideoDirectoryPath + "/My.Video.With.Additional.Garbage.en.mp3", "eng", "With.Additional.Garbage", index++), - CreateMediaStream(MetadataDirectoryPath + "/My.Video.en.aac", "eng", null, index) - }; + Path = MediaInfoResolverTests.VideoDirectoryPath + "/My.Video.mkv" + }; - BaseItem.MediaSourceManager = Mock.Of(); + var directoryService = MediaInfoResolverTests.GetDirectoryServiceForExternalFile(file, metadataDirectory); + var streams = await _audioResolver.GetExternalStreamsAsync(video, 0, directoryService, false, CancellationToken.None); - var video = new Mock