< Summary - Jellyfin

Information
Class: MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder
Assembly: MediaBrowser.MediaEncoding
File(s): /srv/git/jellyfin/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
Line coverage
5%
Covered lines: 32
Uncovered lines: 512
Coverable lines: 544
Total lines: 1174
Line coverage: 5.8%
Branch coverage
3%
Covered branches: 7
Total branches: 232
Branch coverage: 3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/6/2026 - 12:14:09 AM Line coverage: 20.7% (16/77) Branch coverage: 0% (0/40) Total lines: 10133/14/2026 - 12:13:58 AM Line coverage: 20.7% (16/77) Branch coverage: 0% (0/40) Total lines: 10534/16/2026 - 12:15:18 AM Line coverage: 36.3% (28/77) Branch coverage: 15% (6/40) Total lines: 10534/19/2026 - 12:14:27 AM Line coverage: 8.3% (41/489) Branch coverage: 6.4% (12/186) Total lines: 10535/4/2026 - 12:15:16 AM Line coverage: 8.3% (41/491) Branch coverage: 6.3% (12/190) Total lines: 10565/13/2026 - 12:15:27 AM Line coverage: 8.2% (41/498) Branch coverage: 5.9% (12/202) Total lines: 10766/1/2026 - 12:16:05 AM Line coverage: 7.9% (40/506) Branch coverage: 5.6% (12/212) Total lines: 10906/2/2026 - 12:15:49 AM Line coverage: 5.5% (28/501) Branch coverage: 2.8% (6/212) Total lines: 10826/6/2026 - 12:15:50 AM Line coverage: 5.5% (28/501) Branch coverage: 2.3% (5/212) Total lines: 10826/8/2026 - 12:16:15 AM Line coverage: 5.8% (32/544) Branch coverage: 3% (7/232) Total lines: 1174 3/6/2026 - 12:14:09 AM Line coverage: 20.7% (16/77) Branch coverage: 0% (0/40) Total lines: 10133/14/2026 - 12:13:58 AM Line coverage: 20.7% (16/77) Branch coverage: 0% (0/40) Total lines: 10534/16/2026 - 12:15:18 AM Line coverage: 36.3% (28/77) Branch coverage: 15% (6/40) Total lines: 10534/19/2026 - 12:14:27 AM Line coverage: 8.3% (41/489) Branch coverage: 6.4% (12/186) Total lines: 10535/4/2026 - 12:15:16 AM Line coverage: 8.3% (41/491) Branch coverage: 6.3% (12/190) Total lines: 10565/13/2026 - 12:15:27 AM Line coverage: 8.2% (41/498) Branch coverage: 5.9% (12/202) Total lines: 10766/1/2026 - 12:16:05 AM Line coverage: 7.9% (40/506) Branch coverage: 5.6% (12/212) Total lines: 10906/2/2026 - 12:15:49 AM Line coverage: 5.5% (28/501) Branch coverage: 2.8% (6/212) Total lines: 10826/6/2026 - 12:15:50 AM Line coverage: 5.5% (28/501) Branch coverage: 2.3% (5/212) Total lines: 10826/8/2026 - 12:16:15 AM Line coverage: 5.8% (32/544) Branch coverage: 3% (7/232) Total lines: 1174

Coverage delta

Coverage delta 28 -28

Metrics

File(s)

