diff --git a/CHANGELOG.md b/CHANGELOG.md index 958f2bcf0..bf0c933ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Note that these subtitles are not currently supported in ETV, but they did cause a playback issue - Fix Jellyfin, Emby and Plex library scans that wouldn't work in certain timezones - Fix song normalization to match FFmpeg Profile bit depth +- Fix bug playing some external subtitle files (e.g. with an apostrophe in the file name) ### Changed - Ignore case of video and audio file extensions in local folder scanner diff --git a/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathById.cs b/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathById.cs new file mode 100644 index 000000000..6b9f5f4fe --- /dev/null +++ b/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathById.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Subtitles.Queries; + +public record GetSubtitlePathById(int Id) : IRequest>; diff --git a/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs b/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs new file mode 100644 index 000000000..a559441bc --- /dev/null +++ b/ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs @@ -0,0 +1,27 @@ +using ErsatzTV.Core; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Subtitles.Queries; + +public class GetSubtitlePathByIdHandler : IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + + public GetSubtitlePathByIdHandler(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public async Task> Handle( + GetSubtitlePathById request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + Option maybeSubtitlePath = await dbContext.Subtitles + .SelectOneAsync(s => s.Id, s => s.Id == request.Id) + .MapT(s => s.Path); + return maybeSubtitlePath.ToEither(BaseError.New($"Unable to locate subtitle with id {request.Id}")); + } +} diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 1b2ab0f4c..3eefbcc44 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -98,6 +98,15 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService preferredSubtitleLanguage, subtitleMode); + foreach (Subtitle subtitle in maybeSubtitle) + { + if (subtitle.SubtitleKind == SubtitleKind.Sidecar) + { + // proxy to avoid dealing with escaping + subtitle.Path = $"http://localhost:{Settings.ListenPort}/media/subtitle/{subtitle.Id}"; + } + } + Option watermarkOptions = disableWatermarks ? None : await _ffmpegProcessService.GetWatermarkOptions( @@ -194,9 +203,12 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService subtitle.Codec, StreamKind.Video); - string path = subtitle.IsImage - ? videoPath - : Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path); + string path = subtitle.IsImage switch + { + true => videoPath, + false when subtitle.SubtitleKind == SubtitleKind.Sidecar => subtitle.Path, + _ => Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path) + }; return new SubtitleInputFile( path, diff --git a/ErsatzTV.FFmpeg/Filter/SubtitlesFilter.cs b/ErsatzTV.FFmpeg/Filter/SubtitlesFilter.cs index b3247425b..e14abfb82 100644 --- a/ErsatzTV.FFmpeg/Filter/SubtitlesFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/SubtitlesFilter.cs @@ -34,7 +34,8 @@ public class SubtitlesFilter : BaseFilter // escape brackets after escaping for windows effectiveFile = effectiveFile .Replace(@"[", @"\[") - .Replace(@"]", @"\]"); + .Replace(@"]", @"\]") + .Replace(@"http://localhost:", @"http\\://localhost\\:"); return $"subtitles={effectiveFile}:fontsdir={fontsDir}"; } diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 041a96050..a521980ae 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -84,6 +84,7 @@ public class TvContext : DbContext public DbSet LanguageCodes { get; set; } public DbSet TraktLists { get; set; } public DbSet FillerPresets { get; set; } + public DbSet Subtitles { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseLoggerFactory(_loggerFactory); diff --git a/ErsatzTV/Controllers/InternalController.cs b/ErsatzTV/Controllers/InternalController.cs index 4e5d43f33..d890c41b5 100644 --- a/ErsatzTV/Controllers/InternalController.cs +++ b/ErsatzTV/Controllers/InternalController.cs @@ -4,6 +4,7 @@ using ErsatzTV.Application.Emby; using ErsatzTV.Application.Jellyfin; using ErsatzTV.Application.Plex; using ErsatzTV.Application.Streaming; +using ErsatzTV.Application.Subtitles.Queries; using ErsatzTV.Core; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Extensions; @@ -143,4 +144,23 @@ public class InternalController : ControllerBase return new RedirectResult(fullPath.ToString()); }); } + + [HttpGet("/media/subtitle/{id:int}")] + public async Task GetSubtitle(int id) + { + Either path = await _mediator.Send(new GetSubtitlePathById(id)); + return path.Match( + Left: _ => new NotFoundResult(), + Right: r => + { + string mimeType = Path.GetExtension(r).ToLowerInvariant() switch + { + "ass" or "ssa" => "text/x-ssa", + "vtt" => "text/vtt", + _ => "application/x-subrip", + }; + + return new PhysicalFileResult(r, mimeType); + }); + } }