< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Subtitles.SubtitleManager
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
Line coverage
12%
Covered lines: 9
Uncovered lines: 63
Coverable lines: 72
Total lines: 414
Line coverage: 12.5%
Branch coverage
0%
Covered branches: 0
Total branches: 14
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
DownloadSubtitles(...)100%210%
UploadSubtitle(...)100%210%
SearchSubtitles(...)0%7280%
Normalize(...)0%620%
GetProviderId(...)100%210%
GetProvider(...)100%210%
DeleteSubtitles(...)100%210%
GetRemoteSubtitles(...)100%210%
GetSupportedProviders(...)0%2040%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Subtitles/SubtitleManager.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.IO;
 7using System.Linq;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Extensions;
 11using MediaBrowser.Common.Extensions;
 12using MediaBrowser.Controller.Entities;
 13using MediaBrowser.Controller.Entities.Movies;
 14using MediaBrowser.Controller.Entities.TV;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.Persistence;
 17using MediaBrowser.Controller.Providers;
 18using MediaBrowser.Controller.Subtitles;
 19using MediaBrowser.Model.Configuration;
 20using MediaBrowser.Model.Entities;
 21using MediaBrowser.Model.Globalization;
 22using MediaBrowser.Model.IO;
 23using MediaBrowser.Model.Providers;
 24using Microsoft.Extensions.Logging;
 25
 26namespace MediaBrowser.Providers.Subtitles
 27{
 28    public class SubtitleManager : ISubtitleManager
 29    {
 30        private readonly ILogger<SubtitleManager> _logger;
 31        private readonly IFileSystem _fileSystem;
 32        private readonly ILibraryMonitor _monitor;
 33        private readonly IMediaSourceManager _mediaSourceManager;
 34        private readonly ILocalizationManager _localization;
 35
 36        private readonly ISubtitleProvider[] _subtitleProviders;
 37
 38        public SubtitleManager(
 39            ILogger<SubtitleManager> logger,
 40            IFileSystem fileSystem,
 41            ILibraryMonitor monitor,
 42            IMediaSourceManager mediaSourceManager,
 43            ILocalizationManager localizationManager,
 44            IEnumerable<ISubtitleProvider> subtitleProviders)
 45        {
 2246            _logger = logger;
 2247            _fileSystem = fileSystem;
 2248            _monitor = monitor;
 2249            _mediaSourceManager = mediaSourceManager;
 2250            _localization = localizationManager;
 2251            _subtitleProviders = subtitleProviders
 2252                .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
 2253                .ToArray();
 2254        }
 55
 56        /// <inheritdoc />
 57        public event EventHandler<SubtitleDownloadFailureEventArgs>? SubtitleDownloadFailure;
 58
 59        /// <inheritdoc />
 60        public async Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancell
 61        {
 62            if (request.Language is not null)
 63            {
 64                var culture = _localization.FindLanguageInfo(request.Language);
 65
 66                if (culture is not null)
 67                {
 68                    request.TwoLetterISOLanguageName = culture.TwoLetterISOLanguageName;
 69                }
 70            }
 71
 72            var contentType = request.ContentType;
 73            var providers = _subtitleProviders
 74                .Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.
 75                .OrderBy(i =>
 76                {
 77                    var index = request.SubtitleFetcherOrder.IndexOf(i.Name);
 78                    return index == -1 ? int.MaxValue : index;
 79                })
 80                .ToArray();
 81
 82            // If not searching all, search one at a time until something is found
 83            if (!request.SearchAllProviders)
 84            {
 85                foreach (var provider in providers)
 86                {
 87                    try
 88                    {
 89                        var searchResults = await provider.Search(request, cancellationToken).ConfigureAwait(false);
 90
 91                        var list = searchResults.ToArray();
 92
 93                        if (list.Length > 0)
 94                        {
 95                            Normalize(list);
 96                            return list;
 97                        }
 98                    }
 99                    catch (Exception ex)
 100                    {
 101                        _logger.LogError(ex, "Error downloading subtitles from {Provider}", provider.Name);
 102                    }
 103                }
 104
 105                return Array.Empty<RemoteSubtitleInfo>();
 106            }
 107
 108            var tasks = providers.Select(async i =>
 109            {
 110                try
 111                {
 112                    var searchResults = await i.Search(request, cancellationToken).ConfigureAwait(false);
 113
 114                    var list = searchResults.ToArray();
 115                    Normalize(list);
 116                    return list;
 117                }
 118                catch (Exception ex)
 119                {
 120                    _logger.LogError(ex, "Error downloading subtitles from {Name}", i.Name);
 121                    return Array.Empty<RemoteSubtitleInfo>();
 122                }
 123            });
 124
 125            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 126
 127            return results.SelectMany(i => i).ToArray();
 128        }
 129
 130        /// <inheritdoc />
 131        public Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken)
 132        {
 0133            var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
 134
 0135            return DownloadSubtitles(video, libraryOptions, subtitleId, cancellationToken);
 136        }
 137
 138        /// <inheritdoc />
 139        public async Task DownloadSubtitles(
 140            Video video,
 141            LibraryOptions libraryOptions,
 142            string subtitleId,
 143            CancellationToken cancellationToken)
 144        {
 145            var parts = subtitleId.Split('_', 2);
 146            var provider = GetProvider(parts[0]);
 147
 148            try
 149            {
 150                var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
 151
 152                await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false);
 153            }
 154            catch (RateLimitExceededException)
 155            {
 156                throw;
 157            }
 158            catch (Exception ex)
 159            {
 160                SubtitleDownloadFailure?.Invoke(this, new SubtitleDownloadFailureEventArgs
 161                {
 162                    Item = video,
 163                    Exception = ex,
 164                    Provider = provider.Name
 165                });
 166
 167                throw;
 168            }
 169        }
 170
 171        /// <inheritdoc />
 172        public Task UploadSubtitle(Video video, SubtitleResponse response)
 173        {
 0174            var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
 0175            return TrySaveSubtitle(video, libraryOptions, response);
 176        }
 177
 178        private async Task TrySaveSubtitle(
 179            Video video,
 180            LibraryOptions libraryOptions,
 181            SubtitleResponse response)
 182        {
 183            var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
 184
 185            var memoryStream = new MemoryStream();
 186            await using (memoryStream.ConfigureAwait(false))
 187            {
 188                var stream = response.Stream;
 189                await using (stream.ConfigureAwait(false))
 190                {
 191                    await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
 192                    memoryStream.Position = 0;
 193                }
 194
 195                var savePaths = new List<string>();
 196                var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvaria
 197
 198                if (response.IsForced)
 199                {
 200                    saveFileName += ".forced";
 201                }
 202
 203                if (response.IsHearingImpaired)
 204                {
 205                    saveFileName += ".sdh";
 206                }
 207
 208                if (saveInMediaFolder)
 209                {
 210                    var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName));
 211                    savePaths.Add(mediaFolderPath);
 212                }
 213
 214                var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
 215
 216                savePaths.Add(internalPath);
 217
 218                await TrySaveToFiles(memoryStream, savePaths, video, response.Format.ToLowerInvariant()).ConfigureAwait(
 219            }
 220        }
 221
 222        private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
 223        {
 224            List<Exception>? exs = null;
 225
 226            foreach (var savePath in savePaths)
 227            {
 228                var path = savePath + "." + extension;
 229                try
 230                {
 231                    if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)
 232                            || path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
 233                    {
 234                        var fileExists = File.Exists(path);
 235                        var counter = 0;
 236
 237                        while (fileExists)
 238                        {
 239                            path = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extensi
 240                            fileExists = File.Exists(path);
 241                            counter++;
 242                        }
 243
 244                        _logger.LogInformation("Saving subtitles to {SavePath}", path);
 245                        _monitor.ReportFileSystemChangeBeginning(path);
 246
 247                        Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Pa
 248
 249                        var fileOptions = AsyncFile.WriteOptions;
 250                        fileOptions.Mode = FileMode.CreateNew;
 251                        fileOptions.PreallocationSize = stream.Length;
 252                        var fs = new FileStream(path, fileOptions);
 253                        await using (fs.ConfigureAwait(false))
 254                        {
 255                            await stream.CopyToAsync(fs).ConfigureAwait(false);
 256                        }
 257
 258                        return;
 259                    }
 260                    else
 261                    {
 262                        // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, ba
 263                        _logger.LogError("An uploaded subtitle could not be saved because the resulting path was invalid
 264                    }
 265                }
 266                catch (Exception ex)
 267                {
 268                    (exs ??= []).Add(ex);
 269                }
 270                finally
 271                {
 272                    _monitor.ReportFileSystemChangeComplete(path, false);
 273                }
 274
 275                stream.Position = 0;
 276            }
 277
 278            if (exs is not null)
 279            {
 280                throw new AggregateException(exs);
 281            }
 282        }
 283
 284        /// <inheritdoc />
 285        public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, bool isAut
 286        {
 0287            if (video.VideoType != VideoType.VideoFile)
 288            {
 0289                return Task.FromResult(Array.Empty<RemoteSubtitleInfo>());
 290            }
 291
 292            VideoContentType mediaType;
 293
 0294            if (video is Episode)
 295            {
 0296                mediaType = VideoContentType.Episode;
 297            }
 0298            else if (video is Movie)
 299            {
 0300                mediaType = VideoContentType.Movie;
 301            }
 302            else
 303            {
 304                // These are the only supported types
 0305                return Task.FromResult(Array.Empty<RemoteSubtitleInfo>());
 306            }
 307
 0308            var request = new SubtitleSearchRequest
 0309            {
 0310                ContentType = mediaType,
 0311                IndexNumber = video.IndexNumber,
 0312                Language = language,
 0313                MediaPath = video.Path,
 0314                Name = video.Name,
 0315                ParentIndexNumber = video.ParentIndexNumber,
 0316                ProductionYear = video.ProductionYear,
 0317                ProviderIds = video.ProviderIds,
 0318                RuntimeTicks = video.RunTimeTicks,
 0319                IsPerfectMatch = isPerfectMatch ?? false,
 0320                IsAutomated = isAutomated
 0321            };
 322
 0323            if (video is Episode episode)
 324            {
 0325                request.IndexNumberEnd = episode.IndexNumberEnd;
 0326                request.SeriesName = episode.SeriesName;
 327            }
 328
 0329            return SearchSubtitles(request, cancellationToken);
 330        }
 331
 332        private void Normalize(IEnumerable<RemoteSubtitleInfo> subtitles)
 333        {
 0334            foreach (var sub in subtitles)
 335            {
 0336                sub.Id = GetProviderId(sub.ProviderName) + "_" + sub.Id;
 337            }
 0338        }
 339
 340        private string GetProviderId(string name)
 341        {
 0342            return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture);
 343        }
 344
 345        private ISubtitleProvider GetProvider(string id)
 346        {
 0347            return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name), StringComparison.Ordinal));
 348        }
 349
 350        /// <inheritdoc />
 351        public Task DeleteSubtitles(BaseItem item, int index)
 352        {
 0353            var stream = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
 0354            {
 0355                Index = index,
 0356                ItemId = item.Id,
 0357                Type = MediaStreamType.Subtitle
 0358            })[0];
 359
 0360            var path = stream.Path;
 0361            _monitor.ReportFileSystemChangeBeginning(path);
 362
 363            try
 364            {
 0365                _fileSystem.DeleteFile(path);
 0366            }
 367            finally
 368            {
 0369                _monitor.ReportFileSystemChangeComplete(path, false);
 0370            }
 371
 0372            return item.RefreshMetadata(CancellationToken.None);
 373        }
 374
 375        /// <inheritdoc />
 376        public Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken)
 377        {
 0378            var parts = id.Split('_', 2);
 379
 0380            var provider = GetProvider(parts[0]);
 0381            id = parts[^1];
 382
 0383            return provider.GetSubtitles(id, cancellationToken);
 384        }
 385
 386        /// <inheritdoc />
 387        public SubtitleProviderInfo[] GetSupportedProviders(BaseItem item)
 388        {
 389            VideoContentType mediaType;
 390
 0391            if (item is Episode)
 392            {
 0393                mediaType = VideoContentType.Episode;
 394            }
 0395            else if (item is Movie)
 396            {
 0397                mediaType = VideoContentType.Movie;
 398            }
 399            else
 400            {
 401                // These are the only supported types
 0402                return Array.Empty<SubtitleProviderInfo>();
 403            }
 404
 0405            return _subtitleProviders
 0406                .Where(i => i.SupportedMediaTypes.Contains(mediaType))
 0407                .Select(i => new SubtitleProviderInfo
 0408                {
 0409                    Name = i.Name,
 0410                    Id = GetProviderId(i.Name)
 0411                }).ToArray();
 412        }
 413    }
 414}