mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-19 05:16:27 -07:00
feat(stats): add TMDB metadata for live-action dramas in the Library
Anime covers come from AniList, which has no live-action titles, so dramas and movies showed blank cards with no description and split across season folders. Library entries now carry a media kind plus a TMDB link, and the cover-art fetcher falls back to TMDB when AniList has no match, accepting only a Japanese non-animated title whose TMDB names match the parsed title exactly. Entries that resolve to the same TMDB title merge into one card regardless of season, and a Link to TMDB action in the detail view covers anything the automatic match missed. Release builds bundle a project-owned TMDB key injected from the SUBMINER_TMDB_API_KEY secret at build time; tmdb.apiKey/apiKeyCommand override it and are required when running from source. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import type { Hono } from 'hono';
|
||||
import type { Context, Hono } from 'hono';
|
||||
import type { AnkiConnectConfig } from '../../../types.js';
|
||||
import {
|
||||
statsJson,
|
||||
type StatsAnilistSearchResult,
|
||||
type StatsAnkiBrowseResponse,
|
||||
} from '../../../types/stats-http-contract.js';
|
||||
import { isTmdbMediaType } from '../../../shared/media-kind.js';
|
||||
import type { AnilistRateLimiter } from '../anilist/rate-limiter.js';
|
||||
import { TmdbApiKeyMissingError, type TmdbClient } from '../tmdb/tmdb-client.js';
|
||||
import { registerStatsCoverRoutes } from '../stats-cover-routes.js';
|
||||
import type { ImmersionTrackerService } from '../immersion-tracker-service.js';
|
||||
import {
|
||||
@@ -27,6 +29,7 @@ export function registerStatsIntegrationRoutes(
|
||||
ankiConnectConfig?: AnkiConnectConfig;
|
||||
getAnkiConnectConfig?: () => AnkiConnectConfig | undefined;
|
||||
anilistRateLimiter?: AnilistRateLimiter;
|
||||
tmdbClient?: TmdbClient;
|
||||
resolveAnkiNoteId?: (noteId: number) => number;
|
||||
},
|
||||
): void {
|
||||
@@ -71,6 +74,50 @@ export function registerStatsIntegrationRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
const tmdbUnavailable = (c: Context, err: unknown) => {
|
||||
if (err instanceof TmdbApiKeyMissingError) {
|
||||
return c.json(statsJson('error', { error: err.message }), 503);
|
||||
}
|
||||
return c.json(statsJson('error', { error: 'TMDB request failed' }), 502);
|
||||
};
|
||||
|
||||
app.get('/api/stats/tmdb/search', async (c) => {
|
||||
const query = (c.req.query('q') ?? '').trim();
|
||||
if (!query) return c.json(statsJson('tmdbSearch', []));
|
||||
const tmdbClient = options?.tmdbClient;
|
||||
if (!tmdbClient) return c.json(statsJson('tmdbSearch', []));
|
||||
try {
|
||||
return c.json(statsJson('tmdbSearch', await tmdbClient.search(query)));
|
||||
} catch (err) {
|
||||
return tmdbUnavailable(c, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.patch('/api/stats/anime/:animeId/tmdb', async (c) => {
|
||||
const animeId = parseIntQuery(c.req.param('animeId'), 0);
|
||||
if (animeId <= 0) return c.body(null, 400);
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const tmdbId = body?.tmdbId;
|
||||
if (
|
||||
typeof tmdbId !== 'number' ||
|
||||
!Number.isInteger(tmdbId) ||
|
||||
tmdbId <= 0 ||
|
||||
!isTmdbMediaType(body?.tmdbType)
|
||||
) {
|
||||
return c.body(null, 400);
|
||||
}
|
||||
const tmdbClient = options?.tmdbClient;
|
||||
if (!tmdbClient) return c.json(statsJson('error', { error: 'TMDB is not available' }), 503);
|
||||
try {
|
||||
const details = await tmdbClient.getDetails(body.tmdbType, tmdbId);
|
||||
if (!details) return c.body(null, 404);
|
||||
await tracker.reassignAnimeTmdb(animeId, details);
|
||||
return c.json(statsJson('reassignAnimeTmdb', { ok: true }));
|
||||
} catch (err) {
|
||||
return tmdbUnavailable(c, err);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/stats/known-words', (c) => {
|
||||
const knownWordsSet = loadKnownWordsSet(options?.knownWordCachePath);
|
||||
if (!knownWordsSet) return c.json(statsJson('knownWords', []));
|
||||
|
||||
Reference in New Issue
Block a user