diff --git a/code/frontend/src/app/core/api/search-stats.api.ts b/code/frontend/src/app/core/api/search-stats.api.ts
index 28d433ec..269f390c 100644
--- a/code/frontend/src/app/core/api/search-stats.api.ts
+++ b/code/frontend/src/app/core/api/search-stats.api.ts
@@ -2,7 +2,7 @@ import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import type { SearchStatsSummary, SearchEvent } from '@core/models/search-stats.models';
-import { SeekerSearchType, SeekerSearchReason, SearchCommandStatus } from '@core/models/search-stats.models';
+import { SeekerSearchType, SeekerSearchReason, SeekerContentKind, SearchCommandStatus } from '@core/models/search-stats.models';
import type { PaginatedResult } from '@core/models/pagination.model';
export enum SortDirection {
@@ -28,6 +28,7 @@ export interface SearchEventsQuery {
searchStatus?: SearchCommandStatus[];
searchType?: SeekerSearchType;
searchReason?: SeekerSearchReason;
+ contentKind?: SeekerContentKind;
grabbed?: boolean;
}
@@ -51,6 +52,7 @@ export class SearchStatsApi {
if (query.sortDirection) params = params.set('sortDirection', query.sortDirection);
if (query.searchType) params = params.set('searchType', query.searchType);
if (query.searchReason) params = params.set('searchReason', query.searchReason);
+ if (query.contentKind) params = params.set('contentKind', query.contentKind);
if (query.grabbed !== undefined) params = params.set('grabbed', String(query.grabbed));
if (query.searchStatus?.length) {
diff --git a/code/frontend/src/app/core/models/search-stats.models.ts b/code/frontend/src/app/core/models/search-stats.models.ts
index 89f86c53..29c65482 100644
--- a/code/frontend/src/app/core/models/search-stats.models.ts
+++ b/code/frontend/src/app/core/models/search-stats.models.ts
@@ -38,6 +38,13 @@ export enum SeekerSearchReason {
Replacement = 'Replacement',
}
+export enum SeekerContentKind {
+ Standard = 'Standard',
+ Daily = 'Daily',
+ Anime = 'Anime',
+ Special = 'Special',
+}
+
export enum SearchCommandStatus {
Pending = 'Pending',
Started = 'Started',
@@ -59,4 +66,5 @@ export interface SearchEvent {
grabbedItems: string[] | null;
cycleId: string | null;
isDryRun: boolean;
+ contentKind: SeekerContentKind | null;
}
diff --git a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html
index 33a6d91a..b1262fca 100644
--- a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html
+++ b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.html
@@ -164,6 +164,9 @@
{{ formatSearchReason(event.searchReason) }}
}
+ @if (event.contentKind) {
+ {{ event.contentKind }}
+ }
@if (event.searchStatus) {
{{ event.searchStatus }}
@@ -269,6 +272,15 @@
/>
+
+
Grabbed
= {
grabbedItems: ['Release.One', 'Release.Two'],
cycleId: 'cycle-aaaaaaaa-1111',
isDryRun: false,
+ contentKind: SeekerContentKind.Special,
},
{
id: 'event-2',
@@ -94,6 +95,7 @@ const EVENTS: PaginatedResult = {
grabbedItems: null,
cycleId: null,
isDryRun: true,
+ contentKind: null,
},
],
page: 1,
@@ -198,6 +200,7 @@ describe('SearchesTabComponent', () => {
searchStatus: undefined,
searchType: undefined,
searchReason: undefined,
+ contentKind: undefined,
grabbed: undefined,
});
});
@@ -213,24 +216,37 @@ describe('SearchesTabComponent', () => {
component.toggleStatus(SearchCommandStatus.Completed);
component.updateDraft('searchType', SeekerSearchType.Replacement);
component.updateDraft('searchReason', SeekerSearchReason.Missing);
+ component.updateDraft('contentKind', SeekerContentKind.Anime);
component.updateDraft('grabbed', 'false');
component.applyFilters();
fixture.detectChanges();
expect(component.drawerOpen()).toBe(false);
expect(component.selectedInstanceId()).toBe('radarr-zulu');
- expect(component.activeFilterCount()).toBe(5);
+ expect(component.activeFilterCount()).toBe(6);
expect(lastQuery()).toMatchObject({
instanceId: 'radarr-zulu',
search: 'show',
searchStatus: [SearchCommandStatus.Failed],
searchType: SeekerSearchType.Replacement,
searchReason: SeekerSearchReason.Missing,
+ contentKind: SeekerContentKind.Anime,
grabbed: false,
cycleId: undefined,
});
});
+ it('sends the chosen content kind in the events query', () => {
+ const { fixture, component, lastQuery } = setup();
+
+ component.openFilters();
+ component.updateDraft('contentKind', SeekerContentKind.Special);
+ component.applyFilters();
+ fixture.detectChanges();
+
+ expect(lastQuery().contentKind).toBe(SeekerContentKind.Special);
+ });
+
it('resolves the current cycle id only for the selected instance and drops it when the instance is cleared', () => {
const { fixture, component, lastQuery } = setup();
@@ -333,6 +349,14 @@ describe('SearchesTabComponent', () => {
);
});
+ it('shows a content kind badge only for an event that has one', () => {
+ const { fixture } = setup();
+
+ const rows = fixture.nativeElement.querySelectorAll('.list-row');
+ expect((rows[0] as HTMLElement).textContent).toContain('Special');
+ expect((rows[1] as HTMLElement).textContent).not.toContain('Special');
+ });
+
it('shows how many items remain in the cycle progress row', () => {
const { fixture } = setup();
diff --git a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts
index 6ffa0614..764e5642 100644
--- a/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts
+++ b/code/frontend/src/app/features/seeker-stats/searches-tab/searches-tab.component.ts
@@ -14,7 +14,7 @@ import { SearchStatsApi, SearchEventsSortBy, SortDirection } from '@core/api/sea
import type { SearchEventsQuery } from '@core/api/search-stats.api';
import type { SearchStatsSummary, SearchEvent, InstanceSearchStat } from '@core/models/search-stats.models';
import type { PaginatedResult } from '@core/models/pagination.model';
-import { SeekerSearchType, SeekerSearchReason, SearchCommandStatus } from '@core/models/search-stats.models';
+import { SeekerSearchType, SeekerSearchReason, SeekerContentKind, SearchCommandStatus } from '@core/models/search-stats.models';
import { AppHubService } from '@core/realtime/app-hub.service';
import { ToastService } from '@core/services/toast.service';
import { PaginationService, PAGE_SIZE_STORAGE_KEYS } from '@core/services/pagination.service';
@@ -46,6 +46,7 @@ interface AdvancedFilters extends Record {
statuses: SearchCommandStatus[];
searchType: SeekerSearchType | '';
searchReason: SeekerSearchReason | '';
+ contentKind: SeekerContentKind | '';
grabbed: TriState;
}
@@ -55,6 +56,7 @@ const EMPTY_FILTERS: AdvancedFilters = {
statuses: [],
searchType: '',
searchReason: '',
+ contentKind: '',
grabbed: 'any',
};
@@ -163,6 +165,7 @@ export class SearchesTabComponent {
searchStatus: a.statuses.length ? a.statuses : undefined,
searchType: a.searchType || undefined,
searchReason: a.searchReason || undefined,
+ contentKind: a.contentKind || undefined,
grabbed: triToBool(a.grabbed),
};
});
@@ -207,6 +210,14 @@ export class SearchesTabComponent {
{ label: 'Replacement', value: SeekerSearchReason.Replacement },
];
+ readonly contentKindOptions: SelectOption[] = [
+ { label: 'Any', value: '' },
+ { label: 'Standard', value: SeekerContentKind.Standard },
+ { label: 'Daily', value: SeekerContentKind.Daily },
+ { label: 'Anime', value: SeekerContentKind.Anime },
+ { label: 'Special', value: SeekerContentKind.Special },
+ ];
+
readonly triStateOptions: SelectOption[] = [
{ label: 'Any', value: 'any' },
{ label: 'Yes', value: 'true' },