trackers: media hub with webtoons, podcasts and videos; shows and anime filters
Deploy production / deploy (push) Successful in 1m22s

- /trackers/media: one card per child (reading, movies, shows, webtoons, music, podcasts,
  videos) with quick stats, a combined currently-consuming feed and a mixed watchlist
- webtoons, podcasts, videos: new owner-editable trackers on one generic media.items table,
  JSON import (owner form or scripts/media-run.sh)
- shows: /trackers/shows -> /trackers/movies?type=show; anime flag on movies.titles
  (backfilled, set from TMDB, owner toggle) and ?type=anime filter
- nav: media replaces the top-level reading, movies and music entries; their URLs are kept
This commit is contained in:
stan committed 2026-10-26 23:34:22 +00:00
1 parent 4ef75f9d25
commit 079399635d
33 files changed
+48352 -44

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
/**
* Import entries into the media trackers' generic children (webtoons, podcasts, videos;
* media.items, /trackers/webtoons|podcasts|videos). Idempotent: rows are keyed by kind +
* normalised title (+ creator for videos); re-running updates the given fields, never duplicates.
* The owner can paste the same JSON on each tracker page (import tile).
*
* On stan-ovh (DB_URL is loaded for you, nothing printed):
* scripts/media-run.sh ~/imports/webtoons.json --kind webtoons
* scripts/media-run.sh ~/imports/media.json # object keyed by kind
* Add --dry-run to parse and print a summary without touching the database.
*
* Format: an array of entries (for --kind, or each with its own "kind"), or an object
* { "webtoons": [...], "podcasts": [...], "videos": [...] }. Entry fields (only title required):
* title, creator | author | host | channel, platform, genre, status, progress | episode | chapter,
* total | total_episodes, series_status (ongoing|completed, webtoons), form (channel|video|playlist,
* videos), url, cover_url (https), rating (0.5–5), who (stan|juhee|both), started, finished
* (YYYY-MM-DD), notes.
* Statuses: webtoons reading|completed|on_hold|dropped|plan; podcasts listening|caught_up|paused|
* dropped|plan; videos watching|following|watched|dropped|plan ("plan to read" etc. map to plan).
*/
import { readFile } from 'node:fs/promises';
import postgres from 'postgres';
import { importRows, parseImport } from '../src/lib/server/media/store';
import { KINDS, MEDIA_KINDS, type MediaKind } from '../src/lib/media/kinds';
const args = process.argv.slice(2);
const flag = (name: string) => {
const i = args.indexOf(name);
return i >= 0 ? args.splice(i, 2)[1] : undefined;
};
const dry = args.includes('--dry-run');
const kindArg = flag('--kind');
const file = args.find((a) => !a.startsWith('--'));
if (!file)
throw new Error(
'usage: media-import.ts <file.json> [--kind webtoons|podcasts|videos] [--dry-run]'
);
const kind: MediaKind | undefined = kindArg
? (MEDIA_KINDS.find((k) => k === kindArg) ??
Object.values(KINDS).find((c) => c.name === kindArg)?.kind)
: undefined;
if (kindArg && !kind) throw new Error(`unknown --kind ${kindArg}`);
const { rows, errors } = parseImport(JSON.parse(await readFile(file, 'utf8')), kind);
const byKind = Object.groupBy(rows, (r) => r.kind);
console.log(
`parsed ${rows.length} entries: ${
Object.entries(byKind)
.map(([k, v]) => `${k} ${v?.length ?? 0}`)
.join(', ') || 'none'
}`
);
if (errors.length) {
for (const e of errors.slice(0, 50)) console.error(` ${e}`);
console.error(`${errors.length} invalid entries; nothing imported`);
process.exit(1);
}
if (dry || !rows.length) process.exit(0);
if (!process.env.DB_URL)
throw new Error('DB_URL is required (use scripts/media-run.sh on stan-ovh)');
const sql = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} });
try {
const r = await importRows(sql, rows);
console.log(`imported ${r.total}: ${r.created} new, ${r.updated} updated`);
} finally {
await sql.end();
}
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Run scripts/media-import.ts on stan-ovh against prod, without printing secrets.
# scripts/media-run.sh ~/imports/webtoons.json --kind webtoons [--dry-run]
# DB_URL (app role) comes from the running app container (db:5432 → 127.0.0.1:5440).
set -euo pipefail
cd "$(dirname "$0")/.."
DB_URL=$(sudo -n docker inspect personal-website-app-1 --format '{{range .Config.Env}}{{println .}}{{end}}' \
| sed -n 's/^DB_URL=//p' | sed 's/@db:5432/@127.0.0.1:5440/')
[ -n "$DB_URL" ] || { echo "no DB_URL from the app container" >&2; exit 1; }
export DB_URL
exec bun scripts/media-import.ts "$@"