Browse Source

fix subtitles from media server libraries (#1233)

* fix embedded subtitles from media servers

* fix plex external subtitles

* fix artwork bug, delete orphaned subtitles

* jellyfin subtitles work again

* emby subtitles work

* rescan all media server libraries
pull/1237/head
Jason Dove 2 years ago committed by GitHub
parent
commit
126304bb8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedSubtitles.cs
  3. 44
      ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedSubtitlesHandler.cs
  4. 4
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  5. 10
      ErsatzTV.Application/Subtitles/Commands/ExtractEmbeddedSubtitlesHandler.cs
  6. 152
      ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs
  7. 3
      ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs
  8. 16
      ErsatzTV.Core/Domain/Metadata/Subtitle.cs
  9. 2
      ErsatzTV.Core/Interfaces/Emby/IEmbyMovieLibraryScanner.cs
  10. 2
      ErsatzTV.Core/Interfaces/Emby/IEmbyTelevisionLibraryScanner.cs
  11. 2
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs
  12. 2
      ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs
  13. 2
      ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs
  14. 6
      ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs
  15. 2
      ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs
  16. 6
      ErsatzTV.Core/Jellyfin/JellyfinStream.cs
  17. 2
      ErsatzTV.FFmpeg/InputFile.cs
  18. 3
      ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs
  19. 2
      ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs
  20. 2
      ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs
  21. 2
      ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs
  22. 13
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  23. 18
      ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs
  24. 1
      ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs
  25. 27
      ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs
  26. 4416
      ErsatzTV.Infrastructure/Migrations/20230407181640_Rescan_AllMediaServerLibraries_Subtitles.Designer.cs
  27. 41
      ErsatzTV.Infrastructure/Migrations/20230407181640_Rescan_AllMediaServerLibraries_Subtitles.cs
  28. 2
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  29. 15
      ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs
  30. 93
      ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs
  31. 7
      ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs
  32. 4
      ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs
  33. 4
      ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs
  34. 9
      ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs
  35. 9
      ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs
  36. 9
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs
  37. 9
      ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs
  38. 46
      ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs
  39. 53
      ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs
  40. 27
      ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs
  41. 14
      ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs
  42. 48
      ErsatzTV/Controllers/InternalController.cs
  43. 4
      ErsatzTV/Services/SchedulerService.cs
  44. 3
      ErsatzTV/Services/WorkerService.cs

2
CHANGELOG.md

@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix decoding of MPEG-4 Part 2 (e.g. DivX) content using NVIDIA acceleration
- Fix color normalization from `bt470bg` to `bt709` using QSV acceleration
- Fix adding files to search index with unknown video codec
- Fix subtitle burn-in (embedded or external) using Jellyfin, Emby and Plex libraries
- **This requires a one-time full library scan, which may take a long time with large libraries.**
### Changed
- Use Poster artwork for XMLTV if available

5
ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedSubtitles.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Maintenance;
public record DeleteOrphanedSubtitles : IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest;

44
ErsatzTV.Application/Maintenance/Commands/DeleteOrphanedSubtitlesHandler.cs

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Maintenance;
public class DeleteOrphanedSubtitlesHandler : IRequestHandler<DeleteOrphanedSubtitles, Either<BaseError, Unit>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
public DeleteOrphanedSubtitlesHandler(IDbContextFactory<TvContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<Either<BaseError, Unit>> Handle(
DeleteOrphanedSubtitles request,
CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
IEnumerable<int> toDelete = await dbContext.Connection.QueryAsync<int>(
@"SELECT S.Id FROM Subtitle S
WHERE S.ArtistMetadataId IS NULL AND S.EpisodeMetadataId IS NULL
AND S.MovieMetadataId IS NULL AND S.MusicVideoMetadataId IS NULL
AND S.OtherVideoMetadataId IS NULL AND S.SeasonMetadataId IS NULL
AND S.ShowMetadataId IS NULL AND s.SongMetadataId IS NULL");
foreach (int id in toDelete)
{
await dbContext.Connection.ExecuteAsync("DELETE FROM Subtitle WHERE Id = @Id", new { Id = id });
}
return Unit.Default;
}
catch (Exception ex)
{
return BaseError.New(ex.Message);
}
}
}

4
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -302,8 +302,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -302,8 +302,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
if (isMediaServer)
{
// TODO: sidecar subtitles are currently unsupported since media servers no longer use direct filesystem access
allSubtitles.RemoveAll(s => s.SubtitleKind == SubtitleKind.Sidecar);
// closed captions are currently unsupported
allSubtitles.RemoveAll(s => s.Codec == "eia_608");
}
return allSubtitles;

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

@ -188,7 +188,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -188,7 +188,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
em => em.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "pgssub"))
.Map(em => em.EpisodeId)
.ToListAsync(cancellationToken);
result.AddRange(episodeIds);
@ -199,7 +199,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -199,7 +199,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "pgssub"))
.Map(mm => mm.MovieId)
.ToListAsync(cancellationToken);
result.AddRange(movieIds);
@ -210,7 +210,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -210,7 +210,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
mm => mm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "pgssub"))
.Map(mm => mm.MusicVideoId)
.ToListAsync(cancellationToken);
result.AddRange(musicVideoIds);
@ -221,7 +221,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -221,7 +221,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
.Filter(
ovm => ovm.Subtitles.Any(
s => s.SubtitleKind == SubtitleKind.Embedded &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle"))
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "pgssub"))
.Map(ovm => ovm.OtherVideoId)
.ToListAsync(cancellationToken);
result.AddRange(otherVideoIds);
@ -250,7 +250,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu @@ -250,7 +250,7 @@ public class ExtractEmbeddedSubtitlesHandler : IRequestHandler<ExtractEmbeddedSu
IEnumerable<Subtitle> subtitles = allSubtitles
.Filter(
s => s.SubtitleKind == SubtitleKind.Embedded && s.IsExtracted == false &&
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle");
s.Codec != "hdmv_pgs_subtitle" && s.Codec != "dvd_subtitle" && s.Codec != "dvdsub" && s.Codec != "pgssub");
// find cache paths for each subtitle
foreach (Subtitle subtitle in subtitles)

152
ErsatzTV.Application/Subtitles/Queries/GetSubtitlePathByIdHandler.cs

