Add movies & TV tracker: movies schema, Netflix/Letterboxd/Plex/JSON/recs importers, TMDB matching, stats, owner visibility

This commit is contained in:
2026-10-26 00:31:23 +00:00
parent 75571bbfe0
commit 71e015d1d0
28 changed files with 42754 additions and 14 deletions

View File

@@ -0,0 +1,47 @@
/**
* Import Ju's recommendations into movies.recs (the "Ju recommends" feed on /trackers/movies).
* Idempotent: upserts on (type, tmdb_id); title, for, reason and added_at are overwritten.
*
* scripts/movies-run.sh recs ~/imports/ju-recs.json # upsert
* scripts/movies-run.sh recs ~/imports/ju-recs.json --prune # also delete recs not in the file
* bun scripts/movies-recs-import.ts --dry-run ju-recs.json # validate only (no DB)
*
* JSON: an array of
* { "tmdb_id": 496243, "type": "movie", "title": "Parasite", "for": "both",
* "reason": "one line on why", "added_at": "2026-10-26T10:00:00+01:00" }
* type: movie | show ("tv" accepted); for: stan | juhee | both; reason and added_at optional
* (added_at defaults to now). Posters and year come from TMDB when TMDB_API_KEY is set.
*/
import { readFile } from 'node:fs/promises';
import postgres from 'postgres';
import { recsFile } from '../src/lib/server/movies/parse';
import { upsertRecs } from '../src/lib/server/movies/store';
import { Tmdb, runTmdb } from '../src/lib/server/movies/tmdb';
const args = process.argv.slice(2);
const files = args.filter((a) => !a.startsWith('--'));
if (files.length !== 1) {
console.error('usage: bun scripts/movies-recs-import.ts [--dry-run] [--prune] <file.json>');
process.exit(2);
}
const parsed = recsFile.safeParse(JSON.parse(await readFile(files[0], 'utf8')));
if (!parsed.success) {
for (const issue of parsed.error.issues.slice(0, 50))
console.error(`${issue.path.join('.')}: ${issue.message}`);
process.exit(1);
}
console.log(`Valid: ${parsed.data.length} recommendations`);
if (args.includes('--dry-run')) process.exit(0);
if (!process.env.DB_URL)
throw new Error('DB_URL is required (use scripts/movies-run.sh on stan-ovh)');
const sql = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} });
try {
const r = await upsertRecs(sql, parsed.data, args.includes('--prune'));
console.log(`Upserted ${r.upserted} recommendations${r.pruned ? `, pruned ${r.pruned}` : ''}`);
if (process.env.TMDB_API_KEY) {
const m = await runTmdb(sql, new Tmdb(process.env.TMDB_API_KEY), { limit: 0 });
console.log(`TMDB: ${m.errors} errors`);
}
} finally {
await sql.end();
}