Browse Source

fix media server text subtitle extraction (#1246)

pull/1248/head
Jason Dove 3 years ago committed by GitHub
parent
commit
494142f026
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 67
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  3. 4416
      ErsatzTV.Infrastructure/Migrations/20230414193046_Clear_SubtitleIsExtracted_AllMediaServers.Designer.cs
  4. 55
      ErsatzTV.Infrastructure/Migrations/20230414193046_Clear_SubtitleIsExtracted_AllMediaServers.cs

1
CHANGELOG.md

@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix `Reset Playout` button to use worker thread instead of UI thread - Fix `Reset Playout` button to use worker thread instead of UI thread
- This fixes potential UI hangs and database concurrency bugs - This fixes potential UI hangs and database concurrency bugs
- Maintain watermark alpha channel (built-in transparency) using QSV acceleration - Maintain watermark alpha channel (built-in transparency) using QSV acceleration
- Properly extract and burn in embedded text subtitles using Jellyfin, Emby and Plex libraries
### Changed ### Changed
- Remove duplicate items from smart collections before scheduling - Remove duplicate items from smart collections before scheduling

67
ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs

@ -4,14 +4,12 @@ using System.Threading.Channels;
using CliWrap; using CliWrap;
using CliWrap.Buffered; using CliWrap.Buffered;
using CliWrap.Builders; using CliWrap.Builders;
using Dapper;
using ErsatzTV.Application.Maintenance; using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions; using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -22,27 +20,18 @@ namespace ErsatzTV.Application.Subtitles;
public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSubtitles, Either<BaseError, Unit>> public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSubtitles, Either<BaseError, Unit>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IEmbyPathReplacementService _embyPathReplacementService;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger; private readonly ILogger<ExtractEmbeddedSubtitlesHandler> _logger;
private readonly IPlexPathReplacementService _plexPathReplacementService;
public ExtractEmbeddedSubtitlesHandler( public ExtractEmbeddedSubtitlesHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService,
IEmbyPathReplacementService embyPathReplacementService,
ChannelWriter<IBackgroundServiceRequest> workerChannel, ChannelWriter<IBackgroundServiceRequest> workerChannel,
ILogger<ExtractEmbeddedSubtitlesHandler> logger) ILogger<ExtractEmbeddedSubtitlesHandler> logger)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_localFileSystem = localFileSystem; _localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService;
_embyPathReplacementService = embyPathReplacementService;
_workerChannel = workerChannel; _workerChannel = workerChannel;
_logger = logger; _logger = logger;
} }
@ -267,7 +256,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
continue; continue;
} }
string mediaItemPath = await GetMediaItemPath(mediaItem); string mediaItemPath = await GetMediaItemPath(dbContext, mediaItem);
ArgumentsBuilder args = new ArgumentsBuilder() ArgumentsBuilder args = new ArgumentsBuilder()
.Add("-nostdin") .Add("-nostdin")
@ -382,7 +371,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
continue; continue;
} }
string mediaItemPath = await GetMediaItemPath(mediaItem); string mediaItemPath = await GetMediaItemPath(dbContext, mediaItem);
var arguments = var arguments =
$"-nostdin -hide_banner -dump_attachment:t:{attachmentIndex} \"\" -i \"{mediaItemPath}\" -y"; $"-nostdin -hide_banner -dump_attachment:t:{attachmentIndex} \"\" -i \"{mediaItemPath}\" -y";
@ -423,7 +412,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
string nameWithExtension = subtitle.Codec switch string nameWithExtension = subtitle.Codec switch
{ {
"subrip" => $"{name}.srt", "subrip" or "srt" => $"{name}.srt",
"ass" => $"{name}.ass", "ass" => $"{name}.ass",
"webvtt" => $"{name}.vtt", "webvtt" => $"{name}.vtt",
"mov_text" => $"{name}.srt", "mov_text" => $"{name}.srt",
@ -438,33 +427,39 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
return Path.Combine(subfolder, subfolder2, nameWithExtension); return Path.Combine(subfolder, subfolder2, nameWithExtension);
} }
private async Task<string> GetMediaItemPath(MediaItem mediaItem) private static async Task<string> GetMediaItemPath(TvContext dbContext, MediaItem mediaItem)
{ {
MediaVersion version = mediaItem.GetHeadVersion(); MediaVersion version = mediaItem.GetHeadVersion();
MediaFile file = version.MediaFiles.Head(); MediaFile file = version.MediaFiles.Head();
string path = file.Path; switch (file)
{
case PlexMediaFile pmf:
Option<int> maybeId = await dbContext.Connection.QuerySingleOrDefaultAsync<int>(
@"SELECT PMS.Id FROM PlexMediaSource PMS
INNER JOIN Library L on PMS.Id = L.MediaSourceId
INNER JOIN LibraryPath LP on L.Id = LP.LibraryId
WHERE LP.Id = @LibraryPathId",
new { mediaItem.LibraryPathId })
.Map(Optional);
foreach (int plexMediaSourceId in maybeId)
{
return $"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{pmf.Key}";
}
break;
}
return mediaItem switch return mediaItem switch
{ {
PlexMovie plexMovie => await _plexPathReplacementService.GetReplacementPlexPath( JellyfinMovie jellyfinMovie =>
plexMovie.LibraryPathId, $"http://localhost:{Settings.ListenPort}/media/jellyfin/{jellyfinMovie.ItemId}",
path), JellyfinEpisode jellyfinEpisode =>
PlexEpisode plexEpisode => await _plexPathReplacementService.GetReplacementPlexPath( $"http://localhost:{Settings.ListenPort}/media/jellyfin/{jellyfinEpisode.ItemId}",
plexEpisode.LibraryPathId, EmbyMovie embyMovie => $"http://localhost:{Settings.ListenPort}/media/emby/{embyMovie.ItemId}",
path), EmbyEpisode embyEpisode => $"http://localhost:{Settings.ListenPort}/media/emby/{embyEpisode.ItemId}",
JellyfinMovie jellyfinMovie => await _jellyfinPathReplacementService.GetReplacementJellyfinPath( _ => file.Path
jellyfinMovie.LibraryPathId,
path),
JellyfinEpisode jellyfinEpisode => await _jellyfinPathReplacementService.GetReplacementJellyfinPath(
jellyfinEpisode.LibraryPathId,
path),
EmbyMovie embyMovie => await _embyPathReplacementService.GetReplacementEmbyPath(
embyMovie.LibraryPathId,
path),
EmbyEpisode embyEpisode => await _embyPathReplacementService.GetReplacementEmbyPath(
embyEpisode.LibraryPathId,
path),
_ => path
}; };
} }

