trackers: media hub with webtoons, podcasts and videos; shows and anime filters
Deploy production / deploy (push) Successful in 1m22s
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:
1 parent
4ef75f9d25
commit
079399635d
33 files changed
+48352
-44
No files matched your search
@@ -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();
|
||||
}
|
||||
Executable
+11
@@ -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 "$@"
|
||||
Reference in new issue
Block a user