using System; using System.Collections.Generic; using System.IO; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; namespace MediaBrowser.Providers.MediaInfo { /// /// Resolves external subtitles for videos. /// public class SubtitleResolver { private readonly ILocalizationManager _localization; /// /// Initializes a new instance of the class. /// /// The localization manager. public SubtitleResolver(ILocalizationManager localization) { _localization = localization; } /// /// Retrieves the external subtitle streams for the provided video. /// /// The video to search from. /// The stream index to start adding subtitle streams at. /// The directory service to search for files. /// True if the directory service cache should be cleared before searching. /// The external subtitle streams located. public List GetExternalSubtitleStreams( Video video, int startIndex, IDirectoryService directoryService, bool clearCache) { var streams = new List(); if (!video.IsFileProtocol) { return streams; } AddExternalSubtitleStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache); startIndex += streams.Count; string folder = video.GetInternalMetadataPath(); if (!Directory.Exists(folder)) { return streams; } try { AddExternalSubtitleStreams(streams, folder, video.Path, startIndex, directoryService, clearCache); } catch (IOException) { } return streams; } /// /// Locates the external subtitle files for the provided video. /// /// The video to search from. /// The directory service to search for files. /// True if the directory service cache should be cleared before searching. /// The external subtitle file paths located. public IEnumerable GetExternalSubtitleFiles( Video video, IDirectoryService directoryService, bool clearCache) { if (!video.IsFileProtocol) { yield break; } var streams = GetExternalSubtitleStreams(video, 0, directoryService, clearCache); foreach (var stream in streams) { yield return stream.Path; } } /// /// Extracts the subtitle files from the provided list and adds them to the list of streams. /// /// The list of streams to add external subtitles to. /// The path to the video file. /// The stream index to start adding subtitle streams at. /// The files to add if they are subtitles. public void AddExternalSubtitleStreams( List streams, string videoPath, int startIndex, IReadOnlyList files) { var videoFileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(videoPath); for (var i = 0; i < files.Count; i++) { var fullName = files[i]; var extension = Path.GetExtension(fullName.AsSpan()); if (!IsSubtitleExtension(extension)) { continue; } var fileNameWithoutExtension = NormalizeFilenameForSubtitleComparison(fullName); MediaStream mediaStream; // The subtitle filename must either be equal to the video filename or start with the video filename followed by a dot if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { mediaStream = new MediaStream { Index = startIndex++, Type = MediaStreamType.Subtitle, IsExternal = true, Path = fullName }; } else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.' && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)) { var isForced = fullName.Contains(".forced.", StringComparison.OrdinalIgnoreCase) || fullName.Contains(".foreign.", StringComparison.OrdinalIgnoreCase); var isDefault = fullName.Contains(".default.", StringComparison.OrdinalIgnoreCase); // Support xbmc naming conventions - 300.spanish.srt var languageSpan = fileNameWithoutExtension; while (languageSpan.Length > 0) { var lastDot = languageSpan.LastIndexOf('.'); if (lastDot < videoFileNameWithoutExtension.Length) { languageSpan = ReadOnlySpan.Empty; break; } var currentSlice = languageSpan[lastDot..]; if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase) || currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase) || currentSlice.Equals(".foreign", StringComparison.OrdinalIgnoreCase)) { languageSpan = languageSpan[..lastDot]; continue; } languageSpan = languageSpan[(lastDot + 1)..]; break; } var language = languageSpan.ToString(); if (string.IsNullOrWhiteSpace(language)) { language = null; } else { // Try to translate to three character code // Be flexible and check against both the full and three character versions var culture = _localization.FindLanguageInfo(language); language = culture == null ? language : culture.ThreeLetterISOLanguageName; } mediaStream = new MediaStream { Index = startIndex++, Type = MediaStreamType.Subtitle, IsExternal = true, Path = fullName, Language = language, IsForced = isForced, IsDefault = isDefault }; } else { continue; } mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant(); streams.Add(mediaStream); } } private static bool IsSubtitleExtension(ReadOnlySpan extension) { return extension.Equals(".srt", StringComparison.OrdinalIgnoreCase) || extension.Equals(".ssa", StringComparison.OrdinalIgnoreCase) || extension.Equals(".ass", StringComparison.OrdinalIgnoreCase) || extension.Equals(".sub", StringComparison.OrdinalIgnoreCase) || extension.Equals(".vtt", StringComparison.OrdinalIgnoreCase) || extension.Equals(".smi", StringComparison.OrdinalIgnoreCase) || extension.Equals(".sami", StringComparison.OrdinalIgnoreCase); } private static ReadOnlySpan NormalizeFilenameForSubtitleComparison(string filename) { // Try to account for sloppy file naming filename = filename.Replace("_", string.Empty, StringComparison.Ordinal); filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal); return Path.GetFileNameWithoutExtension(filename.AsSpan()); } private void AddExternalSubtitleStreams( List streams, string folder, string videoPath, int startIndex, IDirectoryService directoryService, bool clearCache) { var files = directoryService.GetFilePaths(folder, clearCache, true); AddExternalSubtitleStreams(streams, videoPath, startIndex, files); } } }