@ -1,4 +1,7 @@ @@ -1,4 +1,7 @@
using Dapper;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
@ -19,9 +22,150 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E @@ -19,9 +22,150 @@ public class GetSubtitlePathByIdHandler : IRequestHandler<GetSubtitlePathById, E
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<string> 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}"));
Option<Subtitle> maybeSubtitle = await dbContext.Subtitles
.SelectOneAsync(s => s.Id, s => s.Id == request.Id);
foreach (string plexUrl in await GetPlexUrl(request, dbContext, maybeSubtitle))
{
return plexUrl;
}
foreach (string jellyfinUrl in await GetJellyfinUrl(request, dbContext, maybeSubtitle))
{
return jellyfinUrl;
}
foreach (string embyUrl in await GetEmbyUrl(request, dbContext, maybeSubtitle))
{
return embyUrl;
}
return maybeSubtitle
.Map(s => s.Path)
.ToEither(BaseError.New($"Unable to locate subtitle with id {request.Id}"));
}
private static async Task<Option<string>> GetPlexUrl(
GetSubtitlePathById request,
TvContext dbContext,
Option<Subtitle> maybeSubtitle)
{
// check for plex episode
Option<int> maybePlexId = 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
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 = request.Id })
.Map(Optional);
// check for plex movie
if (maybePlexId.IsNone)
{
maybePlexId = 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
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 = request.Id })
.Map(Optional);
}
foreach (int plexMediaSourceId in maybePlexId)
{
foreach (string subtitlePath in maybeSubtitle.Map(s => s.Path))
{
return $"http://localhost:{Settings.ListenPort}/media/plex/{plexMediaSourceId}/{subtitlePath}";
}
}
return Option<string>.None;
}
private static async Task<Option<string>> GetJellyfinUrl(
GetSubtitlePathById request,
TvContext dbContext,
Option<Subtitle> maybeSubtitle)
{
// check for jellyfin episode
Option<string> maybeJellyfinId = await dbContext.Connection.QuerySingleOrDefaultAsync<string>(
@"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 = request.Id })
.Map(Optional);
// check for jellyfin movie
if (maybeJellyfinId.IsNone)
{
maybeJellyfinId = await dbContext.Connection.QuerySingleOrDefaultAsync<string>(
@"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 = request.Id })
.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.ListenPort}/media/jellyfin/{subtitlePath}";
}
}
return Option<string>.None;
}
private static async Task<Option<string>> GetEmbyUrl(
GetSubtitlePathById request,
TvContext dbContext,
Option<Subtitle> maybeSubtitle)
{
// check for emby episode
Option<string> maybeEmbyId = await dbContext.Connection.QuerySingleOrDefaultAsync<string>(
@"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 = request.Id })
.Map(Optional);
// check for emby movie
if (maybeEmbyId.IsNone)
{
maybeEmbyId = await dbContext.Connection.QuerySingleOrDefaultAsync<string>(
@"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 = request.Id })
.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.ListenPort}/media/emby/{subtitlePath}";
}
}
return Option<string>.None;
}
}

3
ErsatzTV.Core/Domain/MediaItem/MediaStreamKind.cs

@ -5,5 +5,6 @@ public enum MediaStreamKind @@ -5,5 +5,6 @@ public enum MediaStreamKind
Video = 1,
Audio = 2,
Subtitle = 3,
Attachment = 4
Attachment = 4,
ExternalSubtitle = 5
}

16
ErsatzTV.Core/Domain/Metadata/Subtitle.cs

@ -15,7 +15,7 @@ public class Subtitle @@ -15,7 +15,7 @@ public class Subtitle
public string Path { get; set; }
public DateTime DateAdded { get; set; }
public DateTime DateUpdated { get; set; }
public bool IsImage => Codec is "hdmv_pgs_subtitle" or "dvd_subtitle";
public bool IsImage => Codec is "hdmv_pgs_subtitle" or "dvd_subtitle" or "dvdsub" or "pgssub";
public static Subtitle FromMediaStream(MediaStream stream) =>
new()
@ -26,8 +26,20 @@ public class Subtitle @@ -26,8 +26,20 @@ public class Subtitle
Forced = stream.Forced,
Language = stream.Language,
StreamIndex = stream.Index,
SubtitleKind = SubtitleKind.Embedded,
Path = !string.IsNullOrWhiteSpace(stream.FileName) ? stream.FileName : null,
SubtitleKind = stream.MediaStreamKind == MediaStreamKind.ExternalSubtitle
? SubtitleKind.Sidecar
: SubtitleKind.Embedded,
DateAdded = DateTime.UtcNow,
DateUpdated = DateTime.UtcNow
};
public static string ExtensionForCodec(string codec) => codec switch
{
"subrip" or "srt" => "srt",
"ass" => "ass",
"webvtt" => "vtt",
"mov_text" => "srt",
_ => string.Empty
};
}

2
ErsatzTV.Core/Interfaces/Emby/IEmbyMovieLibraryScanner.cs

@ -8,8 +8,6 @@ public interface IEmbyMovieLibraryScanner @@ -8,8 +8,6 @@ public interface IEmbyMovieLibraryScanner
string address,
string apiKey,
EmbyLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken);
}

2
ErsatzTV.Core/Interfaces/Emby/IEmbyTelevisionLibraryScanner.cs

@ -8,8 +8,6 @@ public interface IEmbyTelevisionLibraryScanner @@ -8,8 +8,6 @@ public interface IEmbyTelevisionLibraryScanner
string address,
string apiKey,
EmbyLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken);
}

2
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinMovieLibraryScanner.cs

@ -8,8 +8,6 @@ public interface IJellyfinMovieLibraryScanner @@ -8,8 +8,6 @@ public interface IJellyfinMovieLibraryScanner
string address,
string apiKey,
JellyfinLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken);
}

2
ErsatzTV.Core/Interfaces/Jellyfin/IJellyfinTelevisionLibraryScanner.cs

@ -8,8 +8,6 @@ public interface IJellyfinTelevisionLibraryScanner @@ -8,8 +8,6 @@ public interface IJellyfinTelevisionLibraryScanner
string address,
string apiKey,
JellyfinLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken);
}

2
ErsatzTV.Core/Interfaces/Plex/IPlexMovieLibraryScanner.cs

@ -9,8 +9,6 @@ public interface IPlexMovieLibraryScanner @@ -9,8 +9,6 @@ public interface IPlexMovieLibraryScanner
PlexConnection connection,
PlexServerAuthToken token,
PlexLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken);
}

6
ErsatzTV.Core/Interfaces/Plex/IPlexServerApiClient.cs

@ -45,12 +45,6 @@ public interface IPlexServerApiClient @@ -45,12 +45,6 @@ public interface IPlexServerApiClient
PlexConnection connection,
PlexServerAuthToken token);
Task<Either<BaseError, MovieMetadata>> GetMovieMetadata(
PlexLibrary library,
string key,
PlexConnection connection,
PlexServerAuthToken token);
Task<Either<BaseError, ShowMetadata>> GetShowMetadata(
PlexLibrary library,
string key,

2
ErsatzTV.Core/Interfaces/Plex/IPlexTelevisionLibraryScanner.cs

@ -9,8 +9,6 @@ public interface IPlexTelevisionLibraryScanner @@ -9,8 +9,6 @@ public interface IPlexTelevisionLibraryScanner
PlexConnection connection,
PlexServerAuthToken token,
PlexLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken);
}

6
ErsatzTV.Core/Jellyfin/JellyfinStream.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.Core.Jellyfin;
public static class JellyfinStream
{
public static readonly int ExternalStreamOffset = 100_000;
}

2
ErsatzTV.FFmpeg/InputFile.cs

