using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; 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.IO; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Providers.MediaInfo { /// /// Resolves external files for . /// public abstract class MediaInfoResolver { /// /// The instance. /// private readonly ExternalPathParser _externalPathParser; /// /// The instance. /// private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; /// /// The instance. /// private readonly NamingOptions _namingOptions; /// /// The of the files this resolver should resolve. /// private readonly DlnaProfileType _type; /// /// Initializes a new instance of the class. /// /// The localization manager. /// The media encoder. /// The file system. /// The object containing FileExtensions, MediaDefaultFlags, MediaForcedFlags and MediaFlagDelimiters. /// The of the parsed file. protected MediaInfoResolver( ILocalizationManager localizationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, NamingOptions namingOptions, DlnaProfileType type) { _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; _namingOptions = namingOptions; _type = type; _externalPathParser = new ExternalPathParser(namingOptions, localizationManager, _type); } /// /// Retrieves the external streams for the provided video. /// /// The object to search external streams for. /// The stream index to start adding external streams at. /// The directory service to search for files. /// True if the directory service cache should be cleared before searching. /// The cancellation token. /// The external streams located. public async Task> GetExternalStreamsAsync( Video video, int startIndex, IDirectoryService directoryService, bool clearCache, CancellationToken cancellationToken) { if (!video.IsFileProtocol) { return Array.Empty(); } var pathInfos = GetExternalFiles(video, directoryService, clearCache); if (!pathInfos.Any()) { return Array.Empty(); } var mediaStreams = new List(); foreach (var pathInfo in pathInfos) { var mediaInfo = await GetMediaInfo(pathInfo.Path, _type, cancellationToken).ConfigureAwait(false); if (mediaInfo.MediaStreams.Count == 1) { MediaStream mediaStream = mediaInfo.MediaStreams[0]; mediaStream.Index = startIndex++; mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault; mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced; mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); } else { foreach (MediaStream mediaStream in mediaInfo.MediaStreams) { mediaStream.Index = startIndex++; mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); } } } return mediaStreams.AsReadOnly(); } /// /// Returns the external file infos for the given video. /// /// The object to search external files for. /// The directory service to search for files. /// True if the directory service cache should be cleared before searching. /// The external file paths located. public IReadOnlyList GetExternalFiles( Video video, IDirectoryService directoryService, bool clearCache) { if (!video.IsFileProtocol) { return Array.Empty(); } // Check if video folder exists string folder = video.ContainingFolderPath; if (!_fileSystem.DirectoryExists(folder)) { return Array.Empty(); } var files = directoryService.GetFilePaths(folder, clearCache).ToList(); var internalMetadataPath = video.GetInternalMetadataPath(); if (_fileSystem.DirectoryExists(internalMetadataPath)) { files.AddRange(directoryService.GetFilePaths(internalMetadataPath, clearCache)); } if (!files.Any()) { return Array.Empty(); } var externalPathInfos = new List(); ReadOnlySpan prefix = video.FileNameWithoutExtension; foreach (var file in files) { var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.AsSpan()); if (prefix.Equals(fileNameWithoutExtension[..prefix.Length], StringComparison.OrdinalIgnoreCase) && (fileNameWithoutExtension.Length == prefix.Length || _namingOptions.MediaFlagDelimiters.Contains(fileNameWithoutExtension[prefix.Length]))) { var externalPathInfo = _externalPathParser.ParseFile(file, fileNameWithoutExtension[prefix.Length..].ToString()); if (externalPathInfo != null) { externalPathInfos.Add(externalPathInfo); } } } return externalPathInfos; } /// /// Returns the media info of the given file. /// /// The path to the file. /// The . /// The cancellation token to cancel operation. /// The media info for the given file. private Task 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); } /// /// Merges path metadata into stream metadata. /// /// The object. /// The object. /// The modified mediaStream. 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, _ => mediaStream.Type }; return mediaStream; } } }