diff --git a/code/backend/Cleanuparr.Domain/Entities/Arr/ArrEpisodeFile.cs b/code/backend/Cleanuparr.Domain/Entities/Arr/ArrEpisodeFile.cs index 2191a29e..8cd0358b 100644 --- a/code/backend/Cleanuparr.Domain/Entities/Arr/ArrEpisodeFile.cs +++ b/code/backend/Cleanuparr.Domain/Entities/Arr/ArrEpisodeFile.cs @@ -5,4 +5,6 @@ public sealed record ArrEpisodeFile public long Id { get; init; } public bool QualityCutoffNotMet { get; init; } + + public int CustomFormatScore { get; init; } } diff --git a/code/backend/Cleanuparr.Domain/Entities/Arr/EpisodeFileInfo.cs b/code/backend/Cleanuparr.Domain/Entities/Arr/EpisodeFileInfo.cs deleted file mode 100644 index 098a028f..00000000 --- a/code/backend/Cleanuparr.Domain/Entities/Arr/EpisodeFileInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Cleanuparr.Domain.Entities.Arr; - -public sealed record EpisodeFileInfo -{ - public long Id { get; init; } - - public bool QualityCutoffNotMet { get; init; } -} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs b/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs index 871b419e..e34e75cf 100644 --- a/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs +++ b/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs @@ -15,6 +15,4 @@ public sealed record SearchableEpisode public bool HasFile { get; init; } public long EpisodeFileId { get; init; } - - public EpisodeFileInfo? EpisodeFile { get; init; } } diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs index fe375575..69178f03 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs @@ -188,25 +188,31 @@ public sealed class CustomFormatScoreSyncer : IHandler foreach (SearchableSeries[] chunk in allSeries.Chunk(ChunkSize)) { // Collect all episodes with files for this chunk of series - List<(SearchableSeries Series, SearchableEpisode Episode, long FileId)> itemsInChunk = []; + List<(SearchableSeries Series, SearchableEpisode Episode, long FileId, int CfScore)> itemsInChunk = []; foreach (SearchableSeries series in chunk) { try { List episodes = await _sonarrClient.GetEpisodesAsync(arrInstance, series.Id); - int episodesWithFiles = episodes.Count(e => e.HasFile && e.EpisodeFile is not null && e.EpisodeFile.Id > 0); + List episodeFiles = await _sonarrClient.GetEpisodeFilesAsync(arrInstance, series.Id); - _logger.LogTrace("[Sonarr] {InstanceName}: series '{SeriesTitle}' (id={SeriesId}) has {TotalEpisodes} episodes, {WithFiles} with files", - arrInstance.Name, series.Title, series.Id, episodes.Count, episodesWithFiles); + // Build a map of fileId -> episode file + Dictionary fileMap = episodeFiles.ToDictionary(f => f.Id); + // Match episodes to their files via EpisodeFileId + int matched = 0; foreach (SearchableEpisode episode in episodes) { - if (episode.HasFile && episode.EpisodeFile is not null && episode.EpisodeFile.Id > 0) + if (episode.EpisodeFileId > 0 && fileMap.TryGetValue(episode.EpisodeFileId, out ArrEpisodeFile? file)) { - itemsInChunk.Add((series, episode, episode.EpisodeFile.Id)); + itemsInChunk.Add((series, episode, file.Id, file.CustomFormatScore)); + matched++; } } + + _logger.LogTrace("[Sonarr] {InstanceName}: series '{SeriesTitle}' (id={SeriesId}) has {TotalEpisodes} episodes, {FileCount} files, {Matched} matched", + arrInstance.Name, series.Title, series.Id, episodes.Count, episodeFiles.Count, matched); } catch (Exception ex) { @@ -225,12 +231,6 @@ public sealed class CustomFormatScoreSyncer : IHandler continue; } - List fileIds = itemsInChunk.Select(x => x.FileId).ToList(); - Dictionary scores = await _sonarrClient.GetEpisodeFileScoresAsync(arrInstance, fileIds); - - _logger.LogTrace("[Sonarr] {InstanceName}: chunk of {FileCount} file IDs returned {ScoreCount} score(s)", - arrInstance.Name, fileIds.Count, scores.Count); - List seriesIds = itemsInChunk.Select(x => x.Series.Id).Distinct().ToList(); Dictionary<(long, long), CustomFormatScoreEntry> existingEntries = await _dataContext.CustomFormatScoreEntries .Where(e => e.ArrInstanceId == arrInstance.Id @@ -238,16 +238,8 @@ public sealed class CustomFormatScoreSyncer : IHandler && seriesIds.Contains(e.ExternalItemId)) .ToDictionaryAsync(e => (e.ExternalItemId, e.EpisodeId)); - foreach ((SearchableSeries series, SearchableEpisode episode, long fileId) in itemsInChunk) + foreach ((SearchableSeries series, SearchableEpisode episode, long fileId, int cfScore) in itemsInChunk) { - if (!scores.TryGetValue(fileId, out int cfScore)) - { - totalSkipped++; - _logger.LogTrace("[Sonarr] {InstanceName}: skipping '{SeriesTitle}' S{Season:D2}E{Episode:D2} (fileId={FileId}) — no score returned", - arrInstance.Name, series.Title, episode.SeasonNumber, episode.EpisodeNumber, fileId); - continue; - } - profileMap.TryGetValue(series.QualityProfileId, out ArrQualityProfile? profile); int cutoffScore = profile?.CutoffFormatScore ?? 0; string profileName = profile?.Name ?? "Unknown"; @@ -265,7 +257,7 @@ public sealed class CustomFormatScoreSyncer : IHandler await CleanupStaleEntriesAsync(arrInstance.Id, InstanceType.Sonarr, syncStartTime); - _logger.LogInformation("[Sonarr] Synced CF scores for {Count} episodes on {InstanceName} ({Skipped} skipped — no score returned)", + _logger.LogInformation("[Sonarr] Synced CF scores for {Count} episodes on {InstanceName} ({Skipped} skipped)", totalSynced, arrInstance.Name, totalSkipped); }