@ -80,5 +80,5 @@ public record SubtitleInputFile(string Path, IList<MediaStream> SubtitleStreams, @@ -80,5 +80,5 @@ public record SubtitleInputFile(string Path, IList<MediaStream> SubtitleStreams,
Path,
SubtitleStreams)
{
public bool IsImageBased = SubtitleStreams.All(s => s.Codec is "hdmv_pgs_subtitle" or "dvd_subtitle");
public bool IsImageBased = SubtitleStreams.All(s => s.Codec is "hdmv_pgs_subtitle" or "dvd_subtitle" or "dvdsub" or "pgssub");
}

3
ErsatzTV.Infrastructure/Data/Repositories/ArtworkRepository.cs

@ -17,9 +17,10 @@ public class ArtworkRepository : IArtworkRepository @@ -17,9 +17,10 @@ public class ArtworkRepository : IArtworkRepository
return await dbContext.Connection.QueryAsync<Artwork>(
@"SELECT A.Id, A.Path FROM Artwork A
WHERE A.ArtistMetadataId IS NULL AND A.EpisodeMetadataId IS NULL
AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL
AND A.MovieMetadataId IS NULL AND A.MusicVideoMetadataId IS NULL
AND A.SeasonMetadataId IS NULL AND A.ShowMetadataId IS NULL
AND A.SongMetadataId IS NULL AND A.ChannelId IS NULL
AND A.OtherVideoMetadataId IS NULL
AND NOT EXISTS (SELECT * FROM Actor WHERE Actor.ArtworkId = A.Id)")
.Map(result => result.ToList());
}

2
ErsatzTV.Infrastructure/Data/Repositories/EmbyMovieRepository.cs

@ -374,7 +374,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository @@ -374,7 +374,7 @@ public class EmbyMovieRepository : IEmbyMovieRepository
fanArt.DateAdded = incomingFanArt.DateAdded;
fanArt.DateUpdated = incomingFanArt.DateUpdated;
}
// version
MediaVersion version = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();

2
ErsatzTV.Infrastructure/Data/Repositories/EmbyTelevisionRepository.cs

@ -728,7 +728,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository @@ -728,7 +728,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
{
metadata.Artwork.Remove(artworkToRemove);
}
// version
MediaVersion version = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();

2
ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs

@ -731,7 +731,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository @@ -731,7 +731,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
{
metadata.Artwork.Remove(artworkToRemove);
}
// version
MediaVersion version = existing.MediaVersions.Head();
MediaVersion incomingVersion = incoming.MediaVersions.Head();

13
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -5,6 +5,7 @@ using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Serilog;
namespace ErsatzTV.Infrastructure.Data.Repositories;
@ -488,6 +489,12 @@ public class MetadataRepository : IMetadataRepository @@ -488,6 +489,12 @@ public class MetadataRepository : IMetadataRepository
}
public async Task<bool> UpdateSubtitles(Metadata metadata, List<Subtitle> subtitles)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
return await UpdateSubtitles(dbContext, metadata, subtitles);
}
private static async Task<bool> UpdateSubtitles(TvContext dbContext, Metadata metadata, List<Subtitle> subtitles)
{
// _logger.LogDebug(
// "Updating {Count} subtitles; metadata is {Metadata}",
@ -496,8 +503,6 @@ public class MetadataRepository : IMetadataRepository @@ -496,8 +503,6 @@ public class MetadataRepository : IMetadataRepository
int metadataId = metadata.Id;
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
Option<Metadata> maybeMetadata = metadata switch
{
EpisodeMetadata => await dbContext.EpisodeMetadata
@ -548,7 +553,8 @@ public class MetadataRepository : IMetadataRepository @@ -548,7 +553,8 @@ public class MetadataRepository : IMetadataRepository
foreach (Subtitle incomingSubtitle in toUpdate)
{
Subtitle existingSubtitle =
existing.Subtitles.First(s => s.StreamIndex == incomingSubtitle.StreamIndex);
existing.Subtitles.First(
s => s.StreamIndex == incomingSubtitle.StreamIndex);
existingSubtitle.Codec = incomingSubtitle.Codec;
existingSubtitle.Default = incomingSubtitle.Default;
@ -557,6 +563,7 @@ public class MetadataRepository : IMetadataRepository @@ -557,6 +563,7 @@ public class MetadataRepository : IMetadataRepository
existingSubtitle.Language = incomingSubtitle.Language;
existingSubtitle.SubtitleKind = incomingSubtitle.SubtitleKind;
existingSubtitle.DateUpdated = incomingSubtitle.DateUpdated;
existingSubtitle.Path = incomingSubtitle.Path;
dbContext.Entry(existingSubtitle).State = EntityState.Modified;
}

18
ErsatzTV.Infrastructure/Emby/EmbyApiClient.cs

@ -431,7 +431,8 @@ public class EmbyApiClient : IEmbyApiClient @@ -431,7 +431,8 @@ public class EmbyApiClient : IEmbyApiClient
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
Guids = GuidsFromProviderIds(item.ProviderIds),
Subtitles = new List<Subtitle>()
};
// set order on actors
@ -747,7 +748,8 @@ public class EmbyApiClient : IEmbyApiClient @@ -747,7 +748,8 @@ public class EmbyApiClient : IEmbyApiClient
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds),
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
Subtitles = new List<Subtitle>()
};
if (item.IndexNumber.HasValue)
@ -900,14 +902,22 @@ public class EmbyApiClient : IEmbyApiClient @@ -900,14 +902,22 @@ public class EmbyApiClient : IEmbyApiClient
var stream = new MediaStream
{
MediaVersionId = version.Id,
MediaStreamKind = MediaStreamKind.Subtitle,
MediaStreamKind = subtitleStream.IsExternal == true
? MediaStreamKind.ExternalSubtitle
: MediaStreamKind.Subtitle,
Index = subtitleStream.Index,
Codec = subtitleStream.Codec,
Codec = (subtitleStream.Codec ?? string.Empty).ToLowerInvariant(),
Default = subtitleStream.IsDefault,
Forced = subtitleStream.IsForced,
Language = subtitleStream.Language
};
// hacky, oh well
if (subtitleStream.IsExternal == true)
{
stream.FileName = mediaSource.Id;
}
version.Streams.Add(stream);
}

1
ErsatzTV.Infrastructure/Emby/Models/EmbyMediaStreamResponse.cs

@ -15,6 +15,7 @@ public class EmbyMediaStreamResponse @@ -15,6 +15,7 @@ public class EmbyMediaStreamResponse
public string AspectRatio { get; set; }
public int? Channels { get; set; }
public bool? IsAnamorphic { get; set; }
public bool? IsExternal { get; set; }
public string DisplayTitle { get; set; }
public string PixelFormat { get; set; }
public string ColorRange { get; set; }

27
ErsatzTV.Infrastructure/Jellyfin/JellyfinApiClient.cs

