using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Subtitles.Queries; public class GetSubtitlePathByIdHandler(IDbContextFactory dbContextFactory) : IRequestHandler> { public async Task> Handle( GetSubtitlePathById request, CancellationToken cancellationToken) { await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeSubtitle = await dbContext.Subtitles .AsNoTracking() .SelectOneAsync(s => s.Id, s => s.Id == request.Id, cancellationToken); foreach (var subtitle in maybeSubtitle) { if (subtitle is { SubtitleKind: SubtitleKind.Embedded, IsExtracted: true }) { string path = Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path); return new SubtitlePathAndCodec(path, subtitle.Codec); } foreach (string plexUrl in await GetPlexUrl(request.Id, dbContext, maybeSubtitle)) { return new SubtitlePathAndCodec(plexUrl, subtitle.Codec); } foreach (string jellyfinUrl in await GetJellyfinUrl(request.Id, dbContext, maybeSubtitle)) { return new SubtitlePathAndCodec(jellyfinUrl, subtitle.Codec); } foreach (string embyUrl in await GetEmbyUrl(request.Id, dbContext, maybeSubtitle)) { return new SubtitlePathAndCodec(embyUrl, subtitle.Codec); } return new SubtitlePathAndCodec(subtitle.Path, subtitle.Codec); } return BaseError.New($"Unable to locate subtitle with id {request.Id}"); } protected static async Task> GetPlexUrl( int subtitleId, TvContext dbContext, Option maybeSubtitle) { // check for plex episode Option maybePlexId = await dbContext.Connection.QuerySingleOrDefaultAsync( @"select PMS.Id from PlexMediaSource PMS inner join Library L on PMS.Id = L.MediaSourceId inner join LibraryPath LP on L.Id = LP.LibraryId inner join MediaItem MI on LP.Id = MI.LibraryPathId inner join EpisodeMetadata EM on EM.EpisodeId = MI.Id inner join Subtitle S on EM.Id = S.EpisodeMetadataId where S.Id = @SubtitleId", new { SubtitleId = subtitleId }) .Map(Optional); // check for plex movie if (maybePlexId.IsNone) { maybePlexId = await dbContext.Connection.QuerySingleOrDefaultAsync( @"select PMS.Id from PlexMediaSource PMS inner join Library L on PMS.Id = L.MediaSourceId inner join LibraryPath LP on L.Id = LP.LibraryId inner join MediaItem MI on LP.Id = MI.LibraryPathId inner join MovieMetadata MM on MM.MovieId = MI.Id inner join Subtitle S on MM.Id = S.MovieMetadataId where S.Id = @SubtitleId", new { SubtitleId = subtitleId }) .Map(Optional); } foreach (int plexMediaSourceId in maybePlexId) { foreach (string subtitlePath in maybeSubtitle.Map(s => s.Path)) { return $"http://localhost:{Settings.StreamingPort}/media/plex/{plexMediaSourceId}/{subtitlePath}"; } } return Option.None; } protected static async Task> GetJellyfinUrl( int subtitleId, TvContext dbContext, Option maybeSubtitle) { // check for jellyfin episode Option maybeJellyfinId = await dbContext.Connection.QuerySingleOrDefaultAsync( @"select JE.ItemId from JellyfinEpisode JE inner join EpisodeMetadata EM on EM.EpisodeId = JE.Id inner join Subtitle S on EM.Id = S.EpisodeMetadataId where S.Id = @SubtitleId", new { SubtitleId = subtitleId }) .Map(Optional); // check for jellyfin movie if (maybeJellyfinId.IsNone) { maybeJellyfinId = await dbContext.Connection.QuerySingleOrDefaultAsync( @"select JM.ItemId from JellyfinMovie JM inner join MovieMetadata MM on MM.MovieId = JM.Id inner join Subtitle S on MM.Id = S.MovieMetadataId where S.Id = @SubtitleId", new { SubtitleId = subtitleId }) .Map(Optional); } foreach (string jellyfinItemId in maybeJellyfinId) { foreach (Subtitle subtitle in maybeSubtitle) { int index = subtitle.StreamIndex - JellyfinStream.ExternalStreamOffset; string extension = Subtitle.ExtensionForCodec(subtitle.Codec); var subtitlePath = $"Videos/{jellyfinItemId}/{jellyfinItemId}/Subtitles/{index}/{index}/Stream.{extension}"; return $"http://localhost:{Settings.StreamingPort}/media/jellyfin/{subtitlePath}"; } } return Option.None; } protected static async Task> GetEmbyUrl( int subtitleId, TvContext dbContext, Option maybeSubtitle) { // check for emby episode Option maybeEmbyId = await dbContext.Connection.QuerySingleOrDefaultAsync( @"select EE.ItemId from EmbyEpisode EE inner join EpisodeMetadata EM on EM.EpisodeId = EE.Id inner join Subtitle S on EM.Id = S.EpisodeMetadataId where S.Id = @SubtitleId", new { SubtitleId = subtitleId }) .Map(Optional); // check for emby movie if (maybeEmbyId.IsNone) { maybeEmbyId = await dbContext.Connection.QuerySingleOrDefaultAsync( @"select EM.ItemId from EmbyMovie EM inner join MovieMetadata MM on MM.MovieId = EM.Id inner join Subtitle S on MM.Id = S.MovieMetadataId where S.Id = @SubtitleId", new { SubtitleId = subtitleId }) .Map(Optional); } foreach (string embyItemId in maybeEmbyId) { foreach (Subtitle subtitle in maybeSubtitle) { string extension = Subtitle.ExtensionForCodec(subtitle.Codec); var subtitlePath = $"Videos/{embyItemId}/{subtitle.Path}/Subtitles/{subtitle.StreamIndex}/Stream.{extension}"; return $"http://localhost:{Settings.StreamingPort}/media/emby/{subtitlePath}"; } } return Option.None; } }