📄 src/Infrastructure/Ffmpeg/SubtitleReader.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FFMpegCore;
using FFMpegCore.Pipes;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Slopper.Domain;
using SubtitlesParserV2;

namespace Slopper.Infrastructure.Ffmpeg;

internal sealed class SubtitleReader(ILogger<SubtitleReader> logger, IMemoryCache cache) : ISubtitleReader
{
    public async Task<IReadOnlyList<SubtitleEntry>> ReadSubtitles(
        MediaItem media,
        CancellationToken cancellationToken
    ) => await ReadSubtitlesInternal(media);

    public async Task<IReadOnlyList<SubtitleEntry>> ReadSubtitles(
        MediaItem media,
        TimeSpan start,
        TimeSpan duration,
        CancellationToken cancellationToken
    )
    {
        var all = await ReadSubtitlesInternal(media);
        var first = Array.FindIndex(all, s => start <= s.Start);
        var end = start + duration;
        var last = Array.FindIndex(all, s => end <= s.Start + s.Duration);
        if (last < first)
        {
            return [];
        }
        return new ArraySegment<SubtitleEntry>(all, first, last - first + 1);
    }

    private async Task<SubtitleEntry[]> ReadSubtitlesInternal(MediaItem media) =>
        (
            await cache.GetOrCreateAsync(
                media,
                async entry =>
                {
                    entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15);
                    return media.Subtitles switch
                    {
                        Subtitles.External(var path) => ReadSubtitles(path),
                        Subtitles.Embedded(var index) => await ReadSubtitles(media.Path, index),
                        _ => throw new ArgumentException("Unknown subtitle type"),
                    };
                }
            )
        )!;

    private static SubtitleEntry[] ReadSubtitles(string path)
    {
        using var stream = File.OpenRead(path);
        return ReadSubtitles(stream);
    }

    private async Task<SubtitleEntry[]> ReadSubtitles(string path, int index)
    {
        using var stream = new MemoryStream();
        var args = FFMpegArguments
            .FromFileInput(path)
            .OutputToPipe(
                new StreamPipeSink(stream),
                addArguments: options => options.SelectStream(index).ForceFormat("srt")
            );
        using (Tracing.StartReadSubtitles(path, index))
        {
            logger.LogInformation("Running ffmpeg {FfmpegArguments}", args.Arguments);
            await args.ProcessAsynchronously();
        }
        stream.Position = 0;
        return ReadSubtitles(stream);
    }

    private static SubtitleEntry[] ReadSubtitles(Stream stream)
    {
        var result = SubtitleParser.ParseStream(stream) ?? throw new Exception("Cannot parse subtitles.");
        return
        [
            .. result.Subtitles.Select(s => new SubtitleEntry(
                [.. s.Lines],
                TimeSpan.FromMilliseconds(s.StartTime),
                TimeSpan.FromMilliseconds(s.EndTime - s.StartTime)
            )),
        ];
    }
}