@ -494,7 +494,8 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -494,7 +494,8 @@ public class JellyfinApiClient : IJellyfinApiClient
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds)
Guids = GuidsFromProviderIds(item.ProviderIds),
Subtitles = new List<Subtitle>()
};
// set order on actors
@ -834,7 +835,8 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -834,7 +835,8 @@ public class JellyfinApiClient : IJellyfinApiClient
Artwork = new List<Artwork>(),
Guids = GuidsFromProviderIds(item.ProviderIds),
Directors = Optional(item.People).Flatten().Collect(r => ProjectToDirector(r)).ToList(),
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList()
Writers = Optional(item.People).Flatten().Collect(r => ProjectToWriter(r)).ToList(),
Subtitles = new List<Subtitle>()
};
if (item.IndexNumber.HasValue)
@ -904,9 +906,8 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -904,9 +906,8 @@ public class JellyfinApiClient : IJellyfinApiClient
.OrderByDescending(i => i)
.FirstOrDefault();
IList<JellyfinMediaStreamResponse> streams = mediaSource.MediaStreams
.Filter(s => s.IsExternal == false)
.ToList();
IList<JellyfinMediaStreamResponse> streams = mediaSource.MediaStreams;
Option<JellyfinMediaStreamResponse> maybeVideoStream =
streams.Find(s => s.Type == JellyfinMediaStreamType.Video);
return maybeVideoStream.Map(
@ -998,14 +999,24 @@ public class JellyfinApiClient : IJellyfinApiClient @@ -998,14 +999,24 @@ public class JellyfinApiClient : IJellyfinApiClient
var stream = new MediaStream
{
MediaVersionId = version.Id,
MediaStreamKind = MediaStreamKind.Subtitle,
Index = subtitleStream.Index - streamIndexOffset,
Codec = subtitleStream.Codec,
Codec = (subtitleStream.Codec ?? string.Empty).ToLowerInvariant(),
Default = subtitleStream.IsDefault,
Forced = subtitleStream.IsForced,
Language = subtitleStream.Language
};
if (subtitleStream.IsExternal)
{
stream.MediaStreamKind = MediaStreamKind.ExternalSubtitle;
// ensure these don't collide with real indexes from the source file
stream.Index = subtitleStream.Index + JellyfinStream.ExternalStreamOffset;
}
else
{
stream.MediaStreamKind = MediaStreamKind.Subtitle;
stream.Index = subtitleStream.Index - streamIndexOffset;
}
version.Streams.Add(stream);
}

4416
ErsatzTV.Infrastructure/Migrations/20230407181640_Rescan_AllMediaServerLibraries_Subtitles.Designer.cs generated

File diff suppressed because it is too large Load Diff

