diff options
Diffstat (limited to 'MediaBrowser.MediaEncoding/Subtitles')
3 files changed, 324 insertions, 122 deletions
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs index fd55db4ba4..d060b247da 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEditParser.cs @@ -17,7 +17,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public class SubtitleEditParser : ISubtitleParser { private readonly ILogger<SubtitleEditParser> _logger; - private readonly Dictionary<string, SubtitleFormat[]> _subtitleFormats; + private readonly Dictionary<string, List<Type>> _subtitleFormatTypes; /// <summary> /// Initializes a new instance of the <see cref="SubtitleEditParser"/> class. @@ -26,10 +26,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles public SubtitleEditParser(ILogger<SubtitleEditParser> logger) { _logger = logger; - _subtitleFormats = GetSubtitleFormats() - .Where(subtitleFormat => !string.IsNullOrEmpty(subtitleFormat.Extension)) - .GroupBy(subtitleFormat => subtitleFormat.Extension.TrimStart('.'), StringComparer.OrdinalIgnoreCase) - .ToDictionary(g => g.Key, g => g.ToArray(), StringComparer.OrdinalIgnoreCase); + _subtitleFormatTypes = GetSubtitleFormatTypes(); } /// <inheritdoc /> @@ -38,13 +35,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles var subtitle = new Subtitle(); var lines = stream.ReadAllLines().ToList(); - if (!_subtitleFormats.TryGetValue(fileExtension, out var subtitleFormats)) + if (!_subtitleFormatTypes.TryGetValue(fileExtension, out var subtitleFormatTypesForExtension)) { throw new ArgumentException($"Unsupported file extension: {fileExtension}", nameof(fileExtension)); } - foreach (var subtitleFormat in subtitleFormats) + foreach (var subtitleFormatType in subtitleFormatTypesForExtension) { + var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(subtitleFormatType, true)!; _logger.LogDebug( "Trying to parse '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", fileExtension, @@ -54,12 +52,23 @@ namespace MediaBrowser.MediaEncoding.Subtitles { break; } - - _logger.LogError( - "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", - subtitleFormat.ErrorCount, - fileExtension, - subtitleFormat.Name); + else if (subtitleFormat.TryGetErrors(out var errors)) + { + _logger.LogError( + "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser, errors: {Errors}", + subtitleFormat.ErrorCount, + fileExtension, + subtitleFormat.Name, + errors); + } + else + { + _logger.LogError( + "{ErrorCount} errors encountered while parsing '{FileExtension}' subtitle using the {SubtitleFormatParser} format parser", + subtitleFormat.ErrorCount, + fileExtension, + subtitleFormat.Name); + } } if (subtitle.Paragraphs.Count == 0) @@ -86,11 +95,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <inheritdoc /> public bool SupportsFileExtension(string fileExtension) - => _subtitleFormats.ContainsKey(fileExtension); + => _subtitleFormatTypes.ContainsKey(fileExtension); - private List<SubtitleFormat> GetSubtitleFormats() + private Dictionary<string, List<Type>> GetSubtitleFormatTypes() { - var subtitleFormats = new List<SubtitleFormat>(); + var subtitleFormatTypes = new Dictionary<string, List<Type>>(StringComparer.OrdinalIgnoreCase); var assembly = typeof(SubtitleFormat).Assembly; foreach (var type in assembly.GetTypes()) @@ -102,9 +111,20 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - // It shouldn't be null, but the exception is caught if it is - var subtitleFormat = (SubtitleFormat)Activator.CreateInstance(type, true)!; - subtitleFormats.Add(subtitleFormat); + var tempInstance = (SubtitleFormat)Activator.CreateInstance(type, true)!; + var extension = tempInstance.Extension.TrimStart('.'); + if (!string.IsNullOrEmpty(extension)) + { + // Store only the type, we will instantiate from it later + if (!subtitleFormatTypes.TryGetValue(extension, out var subtitleFormatTypesForExtension)) + { + subtitleFormatTypes[extension] = [type]; + } + else + { + subtitleFormatTypesForExtension.Add(type); + } + } } catch (Exception ex) { @@ -112,7 +132,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } - return subtitleFormats; + return subtitleFormatTypes; } } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 4b1b1bbc61..5920fe3289 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -16,7 +16,9 @@ using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; @@ -31,12 +33,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable { private readonly ILogger<SubtitleEncoder> _logger; - private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; private readonly IMediaEncoder _mediaEncoder; private readonly IHttpClientFactory _httpClientFactory; private readonly IMediaSourceManager _mediaSourceManager; private readonly ISubtitleParser _subtitleParser; + private readonly IPathManager _pathManager; + private readonly IServerConfigurationManager _serverConfigurationManager; /// <summary> /// The _semaphoreLocks. @@ -49,24 +52,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles public SubtitleEncoder( ILogger<SubtitleEncoder> logger, - IApplicationPaths appPaths, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IHttpClientFactory httpClientFactory, IMediaSourceManager mediaSourceManager, - ISubtitleParser subtitleParser) + ISubtitleParser subtitleParser, + IPathManager pathManager, + IServerConfigurationManager serverConfigurationManager) { _logger = logger; - _appPaths = appPaths; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _httpClientFactory = httpClientFactory; _mediaSourceManager = mediaSourceManager; _subtitleParser = subtitleParser; + _pathManager = pathManager; + _serverConfigurationManager = serverConfigurationManager; } - private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); - private MemoryStream ConvertSubtitles( Stream stream, string inputFormat, @@ -98,11 +101,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles return ms; } - private void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) + internal void FilterEvents(SubtitleTrackInfo track, long startPositionTicks, long endTimeTicks, bool preserveTimestamps) { - // Drop subs that are earlier than what we're looking for + // Drop subs that have fully elapsed before the requested start position track.TrackEvents = track.TrackEvents - .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 || (i.EndPositionTicks - startPositionTicks) < 0) + .SkipWhile(i => (i.StartPositionTicks - startPositionTicks) < 0 && (i.EndPositionTicks - startPositionTicks) < 0) .ToArray(); if (endTimeTicks > 0) @@ -116,8 +119,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles { foreach (var trackEvent in track.TrackEvents) { - trackEvent.EndPositionTicks -= startPositionTicks; - trackEvent.StartPositionTicks -= startPositionTicks; + trackEvent.EndPositionTicks = Math.Max(0, trackEvent.EndPositionTicks - startPositionTicks); + trackEvent.StartPositionTicks = Math.Max(0, trackEvent.StartPositionTicks - startPositionTicks); } } } @@ -169,21 +172,25 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task<Stream> GetSubtitleStream(SubtitleInfo fileInfo, CancellationToken cancellationToken) { - if (fileInfo.IsExternal) + if (fileInfo.Protocol == MediaProtocol.Http) { - using (var stream = await GetStream(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false)) + var result = await DetectCharset(fileInfo.Path, fileInfo.Protocol, cancellationToken).ConfigureAwait(false); + var detected = result.Detected; + + if (detected is not null) { - var result = CharsetDetector.DetectFromStream(stream).Detected; - stream.Position = 0; + _logger.LogDebug("charset {CharSet} detected for {Path}", detected.EncodingName, fileInfo.Path); - if (result is not null) - { - _logger.LogDebug("charset {CharSet} detected for {Path}", result.EncodingName, fileInfo.Path); + using var stream = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetStreamAsync(new Uri(fileInfo.Path), cancellationToken) + .ConfigureAwait(false); - using var reader = new StreamReader(stream, result.Encoding); - var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + using var reader = new StreamReader(stream, detected.Encoding); + var text = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - return new MemoryStream(Encoding.UTF8.GetBytes(text)); + return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } } @@ -198,10 +205,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles { if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) { - await ExtractAllTextSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); + await ExtractAllExtractableSubtitles(mediaSource, cancellationToken).ConfigureAwait(false); - var outputFormat = GetTextSubtitleFormat(subtitleStream); - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat); + var outputFileExtension = GetExtractableSubtitleFileExtension(subtitleStream); + var outputFormat = GetExtractableSubtitleFormat(subtitleStream); + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFileExtension); return new SubtitleInfo() { @@ -212,9 +220,21 @@ namespace MediaBrowser.MediaEncoding.Subtitles }; } - var currentFormat = (Path.GetExtension(subtitleStream.Path) ?? subtitleStream.Codec) + var currentFormat = subtitleStream.Codec ?? Path.GetExtension(subtitleStream.Path) .TrimStart('.'); + // Handle PGS subtitles as raw streams for the client to render + if (MediaStream.IsPgsFormat(currentFormat)) + { + return new SubtitleInfo() + { + Path = subtitleStream.Path, + Protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path), + Format = "pgssub", + IsExternal = true + }; + } + // Fallback to ffmpeg conversion if (!_subtitleParser.SupportsFileExtension(currentFormat)) { @@ -308,7 +328,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false); } @@ -381,7 +401,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes; + await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false); exitCode = process.ExitCode; } catch (OperationCanceledException) @@ -410,9 +431,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } } - else if (!File.Exists(outputPath)) + else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { failed = true; + + try + { + _logger.LogWarning("Deleting converted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting converted subtitle {Path}", outputPath); + } } if (failed) @@ -428,10 +462,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); } - private string GetTextSubtitleFormat(MediaStream subtitleStream) + private string GetExtractableSubtitleFormat(MediaStream subtitleStream) { if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) - || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)) + || string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase) + || string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase)) { return subtitleStream.Codec; } @@ -441,50 +476,64 @@ namespace MediaBrowser.MediaEncoding.Subtitles } } + private string GetExtractableSubtitleFileExtension(MediaStream subtitleStream) + { + // Using .pgssub as file extension is not allowed by ffmpeg. The file extension for pgs subtitles is .sup. + if (string.Equals(subtitleStream.Codec, "pgssub", StringComparison.OrdinalIgnoreCase)) + { + return "sup"; + } + else + { + return GetExtractableSubtitleFormat(subtitleStream); + } + } + private bool IsCodecCopyable(string codec) { return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase) - || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase); + || string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "pgssub", StringComparison.OrdinalIgnoreCase); } - /// <summary> - /// Extracts all text subtitles. - /// </summary> - /// <param name="mediaSource">The mediaSource.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task ExtractAllExtractableSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken) { - var locks = new List<AsyncKeyedLockReleaser<string>>(); + var locks = new List<IDisposable>(); var extractableStreams = new List<MediaStream>(); try { var subtitleStreams = mediaSource.MediaStreams - .Where(stream => stream.IsTextSubtitleStream && stream.SupportsExternalStream); + .Where(stream => stream is { IsExtractableSubtitleStream: true, SupportsExternalStream: true }); foreach (var subtitleStream in subtitleStreams) { - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); + if (subtitleStream.IsExternal && !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); - var @lock = _semaphoreLocks.GetOrAdd(outputPath); - await @lock.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); + var releaser = await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false); - if (File.Exists(outputPath)) + if (File.Exists(outputPath) && _fileSystem.GetFileInfo(outputPath).Length > 0) { - @lock.Dispose(); + releaser.Dispose(); continue; } - locks.Add(@lock); + locks.Add(releaser); extractableStreams.Add(subtitleStream); } if (extractableStreams.Count > 0) { - await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); + await ExtractAllExtractableSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); + await ExtractAllExtractableSubtitlesMKS(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false); } } catch (Exception ex) @@ -493,28 +542,97 @@ namespace MediaBrowser.MediaEncoding.Subtitles } finally { - foreach (var @lock in locks) + locks.ForEach(x => x.Dispose()); + } + } + + private async Task ExtractAllExtractableSubtitlesMKS( + MediaSourceInfo mediaSource, + List<MediaStream> subtitleStreams, + CancellationToken cancellationToken) + { + var mksFiles = new List<string>(); + + foreach (var subtitleStream in subtitleStreams) + { + if (string.IsNullOrEmpty(subtitleStream.Path) || !subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!mksFiles.Contains(subtitleStream.Path)) + { + mksFiles.Add(subtitleStream.Path); + } + } + + if (mksFiles.Count == 0) + { + return; + } + + foreach (string mksFile in mksFiles) + { + var inputPath = _mediaEncoder.GetInputArgument(mksFile, mediaSource); + var outputPaths = new List<string>(); + var args = string.Format( + CultureInfo.InvariantCulture, + "-i {0}", + inputPath); + + foreach (var subtitleStream in subtitleStreams) { - @lock.Dispose(); + if (!subtitleStream.Path.Equals(mksFile, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); + var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; + var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); + + if (streamIndex == -1) + { + _logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index); + continue; + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid.")); + + outputPaths.Add(outputPath); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", + streamIndex, + outputCodec, + outputPath); } + + await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); } } - private async Task ExtractAllTextSubtitlesInternal( + private async Task ExtractAllExtractableSubtitlesInternal( MediaSourceInfo mediaSource, List<MediaStream> subtitleStreams, CancellationToken cancellationToken) { - var inputPath = mediaSource.Path; + var inputPath = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource); var outputPaths = new List<string>(); var args = string.Format( CultureInfo.InvariantCulture, - "-i \"{0}\" -copyts", + "-i {0}", inputPath); foreach (var subtitleStream in subtitleStreams) { - var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream)); + if (!string.IsNullOrEmpty(subtitleStream.Path) && subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("Subtitle {Index} for file {InputPath} is part in an MKS file. Skipping", inputPath, subtitleStream.Index); + continue; + } + + var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetExtractableSubtitleFileExtension(subtitleStream)); var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt"; var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -529,12 +647,26 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputPaths.Add(outputPath); args += string.Format( CultureInfo.InvariantCulture, - " -map 0:{0} -an -vn -c:s {1} \"{2}\"", + " -map 0:{0} -an -vn -c:s {1} -flush_packets 1 \"{2}\"", streamIndex, outputCodec, outputPath); } + if (outputPaths.Count == 0) + { + return; + } + + await ExtractSubtitlesForFile(inputPath, args, outputPaths, cancellationToken).ConfigureAwait(false); + } + + private async Task ExtractSubtitlesForFile( + string inputPath, + string args, + List<string> outputPaths, + CancellationToken cancellationToken) + { int exitCode; using (var process = new Process @@ -566,7 +698,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes; + await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false); exitCode = process.ExitCode; } catch (OperationCanceledException) @@ -602,10 +735,24 @@ namespace MediaBrowser.MediaEncoding.Subtitles { foreach (var outputPath in outputPaths) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { _logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath); failed = true; + + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } + continue; } @@ -644,7 +791,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false)) { - if (!File.Exists(outputPath)) + if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream); @@ -680,7 +827,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles var processArgs = string.Format( CultureInfo.InvariantCulture, - "-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"", + "-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"", inputPath, subtitleStreamIndex, outputCodec, @@ -717,7 +864,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles try { - await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); + var timeoutMinutes = _serverConfigurationManager.GetEncodingOptions().SubtitleExtractionTimeoutMinutes; + await process.WaitForExitAsync(TimeSpan.FromMinutes(timeoutMinutes)).ConfigureAwait(false); exitCode = process.ExitCode; } catch (OperationCanceledException) @@ -746,9 +894,22 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); } } - else if (!File.Exists(outputPath)) + else if (!File.Exists(outputPath) || _fileSystem.GetFileInfo(outputPath).Length == 0) { failed = true; + + try + { + _logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath); + _fileSystem.DeleteFile(outputPath); + } + catch (FileNotFoundException) + { + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath); + } } if (failed) @@ -806,26 +967,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles private string GetSubtitleCachePath(MediaSourceInfo mediaSource, int subtitleStreamIndex, string outputSubtitleExtension) { - if (mediaSource.Protocol == MediaProtocol.File) - { - var ticksParam = string.Empty; - - var date = _fileSystem.GetLastWriteTimeUtc(mediaSource.Path); - - ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; - - var prefix = filename.Slice(0, 1); - - return Path.Join(SubtitleCachePath, prefix, filename); - } - else - { - ReadOnlySpan<char> filename = (mediaSource.Path + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension; - - var prefix = filename.Slice(0, 1); - - return Path.Join(SubtitleCachePath, prefix, filename); - } + return _pathManager.GetSubtitlePath(mediaSource.Id, subtitleStreamIndex, outputSubtitleExtension); } /// <inheritdoc /> @@ -841,43 +983,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles .ConfigureAwait(false); } - using (var stream = await GetStream(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false)) - { - var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName ?? string.Empty; + var result = await DetectCharset(path, mediaSource.Protocol, cancellationToken).ConfigureAwait(false); + var charset = result.Detected?.EncodingName ?? string.Empty; - // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding - if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) - && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase) - || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase))) - { - charset = string.Empty; - } + // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding + if ((path.EndsWith(".ass", StringComparison.Ordinal) || path.EndsWith(".ssa", StringComparison.Ordinal) || path.EndsWith(".srt", StringComparison.Ordinal)) + && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase) + || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase))) + { + charset = string.Empty; + } - _logger.LogDebug("charset {0} detected for {Path}", charset, path); + _logger.LogDebug("charset {0} detected for {Path}", charset, path); - return charset; - } + return charset; } - private async Task<Stream> GetStream(string path, MediaProtocol protocol, CancellationToken cancellationToken) + private async Task<DetectionResult> DetectCharset(string path, MediaProtocol protocol, CancellationToken cancellationToken) { switch (protocol) { case MediaProtocol.Http: - { - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(new Uri(path), cancellationToken) - .ConfigureAwait(false); - return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - } + { + using var stream = await _httpClientFactory + .CreateClient(NamedClient.Default) + .GetStreamAsync(new Uri(path), cancellationToken) + .ConfigureAwait(false); + + return await CharsetDetector.DetectFromStreamAsync(stream, cancellationToken).ConfigureAwait(false); + } case MediaProtocol.File: - return AsyncFile.OpenRead(path); + { + return await CharsetDetector.DetectFromFileAsync(path, cancellationToken) + .ConfigureAwait(false); + } + default: - throw new ArgumentOutOfRangeException(nameof(protocol)); + throw new ArgumentOutOfRangeException(nameof(protocol), protocol, "Unsupported protocol"); } } + public async Task<string> GetSubtitleFilePath(MediaStream subtitleStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken) + { + var info = await GetReadableFile(mediaSource, subtitleStream, cancellationToken) + .ConfigureAwait(false); + return info.Path; + } + /// <inheritdoc /> public void Dispose() { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs new file mode 100644 index 0000000000..88c2bf3db2 --- /dev/null +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleFormatExtensions.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using Nikse.SubtitleEdit.Core.SubtitleFormats; + +namespace MediaBrowser.MediaEncoding.Subtitles; + +internal static class SubtitleFormatExtensions +{ + /// <summary> + /// Will try to find errors if supported by provider. + /// </summary> + /// <param name="format">The subtitle format.</param> + /// <param name="errors">The out errors value.</param> + /// <returns>True if errors are available for given format.</returns> + public static bool TryGetErrors(this SubtitleFormat format, [NotNullWhen(true)] out string? errors) + { + errors = format switch + { + SubStationAlpha ssa => ssa.Errors, + AdvancedSubStationAlpha assa => assa.Errors, + SubRip subRip => subRip.Errors, + MicroDvd microDvd => microDvd.Errors, + DCinemaSmpte2007 smpte2007 => smpte2007.Errors, + DCinemaSmpte2010 smpte2010 => smpte2010.Errors, + _ => null, + }; + + return !string.IsNullOrWhiteSpace(errors); + } +} |
