feat(stats): add live-action library support and harden stats runtime

- Add TMDB metadata, linking, filtering, and live-action library merging
- Harden stats request validation, lifecycle handling, and compiled runtime coverage
- Fix Anki media synchronization and configuration validation
This commit is contained in:
2026-09-21 00:18:32 -07:00
158 changed files with 6623 additions and 1119 deletions
+1
View File
@@ -157,6 +157,7 @@ export interface AnkiConnectConfig {
fields?: {
word?: string;
audio?: string;
wordAudio?: string;
image?: string;
sentence?: string;
miscInfo?: string;
+7
View File
@@ -12,6 +12,7 @@ import type {
ImmersionTrackingRetentionMode,
ImmersionTrackingRetentionPreset,
TsukihimeConfig,
TmdbConfig,
JellyfinConfig,
AnimeConfig,
JimakuConfig,
@@ -162,6 +163,7 @@ export interface Config {
/** @deprecated Use tsukihime. */
animetosho?: TsukihimeConfig;
tsukihime?: TsukihimeConfig;
tmdb?: TmdbConfig;
anilist?: AnilistConfig;
yomitan?: YomitanConfig;
jellyfin?: JellyfinConfig;
@@ -230,6 +232,7 @@ export interface ResolvedConfig {
fields: {
word: string;
audio: string;
wordAudio: string;
image: string;
sentence: string;
miscInfo: string;
@@ -342,6 +345,10 @@ export interface ResolvedConfig {
apiBaseUrl: string;
maxSearchResults: number;
};
tmdb: {
apiKey: string;
apiKeyCommand: string;
};
anilist: {
enabled: boolean;
accessToken: string;
+6
View File
@@ -321,3 +321,9 @@ export interface TsukihimeConfig {
apiBaseUrl?: string;
maxSearchResults?: number;
}
/** TMDB (The Movie Database) access for live-action drama and movie metadata. */
export interface TmdbConfig {
apiKey?: string;
apiKeyCommand?: string;
}
+24
View File
@@ -27,6 +27,7 @@ import type {
WatchTimePerAnime,
WordDetailData,
} from './stats-wire';
import type { TmdbMediaType } from '../shared/media-kind';
export type StatsTrendRange = '7d' | '30d' | '90d' | '365d' | 'all';
export type StatsTrendGroupBy = 'day' | 'month';
@@ -78,6 +79,25 @@ export interface StatsAnilistSearchResult {
title: { romaji: string | null; english: string | null; native: string | null } | null;
}
export interface StatsTmdbSearchResult {
tmdbId: number;
tmdbType: TmdbMediaType;
/** English title when TMDB has one, otherwise the original title. */
title: string;
originalTitle: string;
originalLanguage: string;
overview: string | null;
posterUrl: string | null;
year: number | null;
/** True when TMDB tags the title with the Animation genre. */
isAnimation: boolean;
}
export interface StatsTmdbAssignment {
tmdbId: number;
tmdbType: TmdbMediaType;
}
export interface StatsAnilistAssignment {
anilistId: number;
titleRomaji?: string | null;
@@ -209,11 +229,13 @@ export interface StatsJsonResponseMap {
moveVideoToAnime: StatsMoveVideoResponse;
dismissAnimeMergeRecommendation: StatsOkResponse;
anilistSearch: StatsAnilistSearchResult[];
tmdbSearch: StatsTmdbSearchResult[];
knownWords: string[];
knownWordsSummary: StatsKnownWordsSummary;
animeKnownWordsSummary: StatsKnownWordsSummary;
mediaKnownWordsSummary: StatsKnownWordsSummary;
reassignAnimeAnilist: StatsOkResponse;
reassignAnimeTmdb: StatsOkResponse;
coverImages: StatsCoverImagesData;
episodeDetail: EpisodeDetailData;
ankiBrowse: StatsAnkiBrowseResponse;
@@ -302,6 +324,8 @@ export interface StatsHttpClient {
getMediaKnownWordsSummary: (videoId: number) => Promise<StatsKnownWordsSummary>;
searchAnilist: (query: string) => Promise<StatsAnilistSearchResult[]>;
reassignAnimeAnilist: (animeId: number, info: StatsAnilistAssignment) => Promise<void>;
searchTmdb: (query: string) => Promise<StatsTmdbSearchResult[]>;
reassignAnimeTmdb: (animeId: number, info: StatsTmdbAssignment) => Promise<void>;
mineCard: (params: StatsMineCardParams) => Promise<StatsMineCardResponse>;
ankiBrowse: (noteId: number) => Promise<void>;
ankiNotesInfo: (noteIds: number[]) => Promise<StatsAnkiNoteInfo[]>;
+6 -1
View File
@@ -1,4 +1,5 @@
import type { MediaKind } from '../shared/media-kind';
import type { MediaKind, TmdbMediaType } from '../shared/media-kind';
export interface SessionSummary {
sessionId: number;
canonicalTitle: string | null;
@@ -245,6 +246,8 @@ export interface AnimeLibraryItem {
animeId: number;
canonicalTitle: string;
anilistId: number | null;
tmdbId: number | null;
tmdbType: TmdbMediaType | null;
totalSessions: number;
totalActiveMs: number;
totalCards: number;
@@ -267,6 +270,8 @@ export interface AnimeDetailData {
animeId: number;
canonicalTitle: string;
anilistId: number | null;
tmdbId: number | null;
tmdbType: TmdbMediaType | null;
titleRomaji: string | null;
titleEnglish: string | null;
titleNative: string | null;