41
ErsatzTV.Infrastructure/Migrations/20230407181640_Rescan_AllMediaServerLibraries_Subtitles.cs

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
/// <inheritdoc />
public partial class Rescan_AllMediaServerLibraries_Subtitles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE Id IN (SELECT Id FROM EmbyLibrary)");
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE Id IN (SELECT Id FROM JellyfinLibrary)");
migrationBuilder.Sql("UPDATE PlexMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE PlexShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE PlexSeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE PlexEpisode SET Etag = NULL");
migrationBuilder.Sql(
@"UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE Id IN (SELECT Id FROM PlexLibrary)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

2
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.HasAnnotation("ProductVersion", "7.0.4");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{

15
ErsatzTV.Infrastructure/Plex/Models/PlexStreamResponse.cs

@ -7,8 +7,18 @@ public class PlexStreamResponse @@ -7,8 +7,18 @@ public class PlexStreamResponse
[XmlAttribute("id")]
public int Id { get; set; }
[XmlIgnore]
public int? Index { get; set; }
[XmlAttribute("index")]
public int Index { get; set; }
public string IndexString
{
get => Index.HasValue ? Index.Value.ToString() : string.Empty;
set => Index = !string.IsNullOrEmpty(value) ? int.Parse(value) : null;
}
[XmlAttribute("key")]
public string Key { get; set; }
[XmlAttribute("default")]
public bool Default { get; set; }
@ -16,6 +26,9 @@ public class PlexStreamResponse @@ -16,6 +26,9 @@ public class PlexStreamResponse
[XmlAttribute("forced")]
public bool Forced { get; set; }
[XmlAttribute("embeddedInVideo")]
public bool EmbeddedInVideo { get; set; }
[XmlAttribute("languageCode")]
public string LanguageCode { get; set; }

93
ErsatzTV.Infrastructure/Plex/PlexServerApiClient.cs

@ -186,28 +186,6 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -186,28 +186,6 @@ public class PlexServerApiClient : IPlexServerApiClient
return GetPagedLibraryContents(connection, CountItems, GetItems);
}
// this shouldn't be called anymore
public async Task<Either<BaseError, MovieMetadata>> GetMovieMetadata(
PlexLibrary library,
string key,
PlexConnection connection,
PlexServerAuthToken token)
{
try
{
IPlexServerApi service = XmlServiceFor(connection.Uri);
return await service.GetVideoMetadata(key, token.AuthToken)
.Map(Optional)
.Map(r => r.Filter(m => m.Metadata.Media.Count > 0 && m.Metadata.Media.Any(media => media.Part.Count > 0)))
.MapT(response => ProjectToMovieMetadata(response.Metadata, library.MediaSourceId))
.Map(o => o.ToEither<BaseError>("Unable to locate metadata"));
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
public async Task<Either<BaseError, ShowMetadata>> GetShowMetadata(
PlexLibrary library,
string key,
@ -246,7 +224,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -246,7 +224,7 @@ public class PlexServerApiClient : IPlexServerApiClient
{
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
return maybeVersion.Match<Either<BaseError, Tuple<MovieMetadata, MediaVersion>>>(
version => Tuple(ProjectToMovieMetadata(response.Metadata, library.MediaSourceId), version),
version => Tuple(ProjectToMovieMetadata(version, response.Metadata, library.MediaSourceId), version),
() => BaseError.New("Unable to locate metadata"));
},
() => BaseError.New("Unable to locate metadata"));
@ -276,7 +254,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -276,7 +254,7 @@ public class PlexServerApiClient : IPlexServerApiClient
Option<MediaVersion> maybeVersion = ProjectToMediaVersion(response.Metadata);
return maybeVersion.Match<Either<BaseError, Tuple<EpisodeMetadata, MediaVersion>>>(
version => Tuple(
ProjectToEpisodeMetadata(response.Metadata, library.MediaSourceId),
ProjectToEpisodeMetadata(version, response.Metadata, library.MediaSourceId),
version),
() => BaseError.New("Unable to locate metadata"));
},
@ -410,8 +388,6 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -410,8 +388,6 @@ public class PlexServerApiClient : IPlexServerApiClient
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
MovieMetadata metadata = ProjectToMovieMetadata(response, mediaSourceId);
var version = new MediaVersion
{
Name = "Main",
@ -432,6 +408,8 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -432,6 +408,8 @@ public class PlexServerApiClient : IPlexServerApiClient
},
Streams = new List<MediaStream>()
};
MovieMetadata metadata = ProjectToMovieMetadata(version, response, mediaSourceId);
var movie = new PlexMovie
{
@ -445,7 +423,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -445,7 +423,7 @@ public class PlexServerApiClient : IPlexServerApiClient
return movie;
}
private MovieMetadata ProjectToMovieMetadata(PlexMetadataResponse response, int mediaSourceId)
private MovieMetadata ProjectToMovieMetadata(MediaVersion version, PlexMetadataResponse response, int mediaSourceId)
{
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@ -467,8 +445,15 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -467,8 +445,15 @@ public class PlexServerApiClient : IPlexServerApiClient
Actors = Optional(response.Role).Flatten().Map(r => ProjectToModel(r, dateAdded, lastWriteTime))
.ToList(),
Directors = Optional(response.Director).Flatten().Map(d => new Director { Name = d.Tag }).ToList(),
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList()
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList(),
Subtitles = new List<Subtitle>()
};
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle)
.ToList();
metadata.Subtitles.AddRange(subtitleStreams.Map(Subtitle.FromMediaStream));
if (response is PlexXmlMetadataResponse xml)
{
@ -582,7 +567,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -582,7 +567,7 @@ public class PlexServerApiClient : IPlexServerApiClient
{
MediaVersionId = version.Id,
MediaStreamKind = MediaStreamKind.Video,
Index = videoStream.Index,
Index = videoStream.Index!.Value,
Codec = videoStream.Codec,
Profile = (videoStream.Profile ?? string.Empty).ToLowerInvariant(),
Default = videoStream.Default,
@ -594,14 +579,14 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -594,14 +579,14 @@ public class PlexServerApiClient : IPlexServerApiClient
ColorTransfer = (videoStream.ColorTrc ?? string.Empty).ToLowerInvariant(),
ColorPrimaries = (videoStream.ColorPrimaries ?? string.Empty).ToLowerInvariant()
});
foreach (PlexStreamResponse audioStream in streams.Filter(s => s.StreamType == 2))
foreach (PlexStreamResponse audioStream in streams.Filter(s => s.StreamType == 2 && s.Index.HasValue))
{
var stream = new MediaStream
{
MediaVersionId = version.Id,
MediaStreamKind = MediaStreamKind.Audio,
Index = audioStream.Index,
Index = audioStream.Index.Value,
Codec = audioStream.Codec,
Profile = (audioStream.Profile ?? string.Empty).ToLowerInvariant(),
Channels = audioStream.Channels,
@ -610,17 +595,41 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -610,17 +595,41 @@ public class PlexServerApiClient : IPlexServerApiClient
Language = audioStream.LanguageCode,
Title = audioStream.Title ?? string.Empty
};
version.Streams.Add(stream);
}
foreach (PlexStreamResponse subtitleStream in streams.Filter(s => s.StreamType == 3))
// filter to embedded subtitles, but ignore "embedded in video" closed-caption streams
foreach (PlexStreamResponse subtitleStream in
streams.Filter(s => s.StreamType == 3 && s.Index.HasValue && !s.EmbeddedInVideo))
{
var stream = new MediaStream
{
MediaVersionId = version.Id,
MediaStreamKind = MediaStreamKind.Subtitle,
Index = subtitleStream.Index,
Index = subtitleStream.Index.Value,
Codec = subtitleStream.Codec,
Default = subtitleStream.Default,
Forced = subtitleStream.Forced,
Language = subtitleStream.LanguageCode
};
version.Streams.Add(stream);
}
// also include external subtitles
foreach (PlexStreamResponse subtitleStream in
streams.Filter(s => s.StreamType == 3 && !s.Index.HasValue && !string.IsNullOrWhiteSpace(s.Key)))
{
var stream = new MediaStream
{
MediaVersionId = version.Id,
MediaStreamKind = MediaStreamKind.ExternalSubtitle,
// hacky? maybe...
FileName = subtitleStream.Key,
Index = subtitleStream.Id,
Codec = subtitleStream.Codec,
Default = subtitleStream.Default,
Forced = subtitleStream.Forced,
@ -831,7 +840,6 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -831,7 +840,6 @@ public class PlexServerApiClient : IPlexServerApiClient
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
EpisodeMetadata metadata = ProjectToEpisodeMetadata(response, mediaSourceId);
var version = new MediaVersion
{
Name = "Main",
@ -852,6 +860,8 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -852,6 +860,8 @@ public class PlexServerApiClient : IPlexServerApiClient
// specifically omit stream details
Streams = new List<MediaStream>()
};
EpisodeMetadata metadata = ProjectToEpisodeMetadata(version, response, mediaSourceId);
var episode = new PlexEpisode
{
@ -865,7 +875,7 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -865,7 +875,7 @@ public class PlexServerApiClient : IPlexServerApiClient
return episode;
}
private EpisodeMetadata ProjectToEpisodeMetadata(PlexMetadataResponse response, int mediaSourceId)
private EpisodeMetadata ProjectToEpisodeMetadata(MediaVersion version, PlexMetadataResponse response, int mediaSourceId)
{
DateTime dateAdded = DateTimeOffset.FromUnixTimeSeconds(response.AddedAt).DateTime;
DateTime lastWriteTime = DateTimeOffset.FromUnixTimeSeconds(response.UpdatedAt).DateTime;
@ -885,9 +895,16 @@ public class PlexServerApiClient : IPlexServerApiClient @@ -885,9 +895,16 @@ public class PlexServerApiClient : IPlexServerApiClient
.ToList(),
Directors = Optional(response.Director).Flatten().Map(d => new Director { Name = d.Tag }).ToList(),
Writers = Optional(response.Writer).Flatten().Map(w => new Writer { Name = w.Tag }).ToList(),
Tags = new List<Tag>()
Tags = new List<Tag>(),
Subtitles = new List<Subtitle>()
};
var subtitleStreams = version.Streams
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle)
.ToList();
metadata.Subtitles.AddRange(subtitleStreams.Map(Subtitle.FromMediaStream));
if (response is PlexXmlMetadataResponse xml)
{
metadata.Guids = Optional(xml.Guid).Flatten().Map(g => new MetadataGuid { Guid = g.Id }).ToList();

7
ErsatzTV.Scanner/Application/Emby/Commands/SynchronizeEmbyLibraryByIdHandler.cs

@ -18,12 +18,10 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -18,12 +18,10 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
private readonly ILibraryRepository _libraryRepository;
private readonly ILogger<SynchronizeEmbyLibraryByIdHandler> _logger;
private readonly IEmbyApiClient _embyApiClient;
private readonly IMediator _mediator;
private readonly IMediaSourceRepository _mediaSourceRepository;
public SynchronizeEmbyLibraryByIdHandler(
IEmbyApiClient embyApiClient,
IMediator mediator,
IMediaSourceRepository mediaSourceRepository,
IEmbySecretStore embySecretStore,
@ -33,7 +31,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -33,7 +31,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
IConfigElementRepository configElementRepository,
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
{
_embyApiClient = embyApiClient;
_mediator = mediator;
_mediaSourceRepository = mediaSourceRepository;
_embySecretStore = embySecretStore;
@ -68,8 +65,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -68,8 +65,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
parameters.DeepScan,
cancellationToken),
LibraryMediaKind.Shows =>
@ -77,8 +72,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby @@ -77,8 +72,6 @@ public class SynchronizeEmbyLibraryByIdHandler : IRequestHandler<SynchronizeEmby
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
parameters.DeepScan,
cancellationToken),
_ => Unit.Default

4
ErsatzTV.Scanner/Application/Jellyfin/Commands/SynchronizeJellyfinLibraryByIdHandler.cs

@ -80,8 +80,6 @@ public class @@ -80,8 +80,6 @@ public class
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
parameters.DeepScan,
cancellationToken),
LibraryMediaKind.Shows =>
@ -89,8 +87,6 @@ public class @@ -89,8 +87,6 @@ public class
parameters.ConnectionParameters.ActiveConnection.Address,
parameters.ConnectionParameters.ApiKey,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
parameters.DeepScan,
cancellationToken),
_ => Unit.Default