/srv/git/jellyfin/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Globalization;
 8using System.IO;
 9using System.Linq;
 10using System.Net.Http;
 11using System.Text;
 12using System.Threading;
 13using System.Threading.Tasks;
 14using AsyncKeyedLock;
 15using MediaBrowser.Common;
 16using MediaBrowser.Common.Configuration;
 17using MediaBrowser.Common.Extensions;
 18using MediaBrowser.Common.Net;
 19using MediaBrowser.Controller.Configuration;
 20using MediaBrowser.Controller.Entities;
 21using MediaBrowser.Controller.IO;
 22using MediaBrowser.Controller.Library;
 23using MediaBrowser.Controller.MediaEncoding;
 24using MediaBrowser.Model.Dto;
 25using MediaBrowser.Model.Entities;
 26using MediaBrowser.Model.IO;
 27using MediaBrowser.Model.MediaInfo;
 28using Microsoft.Extensions.Logging;
 29using Nikse.SubtitleEdit.Core.Common;
 30using Nikse.SubtitleEdit.Core.SubtitleFormats;
 31using UtfUnknown;
 32using SubtitleFormat = MediaBrowser.Model.MediaInfo.SubtitleFormat;
 33
 34namespace MediaBrowser.MediaEncoding.Subtitles
 35{
 36    public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
 37    {
 38        private readonly ILogger<SubtitleEncoder> _logger;
 39        private readonly IFileSystem _fileSystem;
 40        private readonly IMediaEncoder _mediaEncoder;
 41        private readonly IHttpClientFactory _httpClientFactory;
 42        private readonly IMediaSourceManager _mediaSourceManager;
 43        private readonly ISubtitleParser _subtitleParser;
 44        private readonly IPathManager _pathManager;
 45        private readonly IServerConfigurationManager _serverConfigurationManager;
 46
 47        /// <summary>
 48        /// The _semaphoreLocks.
 49        /// </summary>
 2550        private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
 2551        {
 2552            o.PoolSize = 20;
 2553            o.PoolInitialFill = 1;
 2554        });
 55
 56        public SubtitleEncoder(
 57            ILogger<SubtitleEncoder> logger,
 58            IFileSystem fileSystem,
 59            IMediaEncoder mediaEncoder,
 60            IHttpClientFactory httpClientFactory,
 61            IMediaSourceManager mediaSourceManager,
 62            ISubtitleParser subtitleParser,
 63            IPathManager pathManager,
 64            IServerConfigurationManager serverConfigurationManager)
 65        {
 2566            _logger = logger;
 2567            _fileSystem = fileSystem;
 2568            _mediaEncoder = mediaEncoder;
 2569            _httpClientFactory = httpClientFactory;
 2570            _mediaSourceManager = mediaSourceManager;
 2571            _subtitleParser = subtitleParser;
 2572            _pathManager = pathManager;
 2573            _serverConfigurationManager = serverConfigurationManager;
 2574        }
 75
 76        private MemoryStream ConvertSubtitles(
 77            Stream stream,
 78            SubtitleInfo inputInfo,
 79            string outputFormat,
 80            long startTimeTicks,
 81            long endTimeTicks,
 82            bool preserveOriginalTimestamps)
 83        {
 084            var subtitle = Subtitle.Parse(stream, Path.GetExtension(inputInfo.Path));
 85
 086            FilterEvents(subtitle, startTimeTicks, endTimeTicks, preserveOriginalTimestamps);
 87
 088            var formatter = GetWriter(outputFormat);
 89
 090            var text = formatter.ToText(subtitle, "untitled");
 091            var bytes = Encoding.UTF8.GetBytes(text);
 92
 093            return new MemoryStream(bytes, 0, bytes.Length, false, true);
 94        }
 95
 96        internal void FilterEvents(Subtitle track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps)
 97        {
 98            // Drop subs that have fully elapsed before the requested start position
 099            track.Paragraphs
 0100                .RemoveAll(i => (i.StartTime.TimeSpan.Ticks - startPositionTicks) < 0 && (i.EndTime.TimeSpan.Ticks - sta
 101
 0102            if (endTimeTicks > 0)
 103            {
 0104                track.Paragraphs
 0105                    .RemoveAll(i => i.StartTime.TimeSpan.Ticks > endTimeTicks);
 106            }
 107
 0108            if (!preserveTimestamps)
 109            {
 0110                foreach (var trackEvent in track.Paragraphs)
 111                {
 0112                    trackEvent.StartTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.StartTime.TimeSpan.Tic
 0113                    trackEvent.EndTime = new TimeCode(TimeSpan.FromTicks(Math.Max(0, trackEvent.EndTime.TimeSpan.Ticks -
 114                }
 115            }
 0116        }
 117
 118        async Task<Stream> ISubtitleEncoder.GetSubtitles(BaseItem item, string mediaSourceId, int subtitleStreamIndex, s
 119        {
 0120            ArgumentNullException.ThrowIfNull(item);
 121
 0122            if (string.IsNullOrWhiteSpace(mediaSourceId))
 123            {
 0124                throw new ArgumentNullException(nameof(mediaSourceId));
 125            }
 126
 0127            var mediaSources = await _mediaSourceManager.GetPlaybackMediaSources(item, null, true, false, cancellationTo
 128
 0129            var mediaSource = mediaSources
 0130                .First(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
 131
 0132            var subtitleStream = mediaSource.MediaStreams
 0133               .First(i => i.Type == MediaStreamType.Subtitle && i.Index == subtitleStreamIndex);
 134
 0135            var (stream, info) = await GetSubtitleStream(mediaSource, subtitleStream, cancellationToken)
 0136                        .ConfigureAwait(false);
 137
 138            // Return the original if the same format is being requested
 139            // Character encoding was already handled in GetSubtitleStream
 140            // ASS is a superset of SSA, skipping the conversion and preserving the styles
 0141            if (string.Equals(info.Format, outputFormat, StringComparison.OrdinalIgnoreCase)
 0142                || (string.Equals(info.Format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase)
 0143                    && string.Equals(outputFormat, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase)))
 144            {
 0145                return stream;
 146            }
 147
 0148            using (stream)
 149            {
 0150                return ConvertSubtitles(stream, info, outputFormat, startTimeTicks, endTimeTicks, preserveOriginalTimest
 151            }
 0152        }
 153
 154        private async Task<(Stream Stream, SubtitleInfo Info)> GetSubtitleStream(
 155            MediaSourceInfo mediaSource,
 156            MediaStream subtitleStream,
 157            CancellationToken cancellationToken)
 158        {
 0159            var fileInfo = await GetReadableFile(mediaSource, subtitleStream, cancellationToken).ConfigureAwait(false);
 160
 0161            var stream = await GetSubtitleStream(fileInfo, cancellationToken).ConfigureAwait(false);
 162
 0163            return (stream, fileInfo);
 0164        }
 165
 166        private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken)
 167        {
 0168            if (fileInfo.Protocol == MediaProtocol.Http)
 169            {
 0170                var result = await DetectCharset(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(fal
 0171                var detected = result.Detected;
 172
 0173                if (detected is not null)
 174                {
 0175                    _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path);
 176
 0177                    using var stream = await _httpClientFactory.CreateClient(NamedClient.Default)
 0178                        .GetStreamAsync(new Uri(fileInfo.Path), cancellationToken)
 0179                        .ConfigureAwait(false);
 180
 0181                    await using (stream.ConfigureAwait(false))
 182                    {
 0183                        using var reader = new StreamReader(stream, detected.Encoding);
 0184                        var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
 185
 0186                        return new MemoryStream(Encoding.UTF8.GetBytes(text));
 187                    }
 0188                }
 0189            }
 190
 0191            return AsyncFile.OpenRead(fileInfo.Path);
 0192        }
 193
 194        internal async Task<SubtitleInfo> GetReadableFile(
 195            MediaSourceInfo mediaSource,
 196            MediaStream subtitleStream,
 197            CancellationToken cancellationToken)
 198        {
 4199            if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 200            {
 0201                await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
 202
 0203                var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream);
 0204                var outputFormat = GetExtractableSubtitleFormat(subtitleStream);
 0205                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension)
 0206                    ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUI
 207
 0208                return new SubtitleInfo()
 0209                {
 0210                    Path = outputPath,
 0211                    Protocol = MediaProtocol.File,
 0212                    Format = outputFormat,
 0213                    IsExternal = MediaStream.IsVobSubFormat(outputFormat)
 0214                };
 215            }
 216
 217            // Normalize ffmpeg codec names to the file extensions the parser is keyed on
 4218            var currentFormat = NormalizeCodecToParserExtension((Path.GetExtension(subtitleStream.Path) ?? subtitleStrea
 219
 220            // Handle PGS subtitles as raw streams for the client to render
 4221            if (MediaStream.IsPgsFormat(currentFormat))
 222            {
 0223                return new SubtitleInfo()
 0224                {
 0225                    Path = subtitleStream.Path,
 0226                    Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
 0227                    Format = "pgssub",
 0228                    IsExternal = true
 0229                };
 230            }
 231
 232            // Fallback to ffmpeg conversion
 4233            if (!_subtitleParser.SupportsFileExtension(currentFormat))
 234            {
 235                // Convert
 0236                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, ".srt")
 0237                    ?? throw new ResourceNotFoundException($"MediaSource {mediaSource.Id} has no subtitle cache (non-GUI
 238
 0239                await ConvertTextSubtitleToSrt(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwai
 240
 0241                return new SubtitleInfo()
 0242                {
 0243                    Path = outputPath,
 0244                    Protocol = MediaProtocol.File,
 0245                    Format = "srt",
 0246                    IsExternal = true
 0247                };
 248            }
 249
 250            // It's possible that the subtitleStream and mediaSource don't share the same protocol (e.g. .STRM file with
 4251            return new SubtitleInfo()
 4252            {
 4253                Path = subtitleStream.Path,
 4254                Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path),
 4255                Format = currentFormat,
 4256                IsExternal = true
 4257            };
 4258        }
 259
 260        private bool TryGetWriter(string format, [NotNullWhen(true)] out Nikse.SubtitleEdit.Core.SubtitleFormats.Subtitl
 261        {
 0262            ArgumentException.ThrowIfNullOrEmpty(format);
 263
 0264            if (string.Equals(format, SubtitleFormat.ASS, StringComparison.OrdinalIgnoreCase))
 265            {
 0266                value = new AdvancedSubStationAlpha();
 0267                return true;
 268            }
 269
 0270            if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
 271            {
 0272                value = new JsonWriter();
 0273                return true;
 274            }
 275
 0276            if (string.Equals(format, SubtitleFormat.SRT, StringComparison.OrdinalIgnoreCase)
 0277                || string.Equals(format, SubtitleFormat.SUBRIP, StringComparison.OrdinalIgnoreCase))
 278            {
 0279                value = new SubRip();
 0280                return true;
 281            }
 282
 0283            if (string.Equals(format, SubtitleFormat.SSA, StringComparison.OrdinalIgnoreCase))
 284            {
 0285                value = new SubStationAlpha();
 0286                return true;
 287            }
 288
 0289            if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase)
 0290                || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
 291            {
 0292                value = new WebVTT();
 0293                return true;
 294            }
 295
 0296            if (string.Equals(format, SubtitleFormat.TTML, StringComparison.OrdinalIgnoreCase))
 297            {
 0298                value = new TimedText10();
 0299                return true;
 300            }
 301
 0302            value = null;
 0303            return false;
 304        }
 305
 306        private Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat GetWriter(string format)
 307        {
 0308            if (TryGetWriter(format, out var writer))
 309            {
 0310                return writer;
 311            }
 312
 0313            throw new ArgumentException("Unsupported format: " + format);
 314        }
 315
 316        /// <summary>
 317        /// Converts the text subtitle to SRT.
 318        /// </summary>
 319        /// <param name="subtitleStream">The subtitle stream.</param>
 320        /// <param name="mediaSource">The input mediaSource.</param>
 321        /// <param name="outputPath">The output path.</param>
 322        /// <param name="cancellationToken">The cancellation token.</param>
 323        /// <returns>Task.</returns>
 324        private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outp
 325        {
 0326            using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
 327            {
 0328                if (!IsCachedSubtitleFresh(outputPath, subtitleStream.Path))
 329                {
 0330                    await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).C
 331                }
 0332            }
 0333        }
 334
 335        // ffmpeg codec names don't always match the file extensions the subtitle parser is keyed on.
 336        private static string NormalizeCodecToParserExtension(string codecOrExtension)
 337        {
 4338            return codecOrExtension switch
 4339            {
 0340                "subrip" => "srt",
 0341                "webvtt" => "vtt",
 4342                _ => codecOrExtension
 4343            };
 344        }
 345
 346        // Records "this cache was built from this exact source revision" in a sidecar file next to the cache: "<sizeByt
 0347        private static string GetCacheMetaPath(string cachePath) => cachePath + ".meta";
 348
 349        private static string FormatCacheMeta(long length, DateTime lastWriteUtc)
 0350            => string.Create(CultureInfo.InvariantCulture, $"{length}:{lastWriteUtc.Ticks}");
 351
 352        private bool IsCachedSubtitleFresh(string cachePath, string? sourcePath)
 353        {
 0354            if (!File.Exists(cachePath))
 355            {
 0356                return false;
 357            }
 358
 0359            var cacheInfo = _fileSystem.GetFileInfo(cachePath);
 0360            if (cacheInfo.Length == 0)
 361            {
 0362                return false;
 363            }
 364
 0365            if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath))
 366            {
 0367                return true;
 368            }
 369
 0370            var metaPath = GetCacheMetaPath(cachePath);
 0371            if (!File.Exists(metaPath))
 372            {
 373                // Pre-existing cache from before metadata tracking - regenerate so we can record the source state.
 0374                return false;
 375            }
 376
 377            try
 378            {
 0379                var sourceInfo = _fileSystem.GetFileInfo(sourcePath);
 0380                var expected = FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTimeUtc);
 0381                var actual = File.ReadAllText(metaPath);
 0382                return string.Equals(expected, actual, StringComparison.Ordinal);
 383            }
 0384            catch (IOException)
 385            {
 0386                return false;
 387            }
 0388        }
 389
 390        private void WriteCacheMeta(string cachePath, string? sourcePath)
 391        {
 0392            if (string.IsNullOrEmpty(sourcePath))
 393            {
 0394                return;
 395            }
 396
 397            try
 398            {
 0399                var sourceInfo = _fileSystem.GetFileInfo(sourcePath);
 0400                if (!sourceInfo.Exists)
 401                {
 0402                    return;
 403                }
 404
 0405                File.WriteAllText(GetCacheMetaPath(cachePath), FormatCacheMeta(sourceInfo.Length, sourceInfo.LastWriteTi
 0406            }
 0407            catch (IOException ex)
 408            {
 0409                _logger.LogWarning(ex, "Failed to record subtitle cache metadata for {CachePath}", cachePath);
 0410            }
 0411        }
 412
 413        /// <summary>
 414        /// Converts the text subtitle to SRT internal.
 415        /// </summary>
 416        /// <param name="subtitleStream">The subtitle stream.</param>
 417        /// <param name="mediaSource">The input mediaSource.</param>
 418        /// <param name="outputPath">The output path.</param>
 419        /// <param name="cancellationToken">The cancellation token.</param>
 420        /// <returns>Task.</returns>
 421        /// <exception cref="ArgumentNullException">
 422        /// The <c>inputPath</c> or <c>outputPath</c> is <c>null</c>.
 423        /// </exception>
 424        private async Task ConvertTextSubtitleToSrtInternal(MediaStream subtitleStream, MediaSourceInfo mediaSource, str
 425        {
 0426            var inputPath = subtitleStream.Path;
 0427            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 428
 0429            ArgumentException.ThrowIfNullOrEmpty(outputPath);
 430
 0431            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path (
 432
 0433            var encodingParam = await GetSubtitleFileCharacterSet(subtitleStream, subtitleStream.Language, mediaSource, 
 434
 435            // FFmpeg automatically convert character encoding when it is UTF-16
 436            // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to re
 0437            if ((inputPath.EndsWith(".smi", StringComparison.Ordinal) || inputPath.EndsWith(".sami", StringComparison.Or
 0438                (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) ||
 0439                 encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase)))
 440            {
 0441                encodingParam = string.Empty;
 442            }
 0443            else if (!string.IsNullOrEmpty(encodingParam))
 444            {
 0445                encodingParam = " -sub_charenc " + encodingParam;
 446            }
 447
 448            int exitCode;
 449
 0450            using (var process = new Process
 0451            {
 0452                StartInfo = new ProcessStartInfo
 0453                {
 0454                    CreateNoWindow = true,
 0455                    UseShellExecute = false,
 0456                    FileName = _mediaEncoder.EncoderPath,
 0457                    Arguments = string.Format(CultureInfo.InvariantCulture, "-y {0} -i \"{1}\" -c:s srt \"{2}\"", encodi
 0458                    WindowStyle = ProcessWindowStyle.Hidden,
 0459                    ErrorDialog = false
 0460                },
 0461                EnableRaisingEvents = true
 0462            })
 463            {
 0464                _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
 465
 466                try
 467                {
 0468                    process.Start();
 0469                }
 0470                catch (Exception ex)
 471                {
 0472                    _logger.LogError(ex, "Error starting ffmpeg");
 473
 0474                    throw;
 475                }
 476
 477                try
 478                {
 0479                    var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinut
 0480                    await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
 0481                    exitCode = process.ExitCode;
 0482                }
 0483                catch (OperationCanceledException)
 484                {
 0485                    process.Kill(true);
 0486                    exitCode = -1;
 0487                }
 0488            }
 489
 0490            var failed = false;
 491
 0492            if (exitCode == -1)
 493            {
 0494                failed = true;
 495
 0496                if (File.Exists(outputPath))
 497                {
 498                    try
 499                    {
 0500                        _logger.LogInformation("Deleting converted subtitle due to failure: {Path}", outputPath);
 0501                        _fileSystem.DeleteFile(outputPath);
 0502                    }
 0503                    catch (IOException ex)
 504                    {
 0505                        _logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
 0506                    }
 507                }
 508            }
 0509            else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 510            {
 0511                failed = true;
 512
 513                try
 514                {
 0515                    _logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath);
 0516                    _fileSystem.DeleteFile(outputPath);
 0517                }
 0518                catch (FileNotFoundException)
 519                {
 0520                }
 0521                catch (IOException ex)
 522                {
 0523                    _logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath);
 0524                }
 525            }
 526
 0527            if (failed)
 528            {
 0529                _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath);
 530
 0531                throw new FfmpegException(
 0532                    string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath))
 533            }
 534
 0535            await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
 536
 0537            WriteCacheMeta(outputPath, inputPath);
 538
 0539            _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
 0540        }
 541
 542        private string GetExtractableSubtitleFormat(MediaStream subtitleStream)
 543        {
 0544            if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
 0545                || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
 0546                || string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase))
 547            {
 0548                return subtitleStream.Codec;
 549            }
 0550            else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
 551            {
 0552                return "mks";
 553            }
 554            else
 555            {
 0556                return "srt";
 557            }
 558        }
 559
 560        private string GetExtractableSubtitleFileExtension(MediaStream subtitleStream)
 561        {
 562            // Using .pgssub as file extension is not allowed by ffmpeg. The file extension for pgs subtitles is .sup.
 0563            if (string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase))
 564            {
 0565                return "sup";
 566            }
 0567            else if (MediaStream.IsVobSubFormat(subtitleStream.Codec))
 568            {
 569                // FFmpeg cannot mux VobSub subtitle streams back into the .idx/.sub pair, so we use .mks container inst
 0570                return "mks";
 571            }
 572            else
 573            {
 0574                return GetExtractableSubtitleFormat(subtitleStream);
 575            }
 576        }
 577
 578        private bool IsCodecCopyable(string codec)
 579        {
 0580            return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase)
 0581                || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
 0582                || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
 0583                || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase)
 0584                || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase)
 0585                || MediaStream.IsVobSubFormat(codec);
 586        }
 587
 588        /// <inheritdoc />
 589        public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToke
 590        {
 0591            var locks = new List<IDisposable>();
 0592            var extractableStreams = new List<MediaStream>();
 593
 594            try
 595            {
 0596                var subtitleStreams = mediaSource.MediaStreams
 0597                    .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true });
 598
 0599                foreach (var subtitleStream in subtitleStreams)
 600                {
 0601                    if (subtitleStream.IsExternal
 0602                        && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 603                    {
 604                        continue;
 605                    }
 606
 0607                    var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitl
 0608                    if (outputPath is null)
 609                    {
 610                        continue;
 611                    }
 612
 0613                    var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false);
 614
 0615                    var sourcePath = string.IsNullOrEmpty(subtitleStream.Path) ? mediaSource.Path : subtitleStream.Path;
 0616                    if (IsCachedSubtitleFresh(outputPath, sourcePath))
 617                    {
 0618                        releaser.Dispose();
 0619                        continue;
 620                    }
 621
 0622                    locks.Add(releaser);
 0623                    extractableStreams.Add(subtitleStream);
 0624                }
 625
 0626                if (extractableStreams.Count > 0)
 627                {
 0628                    await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).Con
 0629                    await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).Configur
 630                }
 0631            }
 0632            catch (Exception ex)
 633            {
 0634                _logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path);
 0635            }
 636            finally
 637            {
 0638                locks.ForEach(x => x.Dispose());
 639            }
 0640        }
 641
 642        private async Task ExtractAllExtractableSubtitlesMKS(
 643           MediaSourceInfo mediaSource,
 644           List<MediaStream> subtitleStreams,
 645           CancellationToken cancellationToken)
 646        {
 0647            var mksFiles = new List<string>();
 648
 0649            foreach (var subtitleStream in subtitleStreams)
 650            {
 0651                if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.
 652                {
 653                    continue;
 654                }
 655
 0656                if (!mksFiles.Contains(subtitleStream.Path))
 657                {
 0658                    mksFiles.Add(subtitleStream.Path);
 659                }
 660            }
 661
 0662            if (mksFiles.Count == 0)
 663            {
 0664                return;
 665            }
 666
 0667            foreach (string mksFile in mksFiles)
 668            {
 0669                var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource);
 0670                var outputPaths = new List<string>();
 0671                var args = string.Format(
 0672                    CultureInfo.InvariantCulture,
 0673                    "-y -i {0}",
 0674                    inputPath);
 675
 0676                foreach (var subtitleStream in subtitleStreams)
 677                {
 0678                    if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase))
 679                    {
 680                        continue;
 681                    }
 682
 0683                    var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitl
 0684                    if (outputPath is null)
 685                    {
 686                        continue;
 687                    }
 688
 0689                    var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
 690                    // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
 0691                    var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.
 0692                    var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 693
 0694                    if (streamIndex == -1)
 695                    {
 0696                        _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this str
 0697                        continue;
 698                    }
 699
 0700                    Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Cal
 701
 0702                    outputPaths.Add(outputPath);
 0703                    args += string.Format(
 0704                        CultureInfo.InvariantCulture,
 0705                        " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
 0706                        streamIndex,
 0707                        outputCodec,
 0708                        outputFormatOption,
 0709                        outputPath);
 710                }
 711
 0712                await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
 713
 0714                foreach (var outputPath in outputPaths)
 715                {
 0716                    WriteCacheMeta(outputPath, mksFile);
 717                }
 0718            }
 0719        }
 720
 721        private async Task ExtractAllExtractableSubtitlesInternal(
 722            MediaSourceInfo mediaSource,
 723            List<MediaStream> subtitleStreams,
 724            CancellationToken cancellationToken)
 725        {
 0726            var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
 0727            var outputPaths = new List<string>();
 0728            var args = string.Format(
 0729                CultureInfo.InvariantCulture,
 0730                "-i {0}",
 0731                inputPath);
 732
 0733            foreach (var subtitleStream in subtitleStreams)
 734            {
 0735                if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.
 736                {
 0737                    _logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath
 0738                    continue;
 739                }
 740
 0741                var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFil
 0742                if (outputPath is null)
 743                {
 744                    continue;
 745                }
 746
 0747                var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
 748                // FFmpeg does not provide an .idx/.sub muxer, so VobSub streams must be written as MKS files.
 0749                var outputFormatOption = MediaStream.IsVobSubFormat(subtitleStream.Codec) ? " -f matroska" : string.Empt
 0750                var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 751
 0752                if (streamIndex == -1)
 753                {
 0754                    _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream"
 0755                    continue;
 756                }
 757
 0758                Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calcula
 759
 0760                outputPaths.Add(outputPath);
 0761                args += string.Format(
 0762                    CultureInfo.InvariantCulture,
 0763                    " -map 0:{0} -an -vn -c:s {1}{2} -flush_packets 1 \"{3}\"",
 0764                    streamIndex,
 0765                    outputCodec,
 0766                    outputFormatOption,
 0767                    outputPath);
 768            }
 769
 0770            if (outputPaths.Count > 0)
 771            {
 0772                await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false);
 773
 0774                foreach (var outputPath in outputPaths)
 775                {
 0776                    WriteCacheMeta(outputPath, mediaSource.Path);
 777                }
 778            }
 0779        }
 780
 781        private async Task ExtractSubtitlesForFile(
 782            string inputPath,
 783            string args,
 784            List<string> outputPaths,
 785            CancellationToken cancellationToken)
 786        {
 787            int exitCode;
 788
 0789            using (var process = new Process
 0790            {
 0791                StartInfo = new ProcessStartInfo
 0792                {
 0793                    CreateNoWindow = true,
 0794                    UseShellExecute = false,
 0795                    FileName = _mediaEncoder.EncoderPath,
 0796                    Arguments = args,
 0797                    WindowStyle = ProcessWindowStyle.Hidden,
 0798                    ErrorDialog = false
 0799                },
 0800                EnableRaisingEvents = true
 0801            })
 802            {
 0803                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 804
 805                try
 806                {
 0807                    process.Start();
 0808                }
 0809                catch (Exception ex)
 810                {
 0811                    _logger.LogError(ex, "Error starting ffmpeg");
 812
 0813                    throw;
 814                }
 815
 816                try
 817                {
 0818                    var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinut
 0819                    await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
 0820                    exitCode = process.ExitCode;
 0821                }
 0822                catch (OperationCanceledException)
 823                {
 0824                    process.Kill(true);
 0825                    exitCode = -1;
 0826                }
 0827            }
 828
 0829            var failed = false;
 830
 0831            if (exitCode == -1)
 832            {
 0833                failed = true;
 834
 0835                foreach (var outputPath in outputPaths)
 836                {
 837                    try
 838                    {
 0839                        _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 0840                        _fileSystem.DeleteFile(outputPath);
 0841                    }
 0842                    catch (FileNotFoundException)
 843                    {
 0844                    }
 0845                    catch (IOException ex)
 846                    {
 0847                        _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 0848                    }
 849                }
 850            }
 851            else
 852            {
 0853                foreach (var outputPath in outputPaths)
 854                {
 0855                    if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 856                    {
 0857                        _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath,
 0858                        failed = true;
 859
 860                        try
 861                        {
 0862                            _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 0863                            _fileSystem.DeleteFile(outputPath);
 0864                        }
 0865                        catch (FileNotFoundException)
 866                        {
 0867                        }
 0868                        catch (IOException ex)
 869                        {
 0870                            _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 0871                        }
 872
 873                        continue;
 874                    }
 875
 0876                    if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase))
 877                    {
 0878                        await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
 879                    }
 880
 0881                    _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", input
 0882                }
 883            }
 884
 0885            if (failed)
 886            {
 0887                throw new FfmpegException(
 0888                    string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath))
 889            }
 0890        }
 891
 892        /// <summary>
 893        /// Extracts the text subtitle.
 894        /// </summary>
 895        /// <param name="mediaSource">The mediaSource.</param>
 896        /// <param name="subtitleStream">The subtitle stream.</param>
 897        /// <param name="outputCodec">The output codec.</param>
 898        /// <param name="outputPath">The output path.</param>
 899        /// <param name="cancellationToken">The cancellation token.</param>
 900        /// <returns>Task.</returns>
 901        /// <exception cref="ArgumentException">Must use inputPath list overload.</exception>
 902        private async Task ExtractTextSubtitle(
 903            MediaSourceInfo mediaSource,
 904            MediaStream subtitleStream,
 905            string outputCodec,
 906            string outputPath,
 907            CancellationToken cancellationToken)
 908        {
 0909            using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
 910            {
 0911                if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 912                {
 0913                    var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
 914
 0915                    var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
 916
 0917                    if (subtitleStream.IsExternal)
 918                    {
 0919                        args = _mediaEncoder.GetExternalSubtitleInputArgument(subtitleStream.Path);
 920                    }
 921
 0922                    await ExtractTextSubtitleInternal(
 0923                        args,
 0924                        subtitleStreamIndex,
 0925                        outputCodec,
 0926                        outputPath,
 0927                        cancellationToken).ConfigureAwait(false);
 928                }
 0929            }
 0930        }
 931
 932        private async Task ExtractTextSubtitleInternal(
 933            string inputPath,
 934            int subtitleStreamIndex,
 935            string outputCodec,
 936            string outputPath,
 937            CancellationToken cancellationToken)
 938        {
 0939            ArgumentException.ThrowIfNullOrEmpty(inputPath);
 940
 0941            ArgumentException.ThrowIfNullOrEmpty(outputPath);
 942
 0943            Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path (
 944
 0945            var processArgs = string.Format(
 0946                CultureInfo.InvariantCulture,
 0947                "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
 0948                inputPath,
 0949                subtitleStreamIndex,
 0950                outputCodec,
 0951                outputPath);
 952
 953            int exitCode;
 954
 0955            using (var process = new Process
 0956            {
 0957                StartInfo = new ProcessStartInfo
 0958                {
 0959                    CreateNoWindow = true,
 0960                    UseShellExecute = false,
 0961                    FileName = _mediaEncoder.EncoderPath,
 0962                    Arguments = processArgs,
 0963                    WindowStyle = ProcessWindowStyle.Hidden,
 0964                    ErrorDialog = false
 0965                },
 0966                EnableRaisingEvents = true
 0967            })
 968            {
 0969                _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
 970
 971                try
 972                {
 0973                    process.Start();
 0974                }
 0975                catch (Exception ex)
 976                {
 0977                    _logger.LogError(ex, "Error starting ffmpeg");
 978
 0979                    throw;
 980                }
 981
 982                try
 983                {
 0984                    var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinut
 0985                    await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false);
 0986                    exitCode = process.ExitCode;
 0987                }
 0988                catch (OperationCanceledException)
 989                {
 0990                    process.Kill(true);
 0991                    exitCode = -1;
 0992                }
 0993            }
 994
 0995            var failed = false;
 996
 0997            if (exitCode == -1)
 998            {
 0999                failed = true;
 1000
 1001                try
 1002                {
 01003                    _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 01004                    _fileSystem.DeleteFile(outputPath);
 01005                }
 01006                catch (FileNotFoundException)
 1007                {
 01008                }
 01009                catch (IOException ex)
 1010                {
 01011                    _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 01012                }
 1013            }
 01014            else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0)
 1015            {
 01016                failed = true;
 1017
 1018                try
 1019                {
 01020                    _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
 01021                    _fileSystem.DeleteFile(outputPath);
 01022                }
 01023                catch (FileNotFoundException)
 1024                {
 01025                }
 01026                catch (IOException ex)
 1027                {
 01028                    _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
 01029                }
 1030            }
 1031
 01032            if (failed)
 1033            {
 01034                _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputP
 1035
 01036                throw new FfmpegException(
 01037                    string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inpu
 1038            }
 1039
 01040            _logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, ou
 1041
 01042            if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
 1043            {
 01044                await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
 1045            }
 01046        }
 1047
 1048        /// <summary>
 1049        /// Sets the ass font.
 1050        /// </summary>
 1051        /// <param name="file">The file.</param>
 1052        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>Syst
 1053        /// <returns>Task.</returns>
 1054        private async Task SetAssFont(string file, CancellationToken cancellationToken = default)
 1055        {
 01056            _logger.LogInformation("Setting ass font within {File}", file);
 1057
 1058            string text;
 1059            Encoding encoding;
 1060
 01061            using (var fileStream = AsyncFile.OpenRead(file))
 01062            using (var reader = new StreamReader(fileStream, true))
 1063            {
 01064                encoding = reader.CurrentEncoding;
 1065
 01066                text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
 01067            }
 1068
 01069            var newText = text.Replace(",Arial,", ",Arial Unicode MS,", StringComparison.Ordinal);
 1070
 01071            if (!string.Equals(text, newText, StringComparison.Ordinal))
 1072            {
 01073                var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.File
 01074                await using (fileStream.ConfigureAwait(false))
 1075                {
 01076                    var writer = new StreamWriter(fileStream, encoding);
 01077                    await using (writer.ConfigureAwait(false))
 1078                    {
 01079                        await writer.WriteAsync(newText.AsMemory(), cancellationToken).ConfigureAwait(false);
 1080                    }
 1081                }
 1082            }
 01083        }
 1084
 1085        private string? GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitle
 1086        {
 01087            return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension);
 1088        }
 1089
 1090        /// <inheritdoc />
 1091        public async Task<string> GetSubtitleFileCharacterSet(MediaStream subtitleStream, string language, MediaSourceIn
 1092        {
 01093            var subtitleCodec = subtitleStream.Codec;
 01094            var path = subtitleStream.Path;
 1095
 01096            if (path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
 1097            {
 01098                var cachePath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + subtitleCodec);
 01099                if (cachePath is not null)
 1100                {
 01101                    path = cachePath;
 01102                    await ExtractTextSubtitle(mediaSource, subtitleStream, subtitleCodec, path, cancellationToken)
 01103                        .ConfigureAwait(false);
 1104                }
 1105            }
 1106
 01107            var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false);
 01108            var charset = result.Detected?.EncodingName ?? string.Empty;
 1109
 1110            // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding
 01111            if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || p
 01112                && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase)
 01113                    || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase)))
 1114            {
 01115                charset = string.Empty;
 1116            }
 1117
 01118            _logger.LogDebug("charset {0} detected for {Path}", charset, path);
 1119
 01120            return charset;
 01121        }
 1122
 1123        private async Task<DetectionResult> DetectCharset(string path, MediaProtocol protocol, CancellationToken cancell
 1124        {
 1125            switch (protocol)
 1126            {
 1127                case MediaProtocol.Http:
 1128                    {
 01129                        using var stream = await _httpClientFactory
 01130                          .CreateClient(NamedClient.Default)
 01131                          .GetStreamAsync(new Uri(path), cancellationToken)
 01132                          .ConfigureAwait(false);
 1133
 01134                        return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(fal
 1135                    }
 1136
 1137                case MediaProtocol.File:
 1138                    {
 01139                        return await CharsetDetector.DetectFromFileAsync(path, cancellationToken)
 01140                                              .ConfigureAwait(false);
 1141                    }
 1142
 1143                default:
 01144                    throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol");
 1145            }
 01146        }
 1147
 1148        public async Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, Cancellat
 1149        {
 01150            var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken)
 01151                .ConfigureAwait(false);
 01152            return info.Path;
 01153        }
 1154
 1155        /// <inheritdoc />
 1156        public void Dispose()
 1157        {
 211158            _semaphoreLocks.Dispose();
 211159        }
 1160
 1161#pragma warning disable CA1034 // Nested types should not be visible
 1162        // Only public for the unit tests
 1163        public readonly record struct SubtitleInfo
 1164        {
 1165            public string Path { get; init; }
 1166
 1167            public MediaProtocol Protocol { get; init; }
 1168
 1169            public string Format { get; init; }
 1170
 1171            public bool IsExternal { get; init; }
 1172        }
 1173    }
 1174}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.MediaEncoding.IMediaEncoder,System.Net.Http.IHttpClientFactory,MediaBrowser.Controller.Library.IMediaSourceManager,MediaBrowser.MediaEncoding.Subtitles.ISubtitleParser,MediaBrowser.Controller.IO.IPathManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
