diff options
15 files changed, 594 insertions, 913 deletions
diff --git a/Emby.Naming/Audio/ExternalAudioFilePathParser.cs b/Emby.Naming/Audio/ExternalAudioFilePathParser.cs deleted file mode 100644 index ab5af9fc6..000000000 --- a/Emby.Naming/Audio/ExternalAudioFilePathParser.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Emby.Naming.Common; -using Jellyfin.Extensions; - -namespace Emby.Naming.Audio -{ - /// <summary> - /// External Audio Parser class. - /// </summary> - public class ExternalAudioFilePathParser - { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="ExternalAudioFilePathParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing AudioFileExtensions, ExternalAudioDefaultFlags, ExternalAudioForcedFlags and ExternalAudioFlagDelimiters.</param> - public ExternalAudioFilePathParser(NamingOptions options) - { - _options = options; - } - - /// <summary> - /// Parse file to determine if it is a ExternalAudio and <see cref="ExternalAudioFileInfo"/>. - /// </summary> - /// <param name="path">Path to file.</param> - /// <returns>Returns null or <see cref="ExternalAudioFileInfo"/> object if parsing is successful.</returns> - public ExternalAudioFileInfo? ParseFile(string path) - { - if (path.Length == 0) - { - return null; - } - - var extension = Path.GetExtension(path); - if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var flags = GetFileFlags(path); - var info = new ExternalAudioFileInfo( - path, - _options.ExternalAudioDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)), - _options.ExternalAudioForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase))); - - return info; - } - - private string[] GetFileFlags(string path) - { - var file = Path.GetFileNameWithoutExtension(path); - - return file.Split(_options.ExternalAudioFlagDelimiters, StringSplitOptions.RemoveEmptyEntries); - } - } -} diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 82a3ad2b7..a79153e86 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -23,47 +23,60 @@ namespace Emby.Naming.Common { VideoFileExtensions = new[] { - ".m4v", + ".001", + ".3g2", ".3gp", - ".nsv", - ".ts", - ".ty", - ".strm", - ".rm", - ".rmvb", - ".ifo", - ".mov", - ".qt", - ".divx", - ".xvid", - ".bivx", - ".vob", - ".nrg", - ".img", - ".iso", - ".pva", - ".wmv", + ".amv", ".asf", ".asx", - ".ogm", - ".m2v", ".avi", ".bin", + ".bivx", + ".divx", + ".dv", ".dvr-ms", - ".mpg", - ".mpeg", - ".mp4", + ".f4v", + ".fli", + ".flv", + ".ifo", + ".img", + ".iso", + ".m2t", + ".m2ts", + ".m2v", + ".m4v", ".mkv", - ".avc", - ".vp3", - ".svq3", + ".mk3d", + ".mov", + ".mp2", + ".mp4", + ".mpe", + ".mpeg", + ".mpg", + ".mts", + ".mxf", + ".nrg", + ".nsv", ".nuv", + ".ogg", + ".ogm", + ".ogv", + ".pva", + ".qt", + ".rec", + ".rm", + ".rmvb", + ".svq3", + ".tp", + ".ts", + ".ty", ".viv", - ".dv", - ".fli", - ".flv", - ".001", - ".tp" + ".vob", + ".vp3", + ".webm", + ".wmv", + ".wtv", + ".xvid" }; VideoFlagDelimiters = new[] @@ -150,35 +163,19 @@ namespace Emby.Naming.Common SubtitleFileExtensions = new[] { ".ass", - ".smi", + ".mks", ".sami", + ".smi", ".srt", ".ssa", ".sub", ".vtt", - ".mks" - }; - - SubtitleFlagDelimiters = new[] - { - '.' - }; - - SubtitleForcedFlags = new[] - { - "foreign", - "forced" - }; - - SubtitleDefaultFlags = new[] - { - "default" }; AlbumStackingPrefixes = new[] { - "disc", "cd", + "disc", "disk", "vol", "volume" @@ -186,82 +183,99 @@ namespace Emby.Naming.Common AudioFileExtensions = new[] { - ".nsv", - ".m4a", - ".flac", + ".669", + ".3gp", + ".aa", ".aac", - ".strm", - ".pls", - ".rm", - ".mpa", - ".wav", - ".wma", - ".ogg", - ".opus", - ".mp3", - ".mp2", - ".mod", + ".aax", + ".ac3", + ".act", + ".adp", + ".adplug", + ".adx", + ".afc", ".amf", - ".669", + ".aif", + ".aiff", + ".alac", + ".amr", + ".ape", + ".ast", + ".au", + ".awb", + ".cda", + ".cue", ".dmf", + ".dsf", ".dsm", + ".dsp", + ".dts", + ".dvf", ".far", + ".flac", ".gdm", + ".gsm", + ".gym", + ".hps", ".imf", ".it", ".m15", + ".m4a", + ".m4b", + ".mac", ".med", + ".mka", + ".mmf", + ".mod", + ".mogg", + ".mp2", + ".mp3", + ".mpa", + ".mpc", + ".mpp", + ".mp+", + ".msv", + ".nmf", + ".nsf", + ".nsv", + ".oga", + ".ogg", ".okt", + ".opus", + ".pls", + ".ra", + ".rf64", + ".rm", ".s3m", - ".stm", ".sfx", + ".shn", + ".sid", + ".spc", + ".stm", + ".strm", ".ult", ".uni", - ".xm", - ".sid", - ".ac3", - ".dts", - ".cue", - ".aif", - ".aiff", - ".ape", - ".mac", - ".mpc", - ".mp+", - ".mpp", - ".shn", + ".vox", + ".wav", + ".wma", ".wv", - ".nsf", - ".spc", - ".gym", - ".adplug", - ".adx", - ".dsp", - ".adp", - ".ymf", - ".ast", - ".afc", - ".hps", + ".xm", ".xsp", - ".acc", - ".m4b", - ".oga", - ".dsf", - ".mka" + ".ymf" }; - ExternalAudioFlagDelimiters = new[] + MediaFlagDelimiters = new[] { - '.' + "." }; - ExternalAudioForcedFlags = new[] + MediaForcedFlags = new[] { "foreign", "forced" }; - ExternalAudioDefaultFlags = new[] + MediaDefaultFlags = new[] { "default" }; @@ -668,39 +682,6 @@ namespace Emby.Naming.Common @"^\s*(?<name>[^ ].*?)\s*$" }; - VideoFileExtensions = new[] - { - ".mkv", - ".m2t", - ".m2ts", - ".img", - ".iso", - ".mk3d", - ".ts", - ".rmvb", - ".mov", - ".avi", - ".mpg", - ".mpeg", - ".wmv", - ".mp4", - ".divx", - ".dvr-ms", - ".wtv", - ".ogm", - ".ogv", - ".asf", - ".m4v", - ".flv", - ".f4v", - ".3gp", - ".webm", - ".mts", - ".m2v", - ".rec", - ".mxf" - }; - MultipleEpisodeExpressions = new[] { @".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$", @@ -732,19 +713,19 @@ namespace Emby.Naming.Common public string[] AudioFileExtensions { get; set; } /// <summary> - /// Gets or sets list of external audio flag delimiters. + /// Gets or sets list of external media flag delimiters. /// </summary> - public char[] ExternalAudioFlagDelimiters { get; set; } + public string[] MediaFlagDelimiters { get; set; } /// <summary> - /// Gets or sets list of external audio forced flags. + /// Gets or sets list of external media forced flags. /// </summary> - public string[] ExternalAudioForcedFlags { get; set; } + public string[] MediaForcedFlags { get; set; } /// <summary> - /// Gets or sets list of external audio default flags. + /// Gets or sets list of external media default flags. /// </summary> - public string[] ExternalAudioDefaultFlags { get; set; } + public string[] MediaDefaultFlags { get; set; } /// <summary> /// Gets or sets list of album stacking prefixes. @@ -757,21 +738,6 @@ namespace Emby.Naming.Common public string[] SubtitleFileExtensions { get; set; } /// <summary> - /// Gets or sets list of subtitles flag delimiters. - /// </summary> - public char[] SubtitleFlagDelimiters { get; set; } - - /// <summary> - /// Gets or sets list of subtitle forced flags. - /// </summary> - public string[] SubtitleForcedFlags { get; set; } - - /// <summary> - /// Gets or sets list of subtitle default flags. - /// </summary> - public string[] SubtitleDefaultFlags { get; set; } - - /// <summary> /// Gets or sets list of episode regular expressions. /// </summary> public EpisodeExpression[] EpisodeExpressions { get; set; } diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs new file mode 100644 index 000000000..7b5767b67 --- /dev/null +++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Linq; +using Emby.Naming.Common; +using Jellyfin.Extensions; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; + +namespace Emby.Naming.ExternalFiles +{ + /// <summary> + /// External file parser class. + /// </summary> + public class ExternalPathParser + { + private readonly NamingOptions _namingOptions; + private readonly DlnaProfileType _type; + private readonly ILocalizationManager _localizationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ExternalPathParser"/> class. + /// </summary> + /// <param name="localizationManager">The localization manager.</param> + /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> + /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param> + public ExternalPathParser(NamingOptions namingOptions, ILocalizationManager localizationManager, DlnaProfileType type) + { + _localizationManager = localizationManager; + _namingOptions = namingOptions; + _type = type; + } + + /// <summary> + /// Parse filename and extract information. + /// </summary> + /// <param name="path">Path to file.</param> + /// <param name="extraString">Part of the filename only containing the extra information.</param> + /// <returns>Returns null or an <see cref="ExternalPathParserResult"/> object if parsing is successful.</returns> + public ExternalPathParserResult? ParseFile(string path, string? extraString) + { + if (path.Length == 0) + { + return null; + } + + var extension = Path.GetExtension(path); + if (!((_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + || (_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + || (_type == DlnaProfileType.Video && _namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))) + { + return null; + } + + var pathInfo = new ExternalPathParserResult(path); + + if (string.IsNullOrEmpty(extraString)) + { + return pathInfo; + } + + foreach (var separator in _namingOptions.MediaFlagDelimiters) + { + var languageString = extraString; + var titleString = string.Empty; + int separatorLength = separator.Length; + + while (languageString.Length > 0) + { + var lastSeparator = languageString.LastIndexOf(separator, StringComparison.OrdinalIgnoreCase); + + if (lastSeparator == -1) + { + break; + } + + string currentSlice = languageString[lastSeparator..]; + + if (_namingOptions.MediaDefaultFlags.Any(s => currentSlice[separatorLength..].Contains(s, StringComparison.OrdinalIgnoreCase))) + { + pathInfo.IsDefault = true; + extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); + languageString = languageString[..lastSeparator]; + continue; + } + + if (_namingOptions.MediaForcedFlags.Any(s => currentSlice[separatorLength..].Contains(s, StringComparison.OrdinalIgnoreCase))) + { + pathInfo.IsForced = true; + extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); + languageString = languageString[..lastSeparator]; + continue; + } + + // Try to translate to three character code + var culture = _localizationManager.FindLanguageInfo(currentSlice[separatorLength..]); + + if (culture != null && pathInfo.Language == null) + { + pathInfo.Language = culture.ThreeLetterISOLanguageName; + extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase); + } + else + { + titleString = currentSlice + titleString; + } + + languageString = languageString[..lastSeparator]; + } + + pathInfo.Title = separatorLength <= titleString.Length ? titleString[separatorLength..] : null; + } + + return pathInfo; + } + } +} diff --git a/Emby.Naming/Audio/ExternalAudioFileInfo.cs b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs index 4d02939cb..1cc773a2e 100644 --- a/Emby.Naming/Audio/ExternalAudioFileInfo.cs +++ b/Emby.Naming/ExternalFiles/ExternalPathParserResult.cs @@ -1,17 +1,17 @@ -namespace Emby.Naming.Audio +namespace Emby.Naming.ExternalFiles { /// <summary> - /// Class holding information about external audio files. + /// Class holding information about external files. /// </summary> - public class ExternalAudioFileInfo + public class ExternalPathParserResult { /// <summary> - /// Initializes a new instance of the <see cref="ExternalAudioFileInfo"/> class. + /// Initializes a new instance of the <see cref="ExternalPathParserResult"/> class. /// </summary> /// <param name="path">Path to file.</param> /// <param name="isDefault">Is default.</param> /// <param name="isForced">Is forced.</param> - public ExternalAudioFileInfo(string path, bool isDefault, bool isForced) + public ExternalPathParserResult(string path, bool isDefault = false, bool isForced = false) { Path = path; IsDefault = isDefault; @@ -42,7 +42,6 @@ namespace Emby.Naming.Audio /// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value> public bool IsDefault { get; set; } - /// <summary> /// Gets or sets a value indicating whether this instance is forced. /// </summary> diff --git a/Emby.Naming/Subtitles/SubtitleFileInfo.cs b/Emby.Naming/Subtitles/SubtitleFileInfo.cs deleted file mode 100644 index ed9ab3ebd..000000000 --- a/Emby.Naming/Subtitles/SubtitleFileInfo.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Emby.Naming.Subtitles -{ - /// <summary> - /// Class holding information about subtitle. - /// </summary> - public class SubtitleFileInfo - { - /// <summary> - /// Initializes a new instance of the <see cref="SubtitleFileInfo"/> class. - /// </summary> - /// <param name="path">Path to file.</param> - /// <param name="isDefault">Is subtitle default.</param> - /// <param name="isForced">Is subtitle forced.</param> - public SubtitleFileInfo(string path, bool isDefault, bool isForced) - { - Path = path; - IsDefault = isDefault; - IsForced = isForced; - } - - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - public string Path { get; set; } - - /// <summary> - /// Gets or sets the language. - /// </summary> - /// <value>The language.</value> - public string? Language { get; set; } - - /// <summary> - /// Gets or sets the title. - /// </summary> - /// <value>The title.</value> - public string? Title { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is default. - /// </summary> - /// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value> - public bool IsDefault { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is forced. - /// </summary> - /// <value><c>true</c> if this instance is forced; otherwise, <c>false</c>.</value> - public bool IsForced { get; set; } - } -} diff --git a/Emby.Naming/Subtitles/SubtitleFilePathParser.cs b/Emby.Naming/Subtitles/SubtitleFilePathParser.cs deleted file mode 100644 index 7b2adf3f5..000000000 --- a/Emby.Naming/Subtitles/SubtitleFilePathParser.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using Emby.Naming.Common; -using Jellyfin.Extensions; - -namespace Emby.Naming.Subtitles -{ - /// <summary> - /// Subtitle Parser class. - /// </summary> - public class SubtitleFilePathParser - { - private readonly NamingOptions _options; - - /// <summary> - /// Initializes a new instance of the <see cref="SubtitleFilePathParser"/> class. - /// </summary> - /// <param name="options"><see cref="NamingOptions"/> object containing SubtitleFileExtensions, SubtitleDefaultFlags, SubtitleForcedFlags and SubtitleFlagDelimiters.</param> - public SubtitleFilePathParser(NamingOptions options) - { - _options = options; - } - - /// <summary> - /// Parse file to determine if it is a subtitle and <see cref="SubtitleFileInfo"/>. - /// </summary> - /// <param name="path">Path to file.</param> - /// <returns>Returns null or <see cref="SubtitleFileInfo"/> object if parsing is successful.</returns> - public SubtitleFileInfo? ParseFile(string path) - { - if (path.Length == 0) - { - return null; - } - - var extension = Path.GetExtension(path); - if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - var flags = GetFileFlags(path); - var info = new SubtitleFileInfo( - path, - _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)), - _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase))); - - return info; - } - - private string[] GetFileFlags(string path) - { - var file = Path.GetFileNameWithoutExtension(path); - - return file.Split(_options.SubtitleFlagDelimiters, StringSplitOptions.RemoveEmptyEntries); - } - } -} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 0f62e8e1e..c52732858 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -887,7 +887,7 @@ namespace MediaBrowser.Controller.Entities return Name; } - public string GetInternalMetadataPath() + public virtual string GetInternalMetadataPath() { var basePath = ConfigurationManager.ApplicationPaths.InternalMetadataPath; diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs deleted file mode 100644 index 745738f75..000000000 --- a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Emby.Naming.Audio; -using Emby.Naming.Common; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.MediaInfo; - -namespace MediaBrowser.Providers.MediaInfo -{ - /// <summary> - /// Resolves external audios for videos. - /// </summary> - public class AudioResolver - { - private readonly ILocalizationManager _localizationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly NamingOptions _namingOptions; - private readonly ExternalAudioFilePathParser _externalAudioFilePathParser; - private readonly CompareInfo _compareInfo = CultureInfo.InvariantCulture.CompareInfo; - private const CompareOptions CompareOptions = System.Globalization.CompareOptions.IgnoreCase | System.Globalization.CompareOptions.IgnoreNonSpace | System.Globalization.CompareOptions.IgnoreSymbols; - - /// <summary> - /// Initializes a new instance of the <see cref="AudioResolver"/> class. - /// </summary> - /// <param name="localizationManager">The localization manager.</param> - /// <param name="mediaEncoder">The media encoder.</param> - /// <param name="namingOptions">The naming options.</param> - public AudioResolver( - ILocalizationManager localizationManager, - IMediaEncoder mediaEncoder, - NamingOptions namingOptions) - { - _localizationManager = localizationManager; - _mediaEncoder = mediaEncoder; - _namingOptions = namingOptions; - _externalAudioFilePathParser = new ExternalAudioFilePathParser(_namingOptions); - } - - /// <summary> - /// Returns the audio streams found in the external audio files for the given video. - /// </summary> - /// <param name="video">The video to get the external audio streams from.</param> - /// <param name="startIndex">The stream index to start adding audio streams at.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>A list of external audio streams.</returns> - public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams( - Video video, - int startIndex, - IDirectoryService directoryService, - bool clearCache, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!video.IsFileProtocol) - { - yield break; - } - - string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); - - var externalAudioFileInfos = GetExternalAudioFiles(video, directoryService, clearCache); - foreach (var externalAudioFileInfo in externalAudioFileInfos) - { - string fileName = Path.GetFileName(externalAudioFileInfo.Path); - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(externalAudioFileInfo.Path); - Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(externalAudioFileInfo.Path, cancellationToken).ConfigureAwait(false); - - if (mediaInfo.MediaStreams.Count == 1) - { - MediaStream mediaStream = mediaInfo.MediaStreams.First(); - mediaStream.Index = startIndex++; - mediaStream.Type = MediaStreamType.Audio; - mediaStream.IsExternal = true; - mediaStream.Path = externalAudioFileInfo.Path; - mediaStream.IsDefault = externalAudioFileInfo.IsDefault || mediaStream.IsDefault; - mediaStream.IsForced = externalAudioFileInfo.IsForced || mediaStream.IsForced; - - yield return DetectLanguage(mediaStream, fileNameWithoutExtension, videoFileNameWithoutExtension); - } - else - { - foreach (MediaStream mediaStream in mediaInfo.MediaStreams) - { - mediaStream.Index = startIndex++; - mediaStream.Type = MediaStreamType.Audio; - mediaStream.IsExternal = true; - mediaStream.Path = externalAudioFileInfo.Path; - - yield return DetectLanguage(mediaStream, fileNameWithoutExtension, videoFileNameWithoutExtension); - } - } - } - } - - /// <summary> - /// Returns the external audio file paths for the given video. - /// </summary> - /// <param name="video">The video to get the external audio file paths from.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <returns>A list of external audio file paths.</returns> - public IEnumerable<ExternalAudioFileInfo> GetExternalAudioFiles( - Video video, - IDirectoryService directoryService, - bool clearCache) - { - if (!video.IsFileProtocol) - { - yield break; - } - - // Check if video folder exists - string folder = video.ContainingFolderPath; - if (!Directory.Exists(folder)) - { - yield break; - } - - var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); - - var files = directoryService.GetFilePaths(folder, clearCache, true); - for (int i = 0; i < files.Count; i++) - { - var subtitleFileInfo = _externalAudioFilePathParser.ParseFile(files[i]); - - if (subtitleFileInfo == null) - { - continue; - } - - yield return subtitleFileInfo; - } - } - - /// <summary> - /// Returns the media info of the given audio file. - /// </summary> - /// <param name="path">The path to the audio file.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>The media info for the given audio file.</returns> - private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - return _mediaEncoder.GetMediaInfo( - new MediaInfoRequest - { - MediaType = DlnaProfileType.Audio, - MediaSource = new MediaSourceInfo - { - Path = path, - Protocol = MediaProtocol.File - } - }, - cancellationToken); - } - - private MediaStream DetectLanguage(MediaStream mediaStream, string fileNameWithoutExtension, string videoFileNameWithoutExtension) - { - // Support xbmc naming conventions - 300.spanish.srt - var languageString = fileNameWithoutExtension; - while (languageString.Length > 0) - { - var lastDot = languageString.LastIndexOf('.'); - if (lastDot < videoFileNameWithoutExtension.Length) - { - break; - } - - var currentSlice = languageString[lastDot..]; - languageString = languageString[..lastDot]; - - if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) - || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) - || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var currentSliceString = currentSlice[1..]; - - // Try to translate to three character code - var culture = _localizationManager.FindLanguageInfo(currentSliceString); - - if (culture == null || mediaStream.Language != null) - { - if (mediaStream.Title == null) - { - mediaStream.Title = currentSliceString; - } - } - else - { - mediaStream.Language = culture.ThreeLetterISOLanguageName; - } - } - - return mediaStream; - } - } -} diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index 3a819ff7c..dab06cc08 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.MediaInfo; @@ -39,8 +40,8 @@ namespace MediaBrowser.Providers.MediaInfo IHasItemChangeMonitor { private readonly ILogger<FFProbeProvider> _logger; - private readonly SubtitleResolver _subtitleResolver; - private readonly AudioResolver _audioResolver; + private readonly MediaInfoResolver _subtitleResolver; + private readonly MediaInfoResolver _audioResolver; private readonly FFProbeVideoInfo _videoProber; private readonly FFProbeAudioInfo _audioProber; private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); @@ -60,8 +61,8 @@ namespace MediaBrowser.Providers.MediaInfo NamingOptions namingOptions) { _logger = logger; - _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions); - _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager, mediaEncoder, namingOptions); + _audioResolver = new MediaInfoResolver(localization, mediaEncoder, namingOptions, DlnaProfileType.Audio); + _subtitleResolver = new MediaInfoResolver(localization, mediaEncoder, namingOptions, DlnaProfileType.Subtitle); _videoProber = new FFProbeVideoInfo( _logger, mediaSourceManager, @@ -104,7 +105,7 @@ namespace MediaBrowser.Providers.MediaInfo if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder && !video.SubtitleFiles.SequenceEqual( - _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false) + _subtitleResolver.GetExternalFiles(video, directoryService, false) .Select(info => info.Path).ToList(), StringComparer.Ordinal)) { @@ -114,7 +115,7 @@ namespace MediaBrowser.Providers.MediaInfo if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder && !video.AudioFiles.SequenceEqual( - _audioResolver.GetExternalAudioFiles(video, directoryService, false) + _audioResolver.GetExternalFiles(video, directoryService, false) .Select(info => info.Path).ToList(), StringComparer.Ordinal)) { diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index fa02874f2..cce9b5aeb 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -44,8 +44,8 @@ namespace MediaBrowser.Providers.MediaInfo private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; - private readonly AudioResolver _audioResolver; - private readonly SubtitleResolver _subtitleResolver; + private readonly MediaInfoResolver _audioResolver; + private readonly MediaInfoResolver _subtitleResolver; private readonly IMediaSourceManager _mediaSourceManager; private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; @@ -62,8 +62,8 @@ namespace MediaBrowser.Providers.MediaInfo ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager, - SubtitleResolver subtitleResolver, - AudioResolver audioResolver) + MediaInfoResolver subtitleResolver, + MediaInfoResolver audioResolver) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -536,7 +536,7 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) { var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1); - var externalSubtitleStreamsAsync = _subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false, cancellationToken); + var externalSubtitleStreamsAsync = _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken); List<MediaStream> externalSubtitleStreams = new List<MediaStream>(); @@ -597,7 +597,7 @@ namespace MediaBrowser.Providers.MediaInfo // Rescan if (downloadedLanguages.Count > 0) { - await foreach (MediaStream externalSubtitleStream in _subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, true, cancellationToken)) + await foreach (MediaStream externalSubtitleStream in _subtitleResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, true, cancellationToken)) { externalSubtitleStreams.Add(externalSubtitleStream); } @@ -623,7 +623,7 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) { var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1; - var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken); + var externalAudioStreams = _audioResolver.GetExternalStreamsAsync(video, startIndex, options.DirectoryService, false, cancellationToken); await foreach (MediaStream externalAudioStream in externalAudioStreams) { diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs new file mode 100644 index 000000000..51cf6d133 --- /dev/null +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Emby.Naming.Common; +using Emby.Naming.ExternalFiles; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.MediaInfo; + +namespace MediaBrowser.Providers.MediaInfo +{ + /// <summary> + /// Resolves external files for videos. + /// </summary> + public class MediaInfoResolver + { + /// <summary> + /// The <see cref="CompareOptions"/> instance. + /// </summary> + private const CompareOptions CompareOptions = System.Globalization.CompareOptions.IgnoreCase | System.Globalization.CompareOptions.IgnoreNonSpace | System.Globalization.CompareOptions.IgnoreSymbols; + + /// <summary> + /// The <see cref="CompareInfo"/> instance. + /// </summary> + private readonly CompareInfo _compareInfo = CultureInfo.InvariantCulture.CompareInfo; + + /// <summary> + /// The <see cref="ExternalPathParser"/> instance. + /// </summary> + private readonly ExternalPathParser _externalPathParser; + + /// <summary> + /// The <see cref="IMediaEncoder"/> instance. + /// </summary> + private readonly IMediaEncoder _mediaEncoder; + + /// <summary> + /// The <see cref="DlnaProfileType"/> of the files this resolver should resolve. + /// </summary> + private readonly DlnaProfileType _type; + + /// <summary> + /// Initializes a new instance of the <see cref="MediaInfoResolver"/> class. + /// </summary> + /// <param name="localizationManager">The localization manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="namingOptions">The <see cref="NamingOptions"/> object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters.</param> + /// <param name="type">The <see cref="DlnaProfileType"/> of the parsed file.</param> + public MediaInfoResolver( + ILocalizationManager localizationManager, + IMediaEncoder mediaEncoder, + NamingOptions namingOptions, + DlnaProfileType type) + { + _mediaEncoder = mediaEncoder; + _type = type; + _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type); + } + + /// <summary> + /// Retrieves the external streams for the provided video. + /// </summary> + /// <param name="video">The <see cref="Video"/> object to search external streams for.</param> + /// <param name="startIndex">The stream index to start adding external streams at.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>The external streams located.</returns> + public async IAsyncEnumerable<MediaStream> GetExternalStreamsAsync( + Video video, + int startIndex, + IDirectoryService directoryService, + bool clearCache, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!video.IsFileProtocol) + { + yield break; + } + + var pathInfos = GetExternalFiles(video, directoryService, clearCache); + + foreach (var pathInfo in pathInfos) + { + Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false); + + if (mediaInfo.MediaStreams.Count == 1) + { + MediaStream mediaStream = mediaInfo.MediaStreams.First(); + mediaStream.Index = startIndex++; + mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault; + mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced; + + yield return MergeMetadata(mediaStream, pathInfo); + } + else + { + foreach (MediaStream mediaStream in mediaInfo.MediaStreams) + { + mediaStream.Index = startIndex++; + + yield return MergeMetadata(mediaStream, pathInfo); + } + } + } + } + + /// <summary> + /// Returns the external file infos for the given video. + /// </summary> + /// <param name="video">The <see cref="Video"/> object to search external files for.</param> + /// <param name="directoryService">The directory service to search for files.</param> + /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> + /// <returns>The external file paths located.</returns> + public IEnumerable<ExternalPathParserResult> GetExternalFiles( + Video video, + IDirectoryService directoryService, + bool clearCache) + { + if (!video.IsFileProtocol) + { + yield break; + } + + // Check if video folder exists + string folder = video.ContainingFolderPath; + if (!Directory.Exists(folder)) + { + yield break; + } + + var files = directoryService.GetFilePaths(folder, clearCache).ToList(); + files.AddRange(directoryService.GetFilePaths(video.GetInternalMetadataPath(), clearCache)); + + foreach (var file in files) + { + if (_compareInfo.IsPrefix(Path.GetFileNameWithoutExtension(file), video.FileNameWithoutExtension, CompareOptions, out int matchLength)) + { + var externalPathInfo = _externalPathParser.ParseFile(file, Path.GetFileNameWithoutExtension(file)[matchLength..]); + + if (externalPathInfo != null) + { + yield return externalPathInfo; + } + } + } + } + + /// <summary> + /// Returns the media info of the given file. + /// </summary> + /// <param name="path">The path to the file.</param> + /// <param name="type">The <see cref="DlnaProfileType"/>.</param> + /// <param name="cancellationToken">The cancellation token to cancel operation.</param> + /// <returns>The media info for the given file.</returns> + private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, DlnaProfileType type, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return _mediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaType = type, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = MediaProtocol.File + } + }, + cancellationToken); + } + + /// <summary> + /// Merges path metadata into stream metadata. + /// </summary> + /// <param name="mediaStream">The <see cref="MediaStream"/> object.</param> + /// <param name="pathInfo">The <see cref="ExternalPathParserResult"/> object.</param> + /// <returns>The modified mediaStream.</returns> + private MediaStream MergeMetadata(MediaStream mediaStream, ExternalPathParserResult pathInfo) + { + mediaStream.Path = pathInfo.Path; + mediaStream.IsExternal = true; + mediaStream.Title = string.IsNullOrEmpty(mediaStream.Title) ? (string.IsNullOrEmpty(pathInfo.Title) ? null : pathInfo.Title) : mediaStream.Title; + mediaStream.Language = string.IsNullOrEmpty(mediaStream.Language) ? (string.IsNullOrEmpty(pathInfo.Language) ? null : pathInfo.Language) : mediaStream.Language; + + mediaStream.Type = _type switch + { + DlnaProfileType.Audio => MediaStreamType.Audio, + DlnaProfileType.Subtitle => MediaStreamType.Subtitle, + DlnaProfileType.Video => MediaStreamType.Video, + _ => mediaStream.Type + }; + + return mediaStream; + } + } +} diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs deleted file mode 100644 index 15beea39a..000000000 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Emby.Naming.Common; -using Emby.Naming.Subtitles; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.MediaInfo; - - -namespace MediaBrowser.Providers.MediaInfo -{ - /// <summary> - /// Resolves external subtitles for videos. - /// </summary> - public class SubtitleResolver - { - private readonly ILocalizationManager _localizationManager; - private readonly IMediaEncoder _mediaEncoder; - private readonly NamingOptions _namingOptions; - private readonly SubtitleFilePathParser _subtitleFilePathParser; - private readonly CompareInfo _compareInfo = CultureInfo.InvariantCulture.CompareInfo; - private const CompareOptions CompareOptions = System.Globalization.CompareOptions.IgnoreCase | System.Globalization.CompareOptions.IgnoreNonSpace | System.Globalization.CompareOptions.IgnoreSymbols; - - /// <summary> - /// Initializes a new instance of the <see cref="SubtitleResolver"/> class. - /// </summary> - /// <param name="localization">The localization manager.</param> - /// <param name="mediaEncoder">The media encoder.</param> - /// <param name="namingOptions">The naming Options.</param> - public SubtitleResolver( - ILocalizationManager localization, - IMediaEncoder mediaEncoder, - NamingOptions namingOptions) - { - _localizationManager = localization; - _mediaEncoder = mediaEncoder; - _namingOptions = namingOptions; - _subtitleFilePathParser = new SubtitleFilePathParser(_namingOptions); - } - - /// <summary> - /// Retrieves the external subtitle streams for the provided video. - /// </summary> - /// <param name="video">The video to search from.</param> - /// <param name="startIndex">The stream index to start adding subtitle streams at.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>The external subtitle streams located.</returns> - public async IAsyncEnumerable<MediaStream> GetExternalSubtitleStreams( - Video video, - int startIndex, - IDirectoryService directoryService, - bool clearCache, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - - cancellationToken.ThrowIfCancellationRequested(); - - if (!video.IsFileProtocol) - { - yield break; - } - - var subtitleFileInfos = GetExternalSubtitleFiles(video, directoryService, clearCache); - - var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); - - foreach (var subtitleFileInfo in subtitleFileInfos) - { - string fileName = Path.GetFileName(subtitleFileInfo.Path); - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(subtitleFileInfo.Path); - Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(subtitleFileInfo.Path, cancellationToken).ConfigureAwait(false); - - if (mediaInfo.MediaStreams.Count == 1) - { - MediaStream mediaStream = mediaInfo.MediaStreams.First(); - mediaStream.Index = startIndex++; - mediaStream.Type = MediaStreamType.Subtitle; - mediaStream.IsExternal = true; - mediaStream.Path = subtitleFileInfo.Path; - mediaStream.IsDefault = subtitleFileInfo.IsDefault || mediaStream.IsDefault; - mediaStream.IsForced = subtitleFileInfo.IsForced || mediaStream.IsForced; - - yield return DetectLanguage(mediaStream, fileNameWithoutExtension, videoFileNameWithoutExtension); - } - else - { - foreach (MediaStream mediaStream in mediaInfo.MediaStreams) - { - mediaStream.Index = startIndex++; - mediaStream.Type = MediaStreamType.Subtitle; - mediaStream.IsExternal = true; - mediaStream.Path = subtitleFileInfo.Path; - - yield return DetectLanguage(mediaStream, fileNameWithoutExtension, videoFileNameWithoutExtension); - } - } - } - } - - /// <summary> - /// Locates the external subtitle files for the provided video. - /// </summary> - /// <param name="video">The video to search from.</param> - /// <param name="directoryService">The directory service to search for files.</param> - /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param> - /// <returns>The external subtitle file paths located.</returns> - public IEnumerable<SubtitleFileInfo> GetExternalSubtitleFiles( - Video video, - IDirectoryService directoryService, - bool clearCache) - { - if (!video.IsFileProtocol) - { - yield break; - } - - // Check if video folder exists - string folder = video.ContainingFolderPath; - if (!Directory.Exists(folder)) - { - yield break; - } - - var videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path); - - var files = directoryService.GetFilePaths(folder, clearCache, true); - for (int i = 0; i < files.Count; i++) - { - var subtitleFileInfo = _subtitleFilePathParser.ParseFile(files[i]); - - if (subtitleFileInfo == null) - { - continue; - } - - yield return subtitleFileInfo; - } - } - - /// <summary> - /// Returns the media info of the given subtitle file. - /// </summary> - /// <param name="path">The path to the subtitle file.</param> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - /// <returns>The media info for the given subtitle file.</returns> - private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - return _mediaEncoder.GetMediaInfo( - new MediaInfoRequest - { - MediaType = DlnaProfileType.Subtitle, - MediaSource = new MediaSourceInfo - { - Path = path, - Protocol = MediaProtocol.File - } - }, - cancellationToken); - } - - private MediaStream DetectLanguage(MediaStream mediaStream, string fileNameWithoutExtension, string videoFileNameWithoutExtension) - { - // Support xbmc naming conventions - 300.spanish.srt - var languageString = fileNameWithoutExtension; - while (languageString.Length > 0) - { - var lastDot = languageString.LastIndexOf('.'); - if (lastDot < videoFileNameWithoutExtension.Length) - { - break; - } - - var currentSlice = languageString[lastDot..]; - languageString = languageString[..lastDot]; - - if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) - || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) - || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var currentSliceString = currentSlice[1..]; - - // Try to translate to three character code - var culture = _localizationManager.FindLanguageInfo(currentSliceString); - - if (culture == null || mediaStream.Language != null) - { - if (mediaStream.Title == null) - { - mediaStream.Title = currentSliceString; - } - } - else - { - mediaStream.Language = culture.ThreeLetterISOLanguageName; - } - } - - return mediaStream; - } - } -} diff --git a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleFilePathParserTests.cs b/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleFilePathParserTests.cs deleted file mode 100644 index 5c62d9418..000000000 --- a/tests/Jellyfin.Naming.Tests/Subtitles/SubtitleFilePathParserTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Emby.Naming.Common; -using Emby.Naming.Subtitles; -using Xunit; - -namespace Jellyfin.Naming.Tests.Subtitles -{ - public class SubtitleFilePathParserTests - { - private readonly NamingOptions _namingOptions = new NamingOptions(); - - [Theory] - [InlineData("The Skin I Live In (2011).srt", false, false)] - [InlineData("The Skin I Live In (2011).eng.srt", false, false)] - [InlineData("The Skin I Live In (2011).default.srt", true, false)] - [InlineData("The Skin I Live In (2011).forced.srt", false, true)] - [InlineData("The Skin I Live In (2011).eng.foreign.srt", false, true)] - [InlineData("The Skin I Live In (2011).eng.default.foreign.srt", true, true)] - [InlineData("The Skin I Live In (2011).default.foreign.eng.srt", true, true)] - public void SubtitleFilePathParser_ValidFileName_Parses(string input, bool isDefault, bool isForced) - { - var parser = new SubtitleFilePathParser(_namingOptions); - - var result = parser.ParseFile(input); - - Assert.Equal(isDefault, result?.IsDefault); - Assert.Equal(isForced, result?.IsForced); - Assert.Equal(input, result?.Path); - } - - [Theory] - [InlineData("The Skin I Live In (2011).mp4")] - [InlineData("")] - public void SubtitleFilePathParser_InvalidFileName_ReturnsNull(string input) - { - var parser = new SubtitleFilePathParser(_namingOptions); - - Assert.Null(parser.ParseFile(input)); - } - } -} diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs index 4a6099519..d0f216eb0 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/AudioResolverTests.cs @@ -1,13 +1,14 @@ +using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Emby.Naming.Common; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Providers.MediaInfo; @@ -18,8 +19,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo { public class AudioResolverTests { - private const string DirectoryPath = "Test Data/Video"; - private readonly AudioResolver _audioResolver; + private const string VideoDirectoryPath = "Test Data/Video"; + private const string MetadataDirectoryPath = "Test Data/Metadata"; + private readonly MediaInfoResolver _audioResolver; public AudioResolverTests() { @@ -45,52 +47,68 @@ namespace Jellyfin.Providers.Tests.MediaInfo } })); - _audioResolver = new AudioResolver(localizationManager.Object, mediaEncoder.Object, new NamingOptions()); + _audioResolver = new MediaInfoResolver(localizationManager.Object, mediaEncoder.Object, new NamingOptions(), DlnaProfileType.Audio); } [Fact] - public async void AddExternalAudioStreams_GivenMixedFilenames_ReturnsValidSubtitles() + public async void AddExternalStreams_GivenMixedFilenames_ReturnsValidSubtitles() { var startIndex = 0; var index = startIndex; var files = new[] { - DirectoryPath + "/My.Video.mp3", - // DirectoryPath + "/Some.Other.Video.mp3", // TODO should not be picked up - DirectoryPath + "/My.Video.png", - DirectoryPath + "/My.Video.srt", - DirectoryPath + "/My.Video.txt", - DirectoryPath + "/My.Video.vtt", - DirectoryPath + "/My.Video.ass", - DirectoryPath + "/My.Video.sub", - DirectoryPath + "/My.Video.ssa", - DirectoryPath + "/My.Video.smi", - DirectoryPath + "/My.Video.sami", - DirectoryPath + "/My.Video.en.mp3", - DirectoryPath + "/My.Video.Label.mp3", - DirectoryPath + "/My.Video.With.Additional.Garbage.en.mp3", - // DirectoryPath + "/My.Video With Additional Garbage.mp3" // TODO no "." after "My.Video", previously would be picked up + 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(DirectoryPath + "/My.Video.mp3", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.en.mp3", "eng", null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.Label.mp3", null, "Label", index++), - CreateMediaStream(DirectoryPath + "/My.Video.With.Additional.Garbage.en.mp3", "eng", "Garbage", index) // TODO only "Garbage" is picked up as title, none of the other extra text + 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) }; BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); - var video = new Movie - { - // Must be valid for video.IsFileProtocol check - Path = DirectoryPath + "/My.Video.mkv" - }; + + var video = new Mock<Video>(); + video.CallBase = true; + video.Setup(moq => moq.Path).Returns(VideoDirectoryPath + "/My.Video.mkv"); + video.Setup(moq => moq.GetInternalMetadataPath()).Returns(MetadataDirectoryPath); var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Video"), It.IsAny<bool>(), It.IsAny<bool>())) .Returns(files); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Metadata"), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(metadataFiles); - var asyncStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); + var asyncStreams = _audioResolver.GetExternalStreamsAsync(video.Object, startIndex, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); var streams = new List<MediaStream>(); await foreach (var stream in asyncStreams) @@ -114,6 +132,8 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Theory] + [InlineData("MyVideo.en.aac", "eng", null, false, false)] + [InlineData("MyVideo.en.forced.default.dts", "eng", null, true, true)] [InlineData("My.Video.mp3", null, null, false, false)] [InlineData("My.Video.English.mp3", "eng", null, false, false)] [InlineData("My.Video.Title.mp3", null, "Title", false, false)] @@ -123,17 +143,19 @@ namespace Jellyfin.Providers.Tests.MediaInfo public async void GetExternalAudioStreams_GivenSingleFile_ReturnsExpectedStream(string file, string? language, string? title, bool isForced, bool isDefault) { BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); - var video = new Movie - { - // Must be valid for video.IsFileProtocol check - Path = DirectoryPath + "/My.Video.mkv" - }; + + var video = new Mock<Video>(); + video.CallBase = true; + video.Setup(moq => moq.Path).Returns(VideoDirectoryPath + "/My.Video.mkv"); + video.Setup(moq => moq.GetInternalMetadataPath()).Returns(MetadataDirectoryPath); var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Video"), It.IsAny<bool>(), It.IsAny<bool>())) - .Returns(new[] { DirectoryPath + "/" + file }); + .Returns(new[] { VideoDirectoryPath + "/" + file }); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Metadata"), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); - var asyncStreams = _audioResolver.GetExternalAudioStreams(video, 0, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); + var asyncStreams = _audioResolver.GetExternalStreamsAsync(video.Object, 0, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); var streams = new List<MediaStream>(); await foreach (var stream in asyncStreams) @@ -145,7 +167,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo var actual = streams[0]; - var expected = CreateMediaStream(DirectoryPath + "/" + file, language, title, 0, isForced, isDefault); + var expected = CreateMediaStream(VideoDirectoryPath + "/" + file, language, title, 0, isForced, isDefault); Assert.Equal(expected.Index, actual.Index); Assert.Equal(expected.Type, actual.Type); Assert.Equal(expected.IsExternal, actual.IsExternal); diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs index 7a1c47fb4..fa261a548 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/SubtitleResolverTests.cs @@ -1,13 +1,14 @@ +using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Emby.Naming.Common; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Providers.MediaInfo; @@ -18,8 +19,9 @@ namespace Jellyfin.Providers.Tests.MediaInfo { public class SubtitleResolverTests { - private const string DirectoryPath = "Test Data/Video"; - private readonly SubtitleResolver _subtitleResolver; + private const string VideoDirectoryPath = "Test Data/Video"; + private const string MetadataDirectoryPath = "Test Data/Metadata"; + private readonly MediaInfoResolver _subtitleResolver; public SubtitleResolverTests() { @@ -54,7 +56,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo } })); - _subtitleResolver = new SubtitleResolver(localizationManager.Object, mediaEncoder.Object, new NamingOptions()); + _subtitleResolver = new MediaInfoResolver(localizationManager.Object, mediaEncoder.Object, new NamingOptions(), DlnaProfileType.Subtitle); } [Fact] @@ -64,52 +66,68 @@ namespace Jellyfin.Providers.Tests.MediaInfo var index = startIndex; var files = new[] { - DirectoryPath + "/My.Video.mp3", - DirectoryPath + "/My.Video.png", - DirectoryPath + "/My.Video.srt", - // DirectoryPath + "/Some.Other.Video.srt", // TODO should not be picked up - DirectoryPath + "/My.Video.txt", - DirectoryPath + "/My.Video.vtt", - DirectoryPath + "/My.Video.ass", - DirectoryPath + "/My.Video.sub", - DirectoryPath + "/My.Video.ssa", - DirectoryPath + "/My.Video.smi", - DirectoryPath + "/My.Video.sami", - DirectoryPath + "/My.Video.en.srt", - DirectoryPath + "/My.Video.default.en.srt", - DirectoryPath + "/My.Video.default.forced.en.srt", - DirectoryPath + "/My.Video.en.default.forced.srt", - DirectoryPath + "/My.Video.With.Additional.Garbage.en.srt", - // DirectoryPath + "/My.Video With Additional Garbage.srt" // TODO no "." after "My.Video", previously would be picked up + VideoDirectoryPath + "/MyVideo.en.srt", + VideoDirectoryPath + "/MyVideo.en.forced.default.sub", + VideoDirectoryPath + "/My.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.mks", + VideoDirectoryPath + "/My.Video.en.srt", + VideoDirectoryPath + "/My.Video.default.en.srt", + VideoDirectoryPath + "/My.Video.default.forced.en.srt", + VideoDirectoryPath + "/My.Video.en.default.forced.srt", + VideoDirectoryPath + "/My.Video.en.With Additional Garbage.sub", + VideoDirectoryPath + "/My.Video.With Additional Garbage.English.sub", + VideoDirectoryPath + "/My.Video.With.Additional.Garbage.en.srt", + VideoDirectoryPath + "/Some.Other.Video.srt" + }; + var metadataFiles = new[] + { + MetadataDirectoryPath + "/My.Video.en.srt" }; var expectedResult = new[] { - CreateMediaStream(DirectoryPath + "/My.Video.srt", "srt", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.vtt", "vtt", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.ass", "ass", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.sub", "sub", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.ssa", "ssa", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.smi", "smi", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.sami", "sami", null, null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.en.srt", "srt", "eng", null, index++), - CreateMediaStream(DirectoryPath + "/My.Video.default.en.srt", "srt", "eng", null, index++, isDefault: true), - CreateMediaStream(DirectoryPath + "/My.Video.default.forced.en.srt", "srt", "eng", null, index++, isForced: true, isDefault: true), - CreateMediaStream(DirectoryPath + "/My.Video.en.default.forced.srt", "srt", "eng", null, index++, isForced: true, isDefault: true), - CreateMediaStream(DirectoryPath + "/My.Video.With.Additional.Garbage.en.srt", "srt", "eng", "Garbage", index) // TODO only "Garbage" is picked up as title, none of the other extra text + CreateMediaStream(VideoDirectoryPath + "/MyVideo.en.srt", "srt", "eng", null, index++), + CreateMediaStream(VideoDirectoryPath + "/MyVideo.en.forced.default.sub", "sub", "eng", null, index++, isDefault: true, isForced: true), + CreateMediaStream(VideoDirectoryPath + "/My.Video.srt", "srt", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.vtt", "vtt", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.ass", "ass", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.sub", "sub", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.ssa", "ssa", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.smi", "smi", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.sami", "sami", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.mks", "mks", null, null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.en.srt", "srt", "eng", null, index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.default.en.srt", "srt", "eng", null, index++, isDefault: true), + CreateMediaStream(VideoDirectoryPath + "/My.Video.default.forced.en.srt", "srt", "eng", null, index++, isForced: true, isDefault: true), + CreateMediaStream(VideoDirectoryPath + "/My.Video.en.default.forced.srt", "srt", "eng", null, index++, isForced: true, isDefault: true), + CreateMediaStream(VideoDirectoryPath + "/My.Video.en.With Additional Garbage.sub", "sub", "eng", "With Additional Garbage", index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.With Additional Garbage.English.sub", "sub", "eng", "With Additional Garbage", index++), + CreateMediaStream(VideoDirectoryPath + "/My.Video.With.Additional.Garbage.en.srt", "srt", "eng", "With.Additional.Garbage", index++), + CreateMediaStream(MetadataDirectoryPath + "/My.Video.en.srt", "srt", "eng", null, index) }; BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); - var video = new Movie - { - // Must be valid for video.IsFileProtocol check - Path = DirectoryPath + "/My.Video.mkv" - }; + + var video = new Mock<Video>(); + video.CallBase = true; + video.Setup(moq => moq.Path).Returns(VideoDirectoryPath + "/My.Video.mkv"); + video.Setup(moq => moq.GetInternalMetadataPath()).Returns(MetadataDirectoryPath); var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Video"), It.IsAny<bool>(), It.IsAny<bool>())) .Returns(files); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Metadata"), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(metadataFiles); - var asyncStreams = _subtitleResolver.GetExternalSubtitleStreams(video, startIndex, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); + var asyncStreams = _subtitleResolver.GetExternalStreamsAsync(video.Object, startIndex, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); var streams = new List<MediaStream>(); await foreach (var stream in asyncStreams) @@ -124,7 +142,6 @@ namespace Jellyfin.Providers.Tests.MediaInfo var actual = streams[i]; Assert.Equal(expected.Index, actual.Index); - // Assert.Equal(expected.Codec, actual.Codec); TODO should codec still be set to file extension? Assert.Equal(expected.Type, actual.Type); Assert.Equal(expected.IsExternal, actual.IsExternal); Assert.Equal(expected.Path, actual.Path); @@ -136,14 +153,10 @@ namespace Jellyfin.Providers.Tests.MediaInfo } [Theory] - [InlineData("My Video.srt", "srt", null, null, false, false)] - [InlineData("My Video.ass", "ass", null, null, false, false)] - [InlineData("my video.srt", "srt", null, null, false, false)] - [InlineData("My Vidè€o.srt", "srt", null, null, false, false)] - [InlineData("My. Video.srt", "srt", null, null, false, false)] + [InlineData("MyVideo.en.srt", "srt", "eng", null, false, false)] + [InlineData("MyVideo.en.forced.default.srt", "srt", "eng", null, true, true)] [InlineData("My.Video.srt", "srt", null, null, false, false)] [InlineData("My.Video.foreign.srt", "srt", null, null, true, false)] - [InlineData("My Video.forced.srt", "srt", null, null, true, false)] [InlineData("My.Video.default.srt", "srt", null, null, false, true)] [InlineData("My.Video.forced.default.srt", "srt", null, null, true, true)] [InlineData("My.Video.en.srt", "srt", "eng", null, false, false)] @@ -153,24 +166,25 @@ namespace Jellyfin.Providers.Tests.MediaInfo [InlineData("My.Video.default.forced.en.srt", "srt", "eng", null, true, true)] [InlineData("My.Video.en.default.forced.srt", "srt", "eng", null, true, true)] [InlineData("My.Video.Track Label.srt", "srt", null, "Track Label", false, false)] - // [InlineData("My.Video.Track.Label.srt", "srt", null, "Track.Label", false, false)] // TODO fails - only "Label" is picked up for title, not "Track.Label" - // [InlineData("MyVideo.Track Label.srt", "srt", null, "Track Label", false, false)] // TODO fails - fuzzy match doesn't pick up on end of matching segment being shorter? + [InlineData("My.Video.Track.Label.srt", "srt", null, "Track.Label", false, false)] [InlineData("My.Video.Track Label.en.default.forced.srt", "srt", "eng", "Track Label", true, true)] [InlineData("My.Video.en.default.forced.Track Label.srt", "srt", "eng", "Track Label", true, true)] public async void AddExternalSubtitleStreams_GivenSingleFile_ReturnsExpectedSubtitle(string file, string codec, string? language, string? title, bool isForced, bool isDefault) { BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>(); - var video = new Movie - { - // Must be valid for video.IsFileProtocol check - Path = DirectoryPath + "/My.Video.mkv" - }; + + var video = new Mock<Video>(); + video.CallBase = true; + video.Setup(moq => moq.Path).Returns(VideoDirectoryPath + "/My.Video.mkv"); + video.Setup(moq => moq.GetInternalMetadataPath()).Returns(MetadataDirectoryPath); var directoryService = new Mock<IDirectoryService>(MockBehavior.Strict); directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Video"), It.IsAny<bool>(), It.IsAny<bool>())) - .Returns(new[] { DirectoryPath + "/" + file }); + .Returns(new[] { VideoDirectoryPath + "/" + file }); + directoryService.Setup(ds => ds.GetFilePaths(It.IsRegex(@"Test Data[/\\]Metadata"), It.IsAny<bool>(), It.IsAny<bool>())) + .Returns(Array.Empty<string>()); - var asyncStreams = _subtitleResolver.GetExternalSubtitleStreams(video, 0, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); + var asyncStreams = _subtitleResolver.GetExternalStreamsAsync(video.Object, 0, directoryService.Object, false, CancellationToken.None).ConfigureAwait(false); var streams = new List<MediaStream>(); await foreach (var stream in asyncStreams) @@ -181,9 +195,8 @@ namespace Jellyfin.Providers.Tests.MediaInfo Assert.Single(streams); var actual = streams[0]; - var expected = CreateMediaStream(DirectoryPath + "/" + file, codec, language, title, 0, isForced, isDefault); + var expected = CreateMediaStream(VideoDirectoryPath + "/" + file, codec, language, title, 0, isForced, isDefault); Assert.Equal(expected.Index, actual.Index); - // Assert.Equal(expected.Codec, actual.Codec); TODO should codec still be set to file extension? Assert.Equal(expected.Type, actual.Type); Assert.Equal(expected.IsExternal, actual.IsExternal); Assert.Equal(expected.Path, actual.Path); |