4
ErsatzTV.Scanner/Application/Plex/Commands/SynchronizePlexLibraryByIdHandler.cs

@ -64,8 +64,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex @@ -64,8 +64,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
parameters.DeepScan,
cancellationToken),
LibraryMediaKind.Shows =>
@ -73,8 +71,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex @@ -73,8 +71,6 @@ public class SynchronizePlexLibraryByIdHandler : IRequestHandler<SynchronizePlex
parameters.ConnectionParameters.ActiveConnection,
parameters.ConnectionParameters.PlexServerAuthToken,
parameters.Library,
parameters.FFmpegPath,
parameters.FFprobePath,
parameters.DeepScan,
cancellationToken),
_ => Unit.Default

9
ErsatzTV.Scanner/Core/Emby/EmbyMovieLibraryScanner.cs

@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;
@ -30,12 +29,8 @@ public class EmbyMovieLibraryScanner : @@ -30,12 +29,8 @@ public class EmbyMovieLibraryScanner :
IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
IMetadataRepository metadataRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<EmbyMovieLibraryScanner> logger)
: base(
localStatisticsProvider,
localSubtitlesProvider,
localFileSystem,
metadataRepository,
mediator,
@ -54,8 +49,6 @@ public class EmbyMovieLibraryScanner : @@ -54,8 +49,6 @@ public class EmbyMovieLibraryScanner :
string address,
string apiKey,
EmbyLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -75,8 +68,6 @@ public class EmbyMovieLibraryScanner : @@ -75,8 +68,6 @@ public class EmbyMovieLibraryScanner :
new EmbyConnectionParameters(address, apiKey),
library,
GetLocalPath,
ffmpegPath,
ffprobePath,
deepScan,
cancellationToken);
}

9
ErsatzTV.Scanner/Core/Emby/EmbyTelevisionLibraryScanner.cs

@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Emby; @@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;
@ -29,13 +28,9 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -29,13 +28,9 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
IEmbyPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
IMetadataRepository metadataRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMediator mediator,
ILogger<EmbyTelevisionLibraryScanner> logger)
: base(
localStatisticsProvider,
localSubtitlesProvider,
localFileSystem,
metadataRepository,
mediator,
@ -54,8 +49,6 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -54,8 +49,6 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
string address,
string apiKey,
EmbyLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -75,8 +68,6 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner< @@ -75,8 +68,6 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
new EmbyConnectionParameters(address, apiKey),
library,
GetLocalPath,
ffmpegPath,
ffprobePath,
deepScan,
cancellationToken);
}

9
ErsatzTV.Scanner/Core/Jellyfin/JellyfinMovieLibraryScanner.cs

@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;
@ -30,12 +29,8 @@ public class JellyfinMovieLibraryScanner : @@ -30,12 +29,8 @@ public class JellyfinMovieLibraryScanner :
IMediaSourceRepository mediaSourceRepository,
ILocalFileSystem localFileSystem,
IMetadataRepository metadataRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<JellyfinMovieLibraryScanner> logger)
: base(
localStatisticsProvider,
localSubtitlesProvider,
localFileSystem,
metadataRepository,
mediator,
@ -54,8 +49,6 @@ public class JellyfinMovieLibraryScanner : @@ -54,8 +49,6 @@ public class JellyfinMovieLibraryScanner :
string address,
string apiKey,
JellyfinLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -75,8 +68,6 @@ public class JellyfinMovieLibraryScanner : @@ -75,8 +68,6 @@ public class JellyfinMovieLibraryScanner :
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
library,
GetLocalPath,
ffmpegPath,
ffprobePath,
deepScan,
cancellationToken);
}

9
ErsatzTV.Scanner/Core/Jellyfin/JellyfinTelevisionLibraryScanner.cs

@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Metadata; @@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;
@ -30,13 +29,9 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -30,13 +29,9 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
IJellyfinPathReplacementService pathReplacementService,
ILocalFileSystem localFileSystem,
IMetadataRepository metadataRepository,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
IMediator mediator,
ILogger<JellyfinTelevisionLibraryScanner> logger)
: base(
localStatisticsProvider,
localSubtitlesProvider,
localFileSystem,
metadataRepository,
mediator,
@ -55,8 +50,6 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -55,8 +50,6 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
string address,
string apiKey,
JellyfinLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -76,8 +69,6 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan @@ -76,8 +69,6 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
library,
GetLocalPath,
ffmpegPath,
ffprobePath,
deepScan,
cancellationToken);
}

46
ErsatzTV.Scanner/Core/Metadata/MediaServerMovieLibraryScanner.cs

