#pragma warning disable CS1591 using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; using static MediaBrowser.Model.IO.IODefaults; namespace MediaBrowser.Providers.Subtitles { public class SubtitleManager : ISubtitleManager { private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; private readonly ILocalizationManager _localization; private ISubtitleProvider[] _subtitleProviders; public SubtitleManager( ILogger logger, IFileSystem fileSystem, ILibraryMonitor monitor, IMediaSourceManager mediaSourceManager, ILocalizationManager localizationManager) { _logger = logger; _fileSystem = fileSystem; _monitor = monitor; _mediaSourceManager = mediaSourceManager; _localization = localizationManager; } /// public event EventHandler SubtitleDownloadFailure; /// public void AddParts(IEnumerable subtitleProviders) { _subtitleProviders = subtitleProviders .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) .ToArray(); } /// public async Task SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken) { if (request.Language != null) { var culture = _localization.FindLanguageInfo(request.Language); if (culture != null) { request.TwoLetterISOLanguageName = culture.TwoLetterISOLanguageName; } } var contentType = request.ContentType; var providers = _subtitleProviders .Where(i => i.SupportedMediaTypes.Contains(contentType)) .Where(i => !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)) .OrderBy(i => { var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name); return index == -1 ? int.MaxValue : index; }) .ToArray(); // If not searching all, search one at a time until something is found if (!request.SearchAllProviders) { foreach (var provider in providers) { try { var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false); var list = searchResults.ToArray(); if (list.Length > 0) { Normalize(list); return list; } } catch (Exception ex) { _logger.LogError(ex, "Error downloading subtitles from {Provider}", provider.Name); } } return Array.Empty(); } var tasks = providers.Select(async i => { try { var searchResults = await i.Search(request, cancellationToken).ConfigureAwait(false); var list = searchResults.ToArray(); Normalize(list); return list; } catch (Exception ex) { _logger.LogError(ex, "Error downloading subtitles from {0}", i.Name); return Array.Empty(); } }); var results = await Task.WhenAll(tasks).ConfigureAwait(false); return results.SelectMany(i => i).ToArray(); } /// public Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken) { var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); return DownloadSubtitles(video, libraryOptions, subtitleId, cancellationToken); } /// public async Task DownloadSubtitles( Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken) { var parts = subtitleId.Split('_', 2); var provider = GetProvider(parts[0]); try { var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false); await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false); } catch (RateLimitExceededException) { throw; } catch (Exception ex) { SubtitleDownloadFailure?.Invoke(this, new SubtitleDownloadFailureEventArgs { Item = video, Exception = ex, Provider = provider.Name }); throw; } } /// public Task UploadSubtitle(Video video, SubtitleResponse response) { var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); return TrySaveSubtitle(video, libraryOptions, response); } private async Task TrySaveSubtitle( Video video, LibraryOptions libraryOptions, SubtitleResponse response) { var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia; using var stream = response.Stream; using var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; var savePaths = new List(); var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); if (response.IsForced) { saveFileName += ".forced"; } saveFileName += "." + response.Format.ToLowerInvariant(); if (saveInMediaFolder) { var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) { savePaths.Add(mediaFolderPath); } } var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) { savePaths.Add(internalPath); } if (savePaths.Count > 0) { await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); } else { _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); } } private async Task TrySaveToFiles(Stream stream, List savePaths) { List exs = null; foreach (var savePath in savePaths) { _logger.LogInformation("Saving subtitles to {0}", savePath); _monitor.ReportFileSystemChangeBeginning(savePath); try { Directory.CreateDirectory(Path.GetDirectoryName(savePath)); // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 . using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true); await stream.CopyToAsync(fs).ConfigureAwait(false); return; } catch (Exception ex) { #pragma warning disable CA1508 exs ??= new List() { ex }; #pragma warning restore CA1508 } finally { _monitor.ReportFileSystemChangeComplete(savePath, false); } stream.Position = 0; } if (exs != null) { throw new AggregateException(exs); } } /// public Task SearchSubtitles(Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken) { if (video.VideoType != VideoType.VideoFile) { return Task.FromResult(Array.Empty()); } VideoContentType mediaType; if (video is Episode) { mediaType = VideoContentType.Episode; } else if (video is Movie) { mediaType = VideoContentType.Movie; } else { // These are the only supported types return Task.FromResult(Array.Empty()); } var request = new SubtitleSearchRequest { ContentType = mediaType, IndexNumber = video.IndexNumber, Language = language, MediaPath = video.Path, Name = video.Name, ParentIndexNumber = video.ParentIndexNumber, ProductionYear = video.ProductionYear, ProviderIds = video.ProviderIds, RuntimeTicks = video.RunTimeTicks, IsPerfectMatch = isPerfectMatch ?? false }; if (video is Episode episode) { request.IndexNumberEnd = episode.IndexNumberEnd; request.SeriesName = episode.SeriesName; } return SearchSubtitles(request, cancellationToken); } private void Normalize(IEnumerable subtitles) { foreach (var sub in subtitles) { sub.Id = GetProviderId(sub.ProviderName) + "_" + sub.Id; } } private string GetProviderId(string name) { return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); } private ISubtitleProvider GetProvider(string id) { return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal)); } /// public Task DeleteSubtitles(BaseItem item, int index) { var stream = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery { Index = index, ItemId = item.Id, Type = MediaStreamType.Subtitle })[0]; var path = stream.Path; _monitor.ReportFileSystemChangeBeginning(path); try { _fileSystem.DeleteFile(path); } finally { _monitor.ReportFileSystemChangeComplete(path, false); } return item.RefreshMetadata(CancellationToken.None); } /// public Task GetRemoteSubtitles(string id, CancellationToken cancellationToken) { var parts = id.Split('_', 2); var provider = GetProvider(parts[0]); id = parts[^1]; return provider.GetSubtitles(id, cancellationToken); } /// public SubtitleProviderInfo[] GetSupportedProviders(BaseItem item) { VideoContentType mediaType; if (item is Episode) { mediaType = VideoContentType.Episode; } else if (item is Movie) { mediaType = VideoContentType.Movie; } else { // These are the only supported types return Array.Empty(); } return _subtitleProviders .Where(i => i.SupportedMediaTypes.Contains(mediaType)) .Select(i => new SubtitleProviderInfo { Name = i.Name, Id = GetProviderId(i.Name) }).ToArray(); } } }