ConvertSubtitles(System.IO.Stream,MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder/SubtitleInfo,System.String,System.Int64,System.Int64,System.Boolean)
FilterEvents(Nikse.SubtitleEdit.Core.Common.Subtitle,System.Int64,System.Int64,System.Boolean)
MediaBrowser-Controller-MediaEncoding-ISubtitleEncoder-GetSubtitles()
GetSubtitleStream()
GetSubtitleStream()
GetReadableFile()
TryGetWriter(System.String,Nikse.SubtitleEdit.Core.SubtitleFormats.SubtitleFormat&)
GetWriter(System.String)
ConvertTextSubtitleToSrt()
NormalizeCodecToParserExtension(System.String)
GetCacheMetaPath(System.String)
FormatCacheMeta(System.Int64,System.DateTime)
IsCachedSubtitleFresh(System.String,System.String)
WriteCacheMeta(System.String,System.String)
ConvertTextSubtitleToSrtInternal()
GetExtractableSubtitleFormat(MediaBrowser.Model.Entities.MediaStream)
GetExtractableSubtitleFileExtension(MediaBrowser.Model.Entities.MediaStream)
IsCodecCopyable(System.String)
ExtractAllExtractableSubtitles()
ExtractAllExtractableSubtitlesMKS()
ExtractAllExtractableSubtitlesInternal()
ExtractSubtitlesForFile()
ExtractTextSubtitle()
ExtractTextSubtitleInternal()
SetAssFont()
GetSubtitleCachePath(MediaBrowser.Model.Dto.MediaSourceInfo,System.Int32,System.String)
GetSubtitleFileCharacterSet()
DetectCharset()
GetSubtitleFilePath()
Dispose()