@ -3,11 +3,11 @@ using ErsatzTV.Core; @@ -3,11 +3,11 @@ using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.MediaSources;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Scanner.Core.Metadata;
@ -20,21 +20,15 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -20,21 +20,15 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
{
private readonly ILocalFileSystem _localFileSystem;
private readonly IMetadataRepository _metadataRepository;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger _logger;
private readonly IMediator _mediator;
protected MediaServerMovieLibraryScanner(
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILocalFileSystem localFileSystem,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger logger)
{
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_localFileSystem = localFileSystem;
_metadataRepository = metadataRepository;
_mediator = mediator;
@ -49,8 +43,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -49,8 +43,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
TConnectionParameters connectionParameters,
TLibrary library,
Func<TMovie, string> getLocalPath,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -69,8 +61,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -69,8 +61,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
connectionParameters,
library,
getLocalPath,
ffmpegPath,
ffprobePath,
GetMovieLibraryItems(connectionParameters, library),
count,
deepScan,
@ -91,8 +81,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -91,8 +81,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
TConnectionParameters connectionParameters,
TLibrary library,
Func<TMovie, string> getLocalPath,
string ffmpegPath,
string ffprobePath,
IAsyncEnumerable<TMovie> movieEntries,
int totalMovieCount,
bool deepScan,
@ -146,8 +134,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -146,8 +134,7 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
library,
existing,
incoming,
deepScan))
.BindT(UpdateSubtitles);
deepScan));
}
else
{
@ -168,8 +155,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -168,8 +155,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
existing,
incoming,
deepScan,
ffmpegPath,
ffprobePath,
None))
.BindT(UpdateSubtitles);
}
@ -393,8 +378,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -393,8 +378,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
result,
incoming,
deepScan,
string.Empty,
string.Empty,
mediaVersion);
foreach (BaseError error in statisticsResult.LeftToSeq())
@ -440,8 +423,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -440,8 +423,6 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
MediaItemScanResult<TMovie> result,
TMovie incoming,
bool deepScan,
string ffmpegPath,
string ffprobePath,
Option<MediaVersion> maybeMediaVersion)
{
TMovie existing = result.Item;
@ -469,24 +450,29 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib @@ -469,24 +450,29 @@ public abstract class MediaServerMovieLibraryScanner<TConnectionParameters, TLib
return result;
}
private async Task<Either<BaseError, MediaItemScanResult<TMovie>>> UpdateSubtitles(
MediaItemScanResult<TMovie> existing)
{
try
{
// skip checking subtitles for files that don't exist locally
if (!_localFileSystem.FileExists(existing.LocalPath))
MediaVersion version = existing.Item.GetHeadVersion();
Option<MovieMetadata> maybeMetadata = existing.Item.MovieMetadata.HeadOrNone();
foreach (MovieMetadata metadata in maybeMetadata)
{
return existing;
}
List<Subtitle> subtitles = version.Streams
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle)
.Map(Subtitle.FromMediaStream)
.ToList();
if (await _localSubtitlesProvider.UpdateSubtitles(existing.Item, existing.LocalPath, false))
{
return existing;
if (await _metadataRepository.UpdateSubtitles(metadata, subtitles))
{
return existing;
}
}
return BaseError.New("Failed to update local subtitles");
return BaseError.New("Failed to update media server subtitles");
}
catch (Exception ex)
{

53
ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs

@ -2,11 +2,11 @@ @@ -2,11 +2,11 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.MediaServer;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.MediaSources;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Scanner.Core.Metadata;
@ -22,21 +22,15 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -22,21 +22,15 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
{
private readonly ILocalFileSystem _localFileSystem;
private readonly IMetadataRepository _metadataRepository;
private readonly ILocalStatisticsProvider _localStatisticsProvider;
private readonly ILocalSubtitlesProvider _localSubtitlesProvider;
private readonly ILogger _logger;
private readonly IMediator _mediator;
protected MediaServerTelevisionLibraryScanner(
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILocalFileSystem localFileSystem,
IMetadataRepository metadataRepository,
IMediator mediator,
ILogger logger)
{
_localStatisticsProvider = localStatisticsProvider;
_localSubtitlesProvider = localSubtitlesProvider;
_localFileSystem = localFileSystem;
_metadataRepository = metadataRepository;
_mediator = mediator;
@ -51,8 +45,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -51,8 +45,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
TConnectionParameters connectionParameters,
TLibrary library,
Func<TEpisode, string> getLocalPath,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -73,8 +65,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -73,8 +65,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
connectionParameters,
library,
getLocalPath,
ffmpegPath,
ffprobePath,
GetShowLibraryItems(connectionParameters, library),
count,
deepScan,
@ -110,8 +100,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -110,8 +100,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
TConnectionParameters connectionParameters,
TLibrary library,
Func<TEpisode, string> getLocalPath,
string ffmpegPath,
string ffprobePath,
IAsyncEnumerable<TShow> showEntries,
int totalShowCount,
bool deepScan,
@ -181,8 +169,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -181,8 +169,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
getLocalPath,
result.Item,
connectionParameters,
ffmpegPath,
ffprobePath,
GetSeasonLibraryItems(library, connectionParameters, result.Item),
deepScan,
cancellationToken);
@ -304,8 +290,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -304,8 +290,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
Func<TEpisode, string> getLocalPath,
TShow show,
TConnectionParameters connectionParameters,
string ffmpegPath,
string ffprobePath,
IAsyncEnumerable<TSeason> seasonEntries,
bool deepScan,
CancellationToken cancellationToken)
@ -369,8 +353,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -369,8 +353,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
show,
result.Item,
connectionParameters,
ffmpegPath,
ffprobePath,
GetEpisodeLibraryItems(library, connectionParameters, show, result.Item),
deepScan,
cancellationToken);
@ -420,8 +402,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -420,8 +402,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
TShow show,
TSeason season,
TConnectionParameters connectionParameters,
string ffmpegPath,
string ffprobePath,
IAsyncEnumerable<TEpisode> episodeEntries,
bool deepScan,
CancellationToken cancellationToken)
@ -472,8 +452,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -472,8 +452,7 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
library,
existing,
incoming,
deepScan))
.BindT(UpdateSubtitles);
deepScan));
}
else
{
@ -494,8 +473,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -494,8 +473,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
existing,
incoming,
deepScan,
ffmpegPath,
ffprobePath,
None))
.BindT(UpdateSubtitles);
}
@ -727,8 +704,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -727,8 +704,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
result,
incoming,
deepScan,
string.Empty,
string.Empty,
mediaVersion);
foreach (BaseError error in statisticsResult.LeftToSeq())
@ -774,8 +749,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -774,8 +749,6 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
MediaItemScanResult<TEpisode> result,
TEpisode incoming,
bool deepScan,
string ffmpegPath,
string ffprobePath,
Option<MediaVersion> maybeMediaVersion)
{
TEpisode existing = result.Item;
@ -830,24 +803,28 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters, @@ -830,24 +803,28 @@ public abstract class MediaServerTelevisionLibraryScanner<TConnectionParameters,
return result;
}
private async Task<Either<BaseError, MediaItemScanResult<TEpisode>>> UpdateSubtitles(
MediaItemScanResult<TEpisode> existing)
{
try
{
// skip checking subtitles for files that don't exist locally
if (!_localFileSystem.FileExists(existing.LocalPath))
MediaVersion version = existing.Item.GetHeadVersion();
Option<EpisodeMetadata> maybeMetadata = existing.Item.EpisodeMetadata.HeadOrNone();
foreach (EpisodeMetadata metadata in maybeMetadata)
{
return existing;
}
List<Subtitle> subtitles = version.Streams
.Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle)
.Map(Subtitle.FromMediaStream)
.ToList();
if (await _localSubtitlesProvider.UpdateSubtitles(existing.Item, existing.LocalPath, false))
{
return existing;
if (await _metadataRepository.UpdateSubtitles(metadata, subtitles))
{
return existing;
}
}
return BaseError.New("Failed to update local subtitles");
return BaseError.New("Failed to update media server subtitles");
}
catch (Exception ex)
{

27
ErsatzTV.Scanner/Core/Plex/PlexMovieLibraryScanner.cs

@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;
@ -33,12 +32,8 @@ public class PlexMovieLibraryScanner : @@ -33,12 +32,8 @@ public class PlexMovieLibraryScanner :
IPlexMovieRepository plexMovieRepository,
IPlexPathReplacementService plexPathReplacementService,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<PlexMovieLibraryScanner> logger)
: base(
localStatisticsProvider,
localSubtitlesProvider,
localFileSystem,
metadataRepository,
mediator,
@ -60,8 +55,6 @@ public class PlexMovieLibraryScanner : @@ -60,8 +55,6 @@ public class PlexMovieLibraryScanner :
PlexConnection connection,
PlexServerAuthToken token,
PlexLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -81,8 +74,6 @@ public class PlexMovieLibraryScanner : @@ -81,8 +74,6 @@ public class PlexMovieLibraryScanner :
new PlexConnectionParameters(connection, token),
library,
GetLocalPath,
ffmpegPath,
ffprobePath,
deepScan,
cancellationToken);
}
@ -117,18 +108,7 @@ public class PlexMovieLibraryScanner : @@ -117,18 +108,7 @@ public class PlexMovieLibraryScanner :
{
if (result.IsAdded || result.Item.Etag != incoming.Etag || deepScan)
{
Either<BaseError, MovieMetadata> maybeMetadata = await _plexServerApiClient.GetMovieMetadata(
library,
incoming.Key.Split("/").Last(),
connectionParameters.Connection,
connectionParameters.Token);
foreach (BaseError error in maybeMetadata.LeftToSeq())
{
_logger.LogWarning("Failed to get movie metadata from Plex: {Error}", error.ToString());
}
return maybeMetadata.ToOption();
throw new NotSupportedException("This shouldn't happen anymore");
}
return None;
@ -358,6 +338,11 @@ public class PlexMovieLibraryScanner : @@ -358,6 +338,11 @@ public class PlexMovieLibraryScanner :
}
}
if (await _metadataRepository.UpdateSubtitles(existingMetadata, fullMetadata.Subtitles))
{
result.IsUpdated = true;
}
if (fullMetadata.SortTitle != existingMetadata.SortTitle)
{
existingMetadata.SortTitle = fullMetadata.SortTitle;

14
ErsatzTV.Scanner/Core/Plex/PlexTelevisionLibraryScanner.cs

@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Plex; @@ -6,7 +6,6 @@ using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
using ErsatzTV.Scanner.Core.Interfaces.Metadata;
using ErsatzTV.Scanner.Core.Metadata;
using Microsoft.Extensions.Logging;
@ -33,12 +32,8 @@ public class PlexTelevisionLibraryScanner : @@ -33,12 +32,8 @@ public class PlexTelevisionLibraryScanner :
IPlexPathReplacementService plexPathReplacementService,
IPlexTelevisionRepository plexTelevisionRepository,
ILocalFileSystem localFileSystem,
ILocalStatisticsProvider localStatisticsProvider,
ILocalSubtitlesProvider localSubtitlesProvider,
ILogger<PlexTelevisionLibraryScanner> logger)
: base(
localStatisticsProvider,
localSubtitlesProvider,
localFileSystem,
metadataRepository,
mediator,
@ -60,8 +55,6 @@ public class PlexTelevisionLibraryScanner : @@ -60,8 +55,6 @@ public class PlexTelevisionLibraryScanner :
PlexConnection connection,
PlexServerAuthToken token,
PlexLibrary library,
string ffmpegPath,
string ffprobePath,
bool deepScan,
CancellationToken cancellationToken)
{
@ -81,8 +74,6 @@ public class PlexTelevisionLibraryScanner : @@ -81,8 +74,6 @@ public class PlexTelevisionLibraryScanner :
new PlexConnectionParameters(connection, token),
library,
GetLocalPath,
ffmpegPath,
ffprobePath,
deepScan,
cancellationToken);
}
@ -601,6 +592,11 @@ public class PlexTelevisionLibraryScanner : @@ -601,6 +592,11 @@ public class PlexTelevisionLibraryScanner :
{
result.IsUpdated = true;
}
if (await _metadataRepository.UpdateSubtitles(existingMetadata, fullMetadata.Subtitles))
{
result.IsUpdated = true;
}
await _metadataRepository.MarkAsUpdated(existingMetadata, fullMetadata.DateUpdated);

