mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
fixed stale monitored flag on touched cf entries
This commit is contained in:
1 parent
19576bc338
commit
e3f3607bcf
2 files changed
+186
-19
No files matched your search
+154
@@ -1186,6 +1186,41 @@ public class CustomFormatScoreSyncerTests : IDisposable
|
||||
(await _fixture.EventsContext.CustomFormatScoreEntries.ToListAsync()).ShouldHaveSingleItem().EpisodeId.ShouldBe(50);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_LidarrAlbumLosingTracksAndUnmonitored_ClearsIsMonitored()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance lidarrInstance = await AddLidarrWithSyncAsync();
|
||||
_fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
|
||||
{
|
||||
ArrInstanceId = lidarrInstance.Id,
|
||||
ExternalItemId = 5,
|
||||
EpisodeId = 50,
|
||||
ItemType = InstanceType.Lidarr,
|
||||
Title = "Test Artist - Album 50",
|
||||
FileId = 901,
|
||||
CurrentScore = 120,
|
||||
CutoffScore = 300,
|
||||
QualityProfileName = "Lossless",
|
||||
IsMonitored = true,
|
||||
LastSyncedAt = new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
});
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
StubLidarrLibrary(
|
||||
lidarrInstance,
|
||||
LidarrArtist(),
|
||||
[LidarrAlbum(50, trackFileCount: 2, trackCount: 3, monitored: false)],
|
||||
[new ArrTrackFile { Id = 901, AlbumId = 50, CustomFormatScore = 120 }]);
|
||||
|
||||
CustomFormatScoreSyncer sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
(await _fixture.EventsContext.CustomFormatScoreEntries.AsNoTracking().ToListAsync()).ShouldHaveSingleItem().IsMonitored.ShouldBeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Score Unchanged Tests
|
||||
@@ -1396,6 +1431,63 @@ public class CustomFormatScoreSyncerTests : IDisposable
|
||||
history[0].Score.ShouldBe(250);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ClearsIsMonitoredWhenMovieWithoutFileIsUnmonitored()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
|
||||
_fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 10,
|
||||
EpisodeId = 0,
|
||||
ItemType = InstanceType.Radarr,
|
||||
Title = "Mario Bros",
|
||||
FileId = 100,
|
||||
CurrentScore = 250,
|
||||
CutoffScore = 500,
|
||||
QualityProfileName = "HD",
|
||||
IsMonitored = true,
|
||||
LastSyncedAt = DateTime.UtcNow.AddHours(-1)
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
_radarrClient
|
||||
.GetQualityProfilesAsync(radarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 1, Name = "HD", CutoffFormatScore = 500 }]);
|
||||
|
||||
_radarrClient
|
||||
.StreamAllMoviesAsync(radarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerable<SearchableMovie>([
|
||||
new SearchableMovie
|
||||
{
|
||||
Id = 10, Title = "Mario Bros", HasFile = false,
|
||||
MovieFile = null,
|
||||
QualityProfileId = 1, Status = "released", Monitored = false
|
||||
}
|
||||
]));
|
||||
|
||||
CustomFormatScoreSyncer sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
List<CustomFormatScoreEntry> entries = await _fixture.EventsContext.CustomFormatScoreEntries.AsNoTracking().ToListAsync();
|
||||
entries.ShouldHaveSingleItem();
|
||||
entries[0].IsMonitored.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_PreservesEntryWhenMovieFileScoreNotReturned()
|
||||
{
|
||||
@@ -1553,6 +1645,68 @@ public class CustomFormatScoreSyncerTests : IDisposable
|
||||
history.ShouldHaveSingleItem();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_ClearsIsMonitoredWhenEpisodeWithoutFileIsUnmonitored()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ArrInstance = sonarrInstance,
|
||||
Enabled = true,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
|
||||
_fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ExternalItemId = 10,
|
||||
EpisodeId = 100,
|
||||
ItemType = InstanceType.Sonarr,
|
||||
Title = "Test Series S01E01",
|
||||
FileId = 500,
|
||||
CurrentScore = 300,
|
||||
CutoffScore = 500,
|
||||
QualityProfileName = "HD",
|
||||
IsMonitored = true,
|
||||
LastSyncedAt = new DateTime(1999, 1, 1, 0, 0, 0, DateTimeKind.Utc)
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
_sonarrClient
|
||||
.GetQualityProfilesAsync(sonarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 1, Name = "HD", CutoffFormatScore = 500 }]);
|
||||
|
||||
_sonarrClient
|
||||
.StreamAllSeriesAsync(sonarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerable<SearchableSeries>([
|
||||
new SearchableSeries { Id = 10, Title = "Test Series", QualityProfileId = 1, Monitored = true }
|
||||
]));
|
||||
|
||||
_sonarrClient
|
||||
.GetEpisodesAsync(sonarrInstance, 10)
|
||||
.Returns([
|
||||
new SearchableEpisode { Id = 100, SeasonNumber = 1, EpisodeNumber = 1, EpisodeFileId = 0, HasFile = false, Monitored = false }
|
||||
]);
|
||||
|
||||
_sonarrClient
|
||||
.GetEpisodeFilesAsync(sonarrInstance, 10)
|
||||
.Returns([]);
|
||||
|
||||
CustomFormatScoreSyncer sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
List<CustomFormatScoreEntry> entries = await _fixture.EventsContext.CustomFormatScoreEntries.AsNoTracking().ToListAsync();
|
||||
entries.ShouldHaveSingleItem();
|
||||
entries[0].IsMonitored.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_PreservesEntryWhenSeriesFetchFails()
|
||||
{
|
||||
|
||||
@@ -153,7 +153,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
DateTimeOffset now = _timeProvider.GetUtcNow();
|
||||
|
||||
List<(SearchableMovie Movie, long FileId)> withFilesBuffer = new(ChunkSize);
|
||||
List<long> withoutFilesBuffer = new(ChunkSize);
|
||||
List<(long Id, bool Monitored)> withoutFilesBuffer = new(ChunkSize);
|
||||
|
||||
int totalMovies = 0;
|
||||
int totalWithFiles = 0;
|
||||
@@ -182,7 +182,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
withoutFilesBuffer.Add(movie.Id);
|
||||
withoutFilesBuffer.Add((movie.Id, movie.Monitored));
|
||||
|
||||
if (withoutFilesBuffer.Count >= ChunkSize)
|
||||
{
|
||||
@@ -221,15 +221,21 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
instanceType, totalSynced, arrInstance.Name, totalSkipped);
|
||||
}
|
||||
|
||||
private async Task FlushMovieTouchChunkAsync(ArrInstance arrInstance, List<long> movieIds, DateTimeOffset now)
|
||||
private async Task FlushMovieTouchChunkAsync(ArrInstance arrInstance, List<(long Id, bool Monitored)> movies, DateTimeOffset now)
|
||||
{
|
||||
InstanceType instanceType = arrInstance.ArrConfig.Type;
|
||||
List<long> chunkList = movieIds.ToList();
|
||||
await _eventsContext.CustomFormatScoreEntries
|
||||
.Where(e => e.ArrInstanceId == arrInstance.Id
|
||||
&& e.ItemType == instanceType
|
||||
&& chunkList.Contains(e.ExternalItemId))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(e => e.LastSyncedAt, now));
|
||||
foreach (IGrouping<bool, (long Id, bool Monitored)> group in movies.GroupBy(x => x.Monitored))
|
||||
{
|
||||
bool isMonitored = group.Key;
|
||||
List<long> chunkList = group.Select(x => x.Id).ToList();
|
||||
await _eventsContext.CustomFormatScoreEntries
|
||||
.Where(e => e.ArrInstanceId == arrInstance.Id
|
||||
&& e.ItemType == instanceType
|
||||
&& chunkList.Contains(e.ExternalItemId))
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(e => e.LastSyncedAt, now)
|
||||
.SetProperty(e => e.IsMonitored, isMonitored));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(int Synced, int Skipped)> FlushMovieWithFilesChunkAsync(
|
||||
@@ -269,6 +275,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
if (existingEntries.TryGetValue(movie.Id, out CustomFormatScoreEntry? skippedEntry))
|
||||
{
|
||||
skippedEntry.LastSyncedAt = now;
|
||||
skippedEntry.IsMonitored = movie.Monitored;
|
||||
}
|
||||
_logger.LogTrace("[{InstanceType}] {InstanceName}: skipping movie '{Title}' (fileId={FileId}), no score returned",
|
||||
instanceType, arrInstance.Name, movie.Title, fileId);
|
||||
@@ -379,7 +386,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
|
||||
// Collect all episodes with files for this chunk of series
|
||||
List<(SearchableSeries Series, SearchableEpisode Episode, long FileId, int CfScore, bool IsMonitored)> itemsInChunk = [];
|
||||
List<(long SeriesId, long EpisodeId)> episodesToTouch = [];
|
||||
List<(long SeriesId, long EpisodeId, bool IsMonitored)> episodesToTouch = [];
|
||||
|
||||
foreach (SeriesFetch seriesFetch in fetched)
|
||||
{
|
||||
@@ -399,7 +406,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
episodesToTouch.Add((series.Id, episode.Id));
|
||||
episodesToTouch.Add((series.Id, episode.Id, series.Monitored && episode.Monitored));
|
||||
if (episode.EpisodeFileId > 0)
|
||||
{
|
||||
skipped++;
|
||||
@@ -454,8 +461,9 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
}
|
||||
|
||||
// Touch entries for episodes that exist but have no file (e.g., RSS upgrade in progress)
|
||||
foreach (var group in episodesToTouch.GroupBy(x => x.SeriesId))
|
||||
foreach (IGrouping<(long SeriesId, bool IsMonitored), (long SeriesId, long EpisodeId, bool IsMonitored)> group in episodesToTouch.GroupBy(x => (x.SeriesId, x.IsMonitored)))
|
||||
{
|
||||
(long seriesId, bool isMonitored) = group.Key;
|
||||
List<long> episodeIds = group.Select(x => x.EpisodeId).ToList();
|
||||
foreach (long[] epChunk in episodeIds.Chunk(ChunkSize))
|
||||
{
|
||||
@@ -463,9 +471,11 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
await _eventsContext.CustomFormatScoreEntries
|
||||
.Where(e => e.ArrInstanceId == arrInstance.Id
|
||||
&& e.ItemType == InstanceType.Sonarr
|
||||
&& e.ExternalItemId == group.Key
|
||||
&& e.ExternalItemId == seriesId
|
||||
&& epChunkList.Contains(e.EpisodeId))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(e => e.LastSyncedAt, now), cancellationToken);
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(e => e.LastSyncedAt, now)
|
||||
.SetProperty(e => e.IsMonitored, isMonitored), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +604,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
.ToList();
|
||||
|
||||
List<(SearchableArtist Artist, SearchableAlbum Album, ArrTrackFile File)> itemsInChunk = [];
|
||||
List<(long ArtistId, long AlbumId)> albumsToTouch = [];
|
||||
List<(long ArtistId, long AlbumId, bool IsMonitored)> albumsToTouch = [];
|
||||
|
||||
foreach (ArtistFetch artistFetch in fetched)
|
||||
{
|
||||
@@ -617,7 +627,7 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
albumsToTouch.Add((artist.Id, album.Id));
|
||||
albumsToTouch.Add((artist.Id, album.Id, artist.Monitored && album.Monitored));
|
||||
if (hasAllTracks)
|
||||
{
|
||||
skipped++;
|
||||
@@ -672,8 +682,9 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
}
|
||||
|
||||
// Keeps partial albums out of stale cleanup
|
||||
foreach (IGrouping<long, (long ArtistId, long AlbumId)> group in albumsToTouch.GroupBy(x => x.ArtistId))
|
||||
foreach (IGrouping<(long ArtistId, bool IsMonitored), (long ArtistId, long AlbumId, bool IsMonitored)> group in albumsToTouch.GroupBy(x => (x.ArtistId, x.IsMonitored)))
|
||||
{
|
||||
(long artistId, bool isMonitored) = group.Key;
|
||||
List<long> albumIds = group.Select(x => x.AlbumId).ToList();
|
||||
foreach (long[] albumChunk in albumIds.Chunk(ChunkSize))
|
||||
{
|
||||
@@ -681,9 +692,11 @@ public sealed class CustomFormatScoreSyncer : IHandler
|
||||
await _eventsContext.CustomFormatScoreEntries
|
||||
.Where(e => e.ArrInstanceId == arrInstance.Id
|
||||
&& e.ItemType == InstanceType.Lidarr
|
||||
&& e.ExternalItemId == group.Key
|
||||
&& e.ExternalItemId == artistId
|
||||
&& albumChunkList.Contains(e.EpisodeId))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(e => e.LastSyncedAt, now), cancellationToken);
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(e => e.LastSyncedAt, now)
|
||||
.SetProperty(e => e.IsMonitored, isMonitored), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user