From 7c82ecdffffe509988959fa967d6a33b69e8343c Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:28:19 -0600 Subject: [PATCH] use separate load queries for jf episode inserts and updates (#2751) --- .../JellyfinTelevisionRepository.cs | 98 ++++++++++--------- .../MediaServerTelevisionLibraryScanner.cs | 36 ++++--- ErsatzTV.Scanner/Program.cs | 1 + 3 files changed, 75 insertions(+), 60 deletions(-) diff --git a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs index f139684d5..749e08f95 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/JellyfinTelevisionRepository.cs @@ -5,7 +5,6 @@ using ErsatzTV.Core.Errors; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; -using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -91,8 +90,6 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeExisting = await dbContext.JellyfinShows .TagWithCallSite() - .Include(m => m.LibraryPath) - .ThenInclude(lp => lp.Library) .Include(m => m.ShowMetadata) .ThenInclude(mm => mm.Genres) .Include(m => m.ShowMetadata) @@ -105,9 +102,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository .ThenInclude(mm => mm.Artwork) .Include(m => m.ShowMetadata) .ThenInclude(mm => mm.Guids) - .Include(m => m.TraktListItems) - .ThenInclude(tli => tli.TraktList) - .SelectOneAsync(s => s.ItemId, s => s.ItemId == item.ItemId, cancellationToken); + .SingleOrDefaultAsync(s => s.ItemId == item.ItemId, cancellationToken); foreach (JellyfinShow jellyfinShow in maybeExisting) { @@ -135,12 +130,11 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); Option maybeExisting = await dbContext.JellyfinSeasons .TagWithCallSite() - .Include(m => m.LibraryPath) .Include(m => m.SeasonMetadata) .ThenInclude(mm => mm.Artwork) .Include(m => m.SeasonMetadata) .ThenInclude(mm => mm.Guids) - .SelectOneAsync(s => s.ItemId, s => s.ItemId == item.ItemId, cancellationToken); + .SingleOrDefaultAsync(s => s.ItemId == item.ItemId, cancellationToken); foreach (JellyfinSeason jellyfinSeason in maybeExisting) { @@ -167,44 +161,56 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository using (ScanProfiler.Measure("DB Ins/Upd Episode")) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - Option maybeExisting = await dbContext.JellyfinEpisodes + + Option maybeExistingState = await dbContext.JellyfinEpisodes .TagWithCallSite() - .Include(m => m.LibraryPath) - .ThenInclude(lp => lp.Library) - .Include(m => m.MediaVersions) - .ThenInclude(mv => mv.MediaFiles) - .Include(m => m.MediaVersions) - .ThenInclude(mv => mv.Streams) - .Include(m => m.MediaVersions) - .ThenInclude(mv => mv.Chapters) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Artwork) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Guids) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Genres) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Tags) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Studios) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Actors) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Directors) - .Include(m => m.EpisodeMetadata) - .ThenInclude(mm => mm.Writers) - .Include(m => m.Season) - .Include(m => m.TraktListItems) - .ThenInclude(tli => tli.TraktList) - .SelectOneAsync(s => s.ItemId, s => s.ItemId == item.ItemId, cancellationToken); - - foreach (JellyfinEpisode jellyfinEpisode in maybeExisting) + .Where(s => s.ItemId == item.ItemId) + .Select(s => new { s.Id, s.Etag }) + .SingleOrDefaultAsync(cancellationToken); + + foreach (dynamic existingState in maybeExistingState) { - var result = new MediaItemScanResult(jellyfinEpisode) { IsAdded = false }; - if (jellyfinEpisode.Etag != item.Etag || deepScan) + int existingId = existingState.Id; + + MediaItemScanResult result; + if (existingState.Etag != item.Etag || deepScan) { - await UpdateEpisode(dbContext, jellyfinEpisode, item, cancellationToken); - result.IsUpdated = true; + JellyfinEpisode existing = await dbContext.JellyfinEpisodes + .TagWithCallSite() + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Artwork) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Guids) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Genres) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Tags) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Actors).ThenInclude(a => a.Artwork) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Directors) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Writers) + .Include(e => e.MediaVersions).ThenInclude(mv => mv.MediaFiles) + .Include(e => e.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(e => e.MediaVersions).ThenInclude(mv => mv.Chapters) + .AsSplitQuery() + .SingleAsync(s => s.Id == existingId, cancellationToken); + + await dbContext.Entry(existing).Reference(e => e.Season).LoadAsync(cancellationToken); + + await UpdateEpisode(dbContext, existing, item, cancellationToken); + + result = new MediaItemScanResult(existing) { IsAdded = false, IsUpdated = true }; + } + else + { + JellyfinEpisode existing = await dbContext.JellyfinEpisodes + .AsNoTracking() + .TagWithCallSite() + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Actors) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Directors) + .Include(e => e.EpisodeMetadata).ThenInclude(em => em.Writers) + .Include(e => e.MediaVersions).ThenInclude(mv => mv.MediaFiles) + .Include(e => e.MediaVersions).ThenInclude(mv => mv.Streams) + .Include(e => e.MediaVersions).ThenInclude(mv => mv.Chapters) + .AsSplitQuery() + .SingleAsync(s => s.Id == existingId, cancellationToken); + + result = new MediaItemScanResult(existing) { IsAdded = false }; } return result; @@ -548,7 +554,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository CancellationToken cancellationToken) { // library path is used for search indexing later - incoming.LibraryPath = existing.LibraryPath; + incoming.LibraryPathId = existing.LibraryPathId; incoming.Id = existing.Id; // metadata @@ -696,7 +702,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository CancellationToken cancellationToken) { // library path is used for search indexing later - incoming.LibraryPath = existing.LibraryPath; + incoming.LibraryPathId = existing.LibraryPathId; incoming.Id = existing.Id; existing.SeasonNumber = incoming.SeasonNumber; @@ -795,7 +801,7 @@ public class JellyfinTelevisionRepository : IJellyfinTelevisionRepository CancellationToken cancellationToken) { // library path is used for search indexing later - incoming.LibraryPath = existing.LibraryPath; + incoming.LibraryPathId = existing.LibraryPathId; incoming.Id = existing.Id; // metadata diff --git a/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs b/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs index be737eb6f..2cb7b6a11 100644 --- a/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs +++ b/ErsatzTV.Scanner/Core/Metadata/MediaServerTelevisionLibraryScanner.cs @@ -464,7 +464,7 @@ public abstract class MediaServerTelevisionLibraryScanner UpdateSubtitles(existing, cancellationToken)) + .BindT(existing => UpdateSubtitles(existing, incoming, deepScan, cancellationToken)) .BindT(existing => UpdateChapters(existing, cancellationToken)); } @@ -831,29 +831,37 @@ public abstract class MediaServerTelevisionLibraryScanner>> UpdateSubtitles( MediaItemScanResult existing, + TEpisode incoming, + bool deepScan, CancellationToken cancellationToken) { try { - using (ScanProfiler.Measure("Update Episode Subtitles")) + if (deepScan || existing.IsAdded || MediaServerEtag(existing.Item) != MediaServerEtag(incoming)) { - MediaVersion version = existing.Item.GetHeadVersion(); - Option maybeMetadata = existing.Item.EpisodeMetadata.HeadOrNone(); - foreach (EpisodeMetadata metadata in maybeMetadata) + using (ScanProfiler.Measure("Update Episode Subtitles")) { - List subtitles = version.Streams - .Filter(s => s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) - .Map(Subtitle.FromMediaStream) - .ToList(); - - if (await _metadataRepository.UpdateSubtitles(metadata, subtitles, cancellationToken)) + MediaVersion version = existing.Item.GetHeadVersion(); + Option maybeMetadata = existing.Item.EpisodeMetadata.HeadOrNone(); + foreach (EpisodeMetadata metadata in maybeMetadata) { - return existing; + List subtitles = version.Streams + .Filter(s => + s.MediaStreamKind is MediaStreamKind.Subtitle or MediaStreamKind.ExternalSubtitle) + .Map(Subtitle.FromMediaStream) + .ToList(); + + if (await _metadataRepository.UpdateSubtitles(metadata, subtitles, cancellationToken)) + { + return existing; + } } - } - return BaseError.New("Failed to update media server subtitles"); + return BaseError.New("Failed to update media server subtitles"); + } } + + return existing; } catch (Exception ex) { diff --git a/ErsatzTV.Scanner/Program.cs b/ErsatzTV.Scanner/Program.cs index d5e09b62e..f5f796284 100644 --- a/ErsatzTV.Scanner/Program.cs +++ b/ErsatzTV.Scanner/Program.cs @@ -62,6 +62,7 @@ public class Program Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Error) + .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console(new CompactJsonFormatter(), standardErrorFromLevel: LogEventLevel.Debug) .CreateLogger();