48
ErsatzTV/Controllers/InternalController.cs

@ -114,11 +114,21 @@ public class InternalController : ControllerBase @@ -114,11 +114,21 @@ public class InternalController : ControllerBase
Left: _ => new NotFoundResult(),
Right: r =>
{
Url fullPath = Flurl.Url.Parse(r.Address)
.AppendPathSegment("Videos")
.AppendPathSegment(path)
.AppendPathSegment("stream")
.SetQueryParam("static", "true");
Url fullPath;
if (path.Contains("Subtitles"))
{
fullPath = Flurl.Url.Parse(r.Address)
.AppendPathSegment(path);
}
else
{
fullPath = Flurl.Url.Parse(r.Address)
.AppendPathSegment("Videos")
.AppendPathSegment(path)
.AppendPathSegment("stream")
.SetQueryParam("static", "true");
}
return new RedirectResult(fullPath.ToString());
});
@ -134,12 +144,23 @@ public class InternalController : ControllerBase @@ -134,12 +144,23 @@ public class InternalController : ControllerBase
Left: _ => new NotFoundResult(),
Right: r =>
{
Url fullPath = Flurl.Url.Parse(r.Address)
.AppendPathSegment("Videos")
.AppendPathSegment(path)
.AppendPathSegment("stream")
.SetQueryParam("static", "true")
.SetQueryParam("X-Emby-Token", r.ApiKey);
Url fullPath;
if (path.Contains("Subtitles"))
{
fullPath = Flurl.Url.Parse(r.Address)
.AppendPathSegment(path)
.SetQueryParam("X-Emby-Token", r.ApiKey);
}
else
{
fullPath = Flurl.Url.Parse(r.Address)
.AppendPathSegment("Videos")
.AppendPathSegment(path)
.AppendPathSegment("stream")
.SetQueryParam("static", "true")
.SetQueryParam("X-Emby-Token", r.ApiKey);
}
return new RedirectResult(fullPath.ToString());
});
@ -153,6 +174,11 @@ public class InternalController : ControllerBase @@ -153,6 +174,11 @@ public class InternalController : ControllerBase
Left: _ => new NotFoundResult(),
Right: r =>
{
if (r.StartsWith("http"))
{
return new RedirectResult(r);
}
string mimeType = Path.GetExtension(r).ToLowerInvariant() switch
{
"ass" or "ssa" => "text/x-ssa",

4
ErsatzTV/Services/SchedulerService.cs

@ -92,6 +92,7 @@ public class SchedulerService : BackgroundService @@ -92,6 +92,7 @@ public class SchedulerService : BackgroundService
try
{
await DeleteOrphanedArtwork(cancellationToken);
await DeleteOrphanedSubtitles(cancellationToken);
await RefreshChannelGuideChannelList(cancellationToken);
await BuildPlayouts(cancellationToken);
#if !DEBUG_NO_SYNC
@ -286,6 +287,9 @@ public class SchedulerService : BackgroundService @@ -286,6 +287,9 @@ public class SchedulerService : BackgroundService
private ValueTask DeleteOrphanedArtwork(CancellationToken cancellationToken) =>
_workerChannel.WriteAsync(new DeleteOrphanedArtwork(), cancellationToken);
private ValueTask DeleteOrphanedSubtitles(CancellationToken cancellationToken) =>
_workerChannel.WriteAsync(new DeleteOrphanedSubtitles(), cancellationToken);
private ValueTask ReleaseMemory(CancellationToken cancellationToken) =>
_workerChannel.WriteAsync(new ReleaseMemory(false), cancellationToken);
}

3
ErsatzTV/Services/WorkerService.cs

@ -68,6 +68,9 @@ public class WorkerService : BackgroundService @@ -68,6 +68,9 @@ public class WorkerService : BackgroundService
case DeleteOrphanedArtwork deleteOrphanedArtwork:
await mediator.Send(deleteOrphanedArtwork, cancellationToken);
break;
case DeleteOrphanedSubtitles deleteOrphanedSubtitles:
await mediator.Send(deleteOrphanedSubtitles, cancellationToken);
break;
case AddTraktList addTraktList:
await mediator.Send(addTraktList, cancellationToken);
break;

Loading…
Cancel
Save