feat(stats): add library entry merge and episode move

- Add multi-select "Merge Selected" flow to fold duplicate library cards into one, preserving sessions, mined cards, and watch time
- Add per-episode "move to another entry" action for reassigning stray episodes, pruning the source entry when emptied
- Auto-merge library entries that resolve to the same AniList id when their parsed seasons are compatible
- Add mergeAnime/moveVideoToAnime service methods, stats-server routes, and HTTP contract types
This commit is contained in:
2026-08-10 22:52:34 -07:00
parent 2fefc83e3f
commit 5938095d92
19 changed files with 1629 additions and 9 deletions
+28
View File
@@ -100,6 +100,30 @@ export interface StatsAnkiNotesInfoRequest {
noteIds: number[];
}
export interface StatsMergeAnimeRequest {
sourceAnimeIds: number[];
}
export interface StatsMoveVideoRequest {
animeId: number;
}
export interface StatsMergeAnimeResponse {
ok: true;
/** Library entry that owns every merged episode afterwards. */
animeId: number;
mergedAnimeIds: number[];
movedVideos: number;
}
export interface StatsMoveVideoResponse {
ok: true;
animeId: number;
previousAnimeId: number | null;
/** True when the previous entry was emptied by the move and removed. */
removedPreviousAnime: boolean;
}
export interface StatsOkResponse {
ok: true;
}
@@ -142,6 +166,8 @@ export interface StatsJsonResponseMap {
deleteSession: StatsOkResponse;
deleteVideo: StatsOkResponse;
deleteAnime: StatsOkResponse;
mergeAnime: StatsMergeAnimeResponse;
moveVideoToAnime: StatsMoveVideoResponse;
anilistSearch: StatsAnilistSearchResult[];
knownWords: string[];
knownWordsSummary: StatsKnownWordsSummary;
@@ -221,6 +247,8 @@ export interface StatsHttpClient {
deleteSessions: (sessionIds: number[]) => Promise<void>;
deleteVideo: (videoId: number) => Promise<void>;
deleteAnime: (animeId: number) => Promise<void>;
mergeAnime: (targetAnimeId: number, sourceAnimeIds: number[]) => Promise<StatsMergeAnimeResponse>;
moveVideoToAnime: (videoId: number, animeId: number) => Promise<StatsMoveVideoResponse>;
getKnownWords: () => Promise<string[]>;
getKnownWordsSummary: () => Promise<StatsKnownWordsSummary>;
getAnimeKnownWordsSummary: (animeId: number) => Promise<StatsKnownWordsSummary>;