4416
ErsatzTV.Infrastructure/Migrations/20230414193046_Clear_SubtitleIsExtracted_AllMediaServers.Designer.cs generated

File diff suppressed because it is too large Load Diff

55
ErsatzTV.Infrastructure/Migrations/20230414193046_Clear_SubtitleIsExtracted_AllMediaServers.cs

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class Clear_SubtitleIsExtracted_AllMediaServers : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"
update Subtitle set IsExtracted = 0 where Id in
(select Subtitle.Id
from Subtitle
inner join EpisodeMetadata EM on Subtitle.EpisodeMetadataId = EM.Id
inner join MediaItem MI on EM.EpisodeId = MI.Id
inner join LibraryPath LP on MI.LibraryPathId = LP.Id
inner join PlexLibrary PL on PL.Id = LP.LibraryId
where Subtitle.Codec = 'srt'
and IsExtracted = 1)");
migrationBuilder.Sql(
@"
update Subtitle set IsExtracted = 0 where Id in
(select Subtitle.Id
from Subtitle
inner join EpisodeMetadata EM on Subtitle.EpisodeMetadataId = EM.Id
inner join MediaItem MI on EM.EpisodeId = MI.Id
inner join LibraryPath LP on MI.LibraryPathId = LP.Id
inner join EmbyLibrary EL on EL.Id = LP.LibraryId
where Subtitle.Codec = 'srt'
and IsExtracted = 1)");
migrationBuilder.Sql(
@"
update Subtitle set IsExtracted = 0 where Id in
(select Subtitle.Id
from Subtitle
inner join EpisodeMetadata EM on Subtitle.EpisodeMetadataId = EM.Id
inner join MediaItem MI on EM.EpisodeId = MI.Id
inner join LibraryPath LP on MI.LibraryPathId = LP.Id
inner join JellyfinLibrary JL on JL.Id = LP.LibraryId
where Subtitle.Codec = 'srt'
and IsExtracted = 1)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
Loading…
Cancel
Save