From 4903b3137b175df95cd3e427188d5574005fea6a Mon Sep 17 00:00:00 2001 From: Flaminel Date: Wed, 1 Apr 2026 13:28:43 +0300 Subject: [PATCH] Add hide unmonitored toggle for quality scores (#535) --- .../CustomFormatScoreControllerTests.cs | 22 ++++++++++++++++++- .../CustomFormatScoreController.cs | 8 ++++++- .../frontend/src/app/core/api/cf-score.api.ts | 3 ++- .../quality-tab/quality-tab.component.html | 5 +++++ .../quality-tab/quality-tab.component.ts | 21 +++++++++++------- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs b/code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs index f5ecc457..a24dc833 100644 --- a/code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs +++ b/code/backend/Cleanuparr.Api.Tests/Features/Seeker/CustomFormatScoreControllerTests.cs @@ -77,6 +77,24 @@ public class CustomFormatScoreControllerTests : IDisposable body.GetProperty("Items")[0].GetProperty("Title").GetString().ShouldBe("Below Cutoff"); } + [Fact] + public async Task GetCustomFormatScores_WithHideUnmonitoredTrue_ExcludesUnmonitoredItems() + { + var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext); + AddScoreEntry(radarr.Id, 1, "Monitored Movie", currentScore: 100, cutoffScore: 500, isMonitored: true); + AddScoreEntry(radarr.Id, 2, "Unmonitored Movie", currentScore: 200, cutoffScore: 500, isMonitored: false); + AddScoreEntry(radarr.Id, 3, "Another Monitored", currentScore: 300, cutoffScore: 500, isMonitored: true); + + var result = await _controller.GetCustomFormatScores(hideUnmonitored: true); + var body = GetResponseBody(result); + + body.GetProperty("TotalCount").GetInt32().ShouldBe(2); + var items = body.GetProperty("Items"); + items.GetArrayLength().ShouldBe(2); + items[0].GetProperty("Title").GetString().ShouldBe("Another Monitored"); + items[1].GetProperty("Title").GetString().ShouldBe("Monitored Movie"); + } + [Fact] public async Task GetCustomFormatScores_WithSearchFilter_ReturnsMatchingTitlesOnly() { @@ -314,7 +332,8 @@ public class CustomFormatScoreControllerTests : IDisposable int currentScore, int cutoffScore, InstanceType itemType = InstanceType.Radarr, - DateTime? lastSynced = null) + DateTime? lastSynced = null, + bool isMonitored = true) { _dataContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry { @@ -327,6 +346,7 @@ public class CustomFormatScoreControllerTests : IDisposable CurrentScore = currentScore, CutoffScore = cutoffScore, QualityProfileName = "HD", + IsMonitored = isMonitored, LastSyncedAt = lastSynced ?? DateTime.UtcNow }); _dataContext.SaveChanges(); diff --git a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs index 64b02507..15842d4d 100644 --- a/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs +++ b/code/backend/Cleanuparr.Api/Features/Seeker/Controllers/CustomFormatScoreController.cs @@ -28,7 +28,8 @@ public sealed class CustomFormatScoreController : ControllerBase [FromQuery] Guid? instanceId = null, [FromQuery] string? search = null, [FromQuery] string sortBy = "title", - [FromQuery] bool hideMet = false) + [FromQuery] bool hideMet = false, + [FromQuery] bool hideUnmonitored = false) { if (page < 1) page = 1; if (pageSize < 1) pageSize = 50; @@ -53,6 +54,11 @@ public sealed class CustomFormatScoreController : ControllerBase query = query.Where(e => e.CurrentScore < e.CutoffScore); } + if (hideUnmonitored) + { + query = query.Where(e => e.IsMonitored); + } + int totalCount = await query.CountAsync(); var items = await (sortBy == "date" diff --git a/code/frontend/src/app/core/api/cf-score.api.ts b/code/frontend/src/app/core/api/cf-score.api.ts index 92897266..c32a5756 100644 --- a/code/frontend/src/app/core/api/cf-score.api.ts +++ b/code/frontend/src/app/core/api/cf-score.api.ts @@ -99,12 +99,13 @@ export class CfScoreApi { return this.http.get('/api/seeker/cf-scores/upgrades', { params }); } - getScores(page = 1, pageSize = 50, search?: string, instanceId?: string, sortBy?: string, hideMet?: boolean): Observable { + getScores(page = 1, pageSize = 50, search?: string, instanceId?: string, sortBy?: string, hideMet?: boolean, hideUnmonitored?: boolean): Observable { const params: Record = { page, pageSize }; if (search) params['search'] = search; if (instanceId) params['instanceId'] = instanceId; if (sortBy) params['sortBy'] = sortBy; if (hideMet) params['hideMet'] = true; + if (hideUnmonitored) params['hideUnmonitored'] = true; return this.http.get('/api/seeker/cf-scores', { params }); } diff --git a/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html b/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html index 7a1726f7..39891231 100644 --- a/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html +++ b/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.html @@ -23,6 +23,11 @@ [checked]="hideMet()" (checkedChange)="onHideMetChange($event)" /> +
diff --git a/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts b/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts index df87adae..299ea521 100644 --- a/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts +++ b/code/frontend/src/app/features/seeker-stats/quality-tab/quality-tab.component.ts @@ -54,6 +54,7 @@ export class QualityTabComponent implements OnInit { readonly sortBy = signal('title'); readonly hideMet = signal(false); + readonly hideUnmonitored = signal(false); readonly sortOptions: SelectOption[] = [ { label: 'Title', value: 'title' }, { label: 'Last Synced', value: 'date' }, @@ -93,7 +94,7 @@ export class QualityTabComponent implements OnInit { loadScores(): void { this.loading.set(true); - this.api.getScores(this.currentPage(), this.pageSize(), this.searchQuery() || undefined, this.selectedInstanceId() || undefined, this.sortBy(), this.hideMet()).subscribe({ + this.api.getScores(this.currentPage(), this.pageSize(), this.searchQuery() || undefined, this.selectedInstanceId() || undefined, this.sortBy(), this.hideMet(), this.hideUnmonitored()).subscribe({ next: (result) => { this.items.set(result.items); this.totalRecords.set(result.totalCount); @@ -122,9 +123,7 @@ export class QualityTabComponent implements OnInit { } onInstanceFilterChange(value: string): void { - this.selectedInstanceId.set(value); - this.currentPage.set(1); - this.loadScores(); + this.applyFilterChange(this.selectedInstanceId, value); } private loadStats(): void { @@ -140,13 +139,19 @@ export class QualityTabComponent implements OnInit { } onSortChange(value: string): void { - this.sortBy.set(value); - this.currentPage.set(1); - this.loadScores(); + this.applyFilterChange(this.sortBy, value); } onHideMetChange(value: boolean): void { - this.hideMet.set(value); + this.applyFilterChange(this.hideMet, value); + } + + onHideUnmonitoredChange(value: boolean): void { + this.applyFilterChange(this.hideUnmonitored, value); + } + + private applyFilterChange(setter: { set: (v: T) => void }, value: T): void { + setter.set(value); this.currentPage.set(1); this.loadScores(); }