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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core;
|
||||
|
||||
namespace ErsatzTV.Application.Maintenance;
|
||||
|
||||
public record DeleteOrphanedSubtitles : IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
|
||||
@@ -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
|
||||
.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
|
||||
.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
|
||||
.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
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ public enum MediaStreamKind
|
||||
Video = 1,
|
||||
Audio = 2,
|
||||
Subtitle = 3,
|
||||
Attachment = 4
|
||||
Attachment = 4,
|
||||
ExternalSubtitle = 5
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ public interface IEmbyMovieLibraryScanner
|
||||
string address,
|
||||
string apiKey,
|
||||
EmbyLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ public interface IEmbyTelevisionLibraryScanner
|
||||
string address,
|
||||
string apiKey,
|
||||
EmbyLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ public interface IJellyfinMovieLibraryScanner
|
||||
string address,
|
||||
string apiKey,
|
||||
JellyfinLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ public interface IJellyfinTelevisionLibraryScanner
|
||||
string address,
|
||||
string apiKey,
|
||||
JellyfinLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ public interface IPlexMovieLibraryScanner
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,8 +9,6 @@ public interface IPlexTelevisionLibraryScanner
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Jellyfin;
|
||||
|
||||
public static class JellyfinStream
|
||||
{
|
||||
public static readonly int ExternalStreamOffset = 100_000;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -728,7 +728,7 @@ public class EmbyTelevisionRepository : IEmbyTelevisionRepository
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
|
||||
|
||||
// version
|
||||
MediaVersion version = existing.MediaVersions.Head();
|
||||
MediaVersion incomingVersion = incoming.MediaVersions.Head();
|
||||
|
||||
@@ -731,7 +731,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository
|
||||
{
|
||||
metadata.Artwork.Remove(artworkToRemove);
|
||||
}
|
||||
|
||||
|
||||
// version
|
||||
MediaVersion version = existing.MediaVersions.Head();
|
||||
MediaVersion incomingVersion = incoming.MediaVersions.Head();
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
existingSubtitle.Language = incomingSubtitle.Language;
|
||||
existingSubtitle.SubtitleKind = incomingSubtitle.SubtitleKind;
|
||||
existingSubtitle.DateUpdated = incomingSubtitle.DateUpdated;
|
||||
existingSubtitle.Path = incomingSubtitle.Path;
|
||||
|
||||
dbContext.Entry(existingSubtitle).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
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
|
||||
.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
|
||||
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
File diff suppressed because it is too large
Load Diff
+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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 =>
|
||||
{
|
||||
|
||||
@@ -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
|
||||
[XmlAttribute("forced")]
|
||||
public bool Forced { get; set; }
|
||||
|
||||
[XmlAttribute("embeddedInVideo")]
|
||||
public bool EmbeddedInVideo { get; set; }
|
||||
|
||||
[XmlAttribute("languageCode")]
|
||||
public string LanguageCode { get; set; }
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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
|
||||
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
|
||||
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
|
||||
},
|
||||
Streams = new List<MediaStream>()
|
||||
};
|
||||
|
||||
MovieMetadata metadata = ProjectToMovieMetadata(version, response, mediaSourceId);
|
||||
|
||||
var movie = new PlexMovie
|
||||
{
|
||||
@@ -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
|
||||
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
|
||||
{
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
// 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
|
||||
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
|
||||
.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();
|
||||
|
||||
@@ -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
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILogger<SynchronizeEmbyLibraryByIdHandler> logger)
|
||||
{
|
||||
_embyApiClient = embyApiClient;
|
||||
_mediator = mediator;
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_embySecretStore = embySecretStore;
|
||||
@@ -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
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
parameters.DeepScan,
|
||||
cancellationToken),
|
||||
_ => Unit.Default
|
||||
|
||||
-4
@@ -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
|
||||
parameters.ConnectionParameters.ActiveConnection.Address,
|
||||
parameters.ConnectionParameters.ApiKey,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
parameters.DeepScan,
|
||||
cancellationToken),
|
||||
_ => Unit.Default
|
||||
|
||||
@@ -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
|
||||
parameters.ConnectionParameters.ActiveConnection,
|
||||
parameters.ConnectionParameters.PlexServerAuthToken,
|
||||
parameters.Library,
|
||||
parameters.FFmpegPath,
|
||||
parameters.FFprobePath,
|
||||
parameters.DeepScan,
|
||||
cancellationToken),
|
||||
_ => Unit.Default
|
||||
|
||||
@@ -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 :
|
||||
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 :
|
||||
string address,
|
||||
string apiKey,
|
||||
EmbyLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -75,8 +68,6 @@ public class EmbyMovieLibraryScanner :
|
||||
new EmbyConnectionParameters(address, apiKey),
|
||||
library,
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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<
|
||||
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<
|
||||
string address,
|
||||
string apiKey,
|
||||
EmbyLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -75,8 +68,6 @@ public class EmbyTelevisionLibraryScanner : MediaServerTelevisionLibraryScanner<
|
||||
new EmbyConnectionParameters(address, apiKey),
|
||||
library,
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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 :
|
||||
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 :
|
||||
string address,
|
||||
string apiKey,
|
||||
JellyfinLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -75,8 +68,6 @@ public class JellyfinMovieLibraryScanner :
|
||||
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
|
||||
library,
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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
|
||||
string address,
|
||||
string apiKey,
|
||||
JellyfinLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -76,8 +69,6 @@ public class JellyfinTelevisionLibraryScanner : MediaServerTelevisionLibraryScan
|
||||
new JellyfinConnectionParameters(address, apiKey, library.MediaSourceId),
|
||||
library,
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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
|
||||
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
|
||||
connectionParameters,
|
||||
library,
|
||||
getLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
GetMovieLibraryItems(connectionParameters, library),
|
||||
count,
|
||||
deepScan,
|
||||
@@ -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
|
||||
library,
|
||||
existing,
|
||||
incoming,
|
||||
deepScan))
|
||||
.BindT(UpdateSubtitles);
|
||||
deepScan));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -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
|
||||
result,
|
||||
incoming,
|
||||
deepScan,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
mediaVersion);
|
||||
|
||||
foreach (BaseError error in statisticsResult.LeftToSeq())
|
||||
@@ -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
|
||||
|
||||
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 _metadataRepository.UpdateSubtitles(metadata, subtitles))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
if (await _localSubtitlesProvider.UpdateSubtitles(existing.Item, existing.LocalPath, false))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
return BaseError.New("Failed to update local subtitles");
|
||||
return BaseError.New("Failed to update media server subtitles");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
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,
|
||||
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,
|
||||
connectionParameters,
|
||||
library,
|
||||
getLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
GetShowLibraryItems(connectionParameters, library),
|
||||
count,
|
||||
deepScan,
|
||||
@@ -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,
|
||||
getLocalPath,
|
||||
result.Item,
|
||||
connectionParameters,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
GetSeasonLibraryItems(library, connectionParameters, result.Item),
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
@@ -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,
|
||||
show,
|
||||
result.Item,
|
||||
connectionParameters,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
GetEpisodeLibraryItems(library, connectionParameters, show, result.Item),
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
@@ -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,
|
||||
library,
|
||||
existing,
|
||||
incoming,
|
||||
deepScan))
|
||||
.BindT(UpdateSubtitles);
|
||||
deepScan));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -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,
|
||||
result,
|
||||
incoming,
|
||||
deepScan,
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
mediaVersion);
|
||||
|
||||
foreach (BaseError error in statisticsResult.LeftToSeq())
|
||||
@@ -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,
|
||||
|
||||
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 _metadataRepository.UpdateSubtitles(metadata, subtitles))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
|
||||
if (await _localSubtitlesProvider.UpdateSubtitles(existing.Item, existing.LocalPath, false))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
return BaseError.New("Failed to update local subtitles");
|
||||
return BaseError.New("Failed to update media server subtitles");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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 :
|
||||
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 :
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -81,8 +74,6 @@ public class PlexMovieLibraryScanner :
|
||||
new PlexConnectionParameters(connection, token),
|
||||
library,
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -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 :
|
||||
}
|
||||
}
|
||||
|
||||
if (await _metadataRepository.UpdateSubtitles(existingMetadata, fullMetadata.Subtitles))
|
||||
{
|
||||
result.IsUpdated = true;
|
||||
}
|
||||
|
||||
if (fullMetadata.SortTitle != existingMetadata.SortTitle)
|
||||
{
|
||||
existingMetadata.SortTitle = fullMetadata.SortTitle;
|
||||
|
||||
@@ -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 :
|
||||
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 :
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary library,
|
||||
string ffmpegPath,
|
||||
string ffprobePath,
|
||||
bool deepScan,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -81,8 +74,6 @@ public class PlexTelevisionLibraryScanner :
|
||||
new PlexConnectionParameters(connection, token),
|
||||
library,
|
||||
GetLocalPath,
|
||||
ffmpegPath,
|
||||
ffprobePath,
|
||||
deepScan,
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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",
|
||||
|
||||
@@ -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
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user