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:
2026-09-18 23:31:35 -07:00
co-authored by Claude Fable 5.1
parent dd76782d30
commit 2b33d32879
68 changed files with 2339 additions and 68 deletions
+29
View File
@@ -0,0 +1,29 @@
import fs from 'node:fs';
import path from 'node:path';
/**
* Release builds carry a project-owned TMDB key so live-action lookups work
* without user setup. The key is injected from the SUBMINER_TMDB_API_KEY
* environment variable at build time (a GitHub Actions secret in CI) and never
* lives in the repository. The runtime reader is
* src/core/services/tmdb/bundled-api-key.ts; keep the file name in sync.
*/
export const BUNDLED_INTEGRATION_KEYS_FILENAME = 'bundled-integration-keys.json';
export const TMDB_API_KEY_ENV = 'SUBMINER_TMDB_API_KEY';
/**
* Write the bundled keys file into `distDir`, or remove a stale one when no
* key is present so a keyless build never ships an older key by accident.
* Returns the names of the keys staged.
*/
export function stageBundledIntegrationKeys(distDir, env = process.env) {
const outputPath = path.join(distDir, BUNDLED_INTEGRATION_KEYS_FILENAME);
const tmdbApiKey = env[TMDB_API_KEY_ENV]?.trim() ?? '';
if (!tmdbApiKey) {
fs.rmSync(outputPath, { force: true });
return [];
}
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify({ tmdbApiKey })}\n`, { mode: 0o644 });
return ['tmdb'];
}
+39
View File
@@ -0,0 +1,39 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
BUNDLED_INTEGRATION_KEYS_FILENAME,
stageBundledIntegrationKeys,
} from './bundled-integration-keys.mjs';
function withDistDir(work: (distDir: string) => void): void {
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), 'subminer-bundled-keys-'));
try {
work(distDir);
} finally {
fs.rmSync(distDir, { recursive: true, force: true });
}
}
test('stageBundledIntegrationKeys writes the TMDB key from the environment', () => {
withDistDir((distDir) => {
const staged = stageBundledIntegrationKeys(distDir, { SUBMINER_TMDB_API_KEY: ' abc123 ' });
assert.deepEqual(staged, ['tmdb']);
const written = JSON.parse(
fs.readFileSync(path.join(distDir, BUNDLED_INTEGRATION_KEYS_FILENAME), 'utf8'),
);
assert.deepEqual(written, { tmdbApiKey: 'abc123' });
});
});
test('stageBundledIntegrationKeys removes a stale file when the variable is unset', () => {
withDistDir((distDir) => {
const outputPath = path.join(distDir, BUNDLED_INTEGRATION_KEYS_FILENAME);
fs.writeFileSync(outputPath, '{"tmdbApiKey":"old"}');
assert.deepEqual(stageBundledIntegrationKeys(distDir, {}), []);
assert.equal(fs.existsSync(outputPath), false);
assert.deepEqual(stageBundledIntegrationKeys(distDir, { SUBMINER_TMDB_API_KEY: ' ' }), []);
});
});
+13
View File
@@ -3,6 +3,7 @@ import os from 'node:os';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { stageBundledIntegrationKeys, TMDB_API_KEY_ENV } from './bundled-integration-keys.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
@@ -97,6 +98,17 @@ function buildMacosHelper() {
}
}
// Only the key names are logged, never the values: CI masks secrets, but a
// stray echo would still leak them into local build logs.
function stageIntegrationKeys() {
const staged = stageBundledIntegrationKeys(path.join(repoRoot, 'dist'));
process.stdout.write(
staged.length > 0
? `Staged bundled integration keys: ${staged.join(', ')}\n`
: `No bundled integration keys (${TMDB_API_KEY_ENV} unset)\n`,
);
}
function main() {
fs.cpSync(path.join(rendererSourceDir, 'fonts'), path.join(repoRoot, 'dist', 'fonts'), {
recursive: true,
@@ -106,6 +118,7 @@ function main() {
copySettingsAssets();
copySyncUiAssets();
buildMacosHelper();
stageIntegrationKeys();
}
main();