media: webtoon metadata (English title, artist, synopsis, years, adaptation) and stored covers with a cover grid
Deploy production / deploy (push) Successful in 1m24s
Deploy production / deploy (push) Successful in 1m24s
This commit is contained in:
1 parent
96d7333fe9
commit
3a2d3cc78c
10 files changed
+46302
-22
No files matched your search
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Store cover images for media.items in media.covers (served by /trackers/media/covers/[id], so
|
||||
* the page never hotlinks). Idempotent: a re-run replaces the image for the same item.
|
||||
*
|
||||
* On stan-ovh (DB_URL is loaded for you, nothing printed):
|
||||
* scripts/media-run.sh covers ~/imports/webtoon-covers/manifest.json --kind webtoons [--dry-run]
|
||||
* Manifest: [{ "title": "치즈인더트랩", "file": "720121.jpg", "source_url": "https://…" }], files
|
||||
* relative to the manifest. Rows are found by the import's match key (kind + normalised title).
|
||||
*/
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { matchKey } from '../src/lib/server/media/store';
|
||||
import { KINDS, MEDIA_KINDS, type MediaKind } from '../src/lib/media/kinds';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const i = args.indexOf('--kind');
|
||||
const kindArg = i >= 0 ? args.splice(i, 2)[1] : 'webtoon';
|
||||
const dry = args.includes('--dry-run');
|
||||
const file = args.find((a) => !a.startsWith('--'));
|
||||
if (!file) throw new Error('usage: media-covers.ts <manifest.json> [--kind webtoons] [--dry-run]');
|
||||
const kind: MediaKind | undefined =
|
||||
MEDIA_KINDS.find((k) => k === kindArg) ??
|
||||
Object.values(KINDS).find((c) => c.name === kindArg)?.kind;
|
||||
if (!kind) throw new Error(`unknown --kind ${kindArg}`);
|
||||
|
||||
const MIME: Record<string, string> = {
|
||||
ffd8ff: 'image/jpeg',
|
||||
'89504e': 'image/png',
|
||||
'524946': 'image/webp',
|
||||
'474946': 'image/gif'
|
||||
};
|
||||
type Entry = { title: string; file: string; source_url?: string };
|
||||
const list = JSON.parse(await readFile(file, 'utf8')) as Entry[];
|
||||
const base = dirname(resolve(file));
|
||||
const items = [];
|
||||
for (const e of list) {
|
||||
const bytes = await readFile(resolve(base, e.file));
|
||||
const mime = MIME[bytes.subarray(0, 3).toString('hex')];
|
||||
if (!mime || bytes.length > 2_000_000) throw new Error(`${e.title}: not an image or too big`);
|
||||
items.push({ ...e, bytes, mime });
|
||||
}
|
||||
console.log(`read ${items.length} covers`);
|
||||
if (dry) process.exit(0);
|
||||
if (!process.env.DB_URL) throw new Error('DB_URL is required (use scripts/media-run.sh covers …)');
|
||||
const sql = postgres(process.env.DB_URL, { max: 1, onnotice: () => {} });
|
||||
let stored = 0;
|
||||
const missing: string[] = [];
|
||||
try {
|
||||
await sql.begin(async (tx) => {
|
||||
for (const c of items) {
|
||||
const [row] =
|
||||
await tx`select id from media.items where match_key = ${matchKey(kind, c.title)}`;
|
||||
if (!row) {
|
||||
missing.push(c.title);
|
||||
continue;
|
||||
}
|
||||
await tx`insert into media.covers (item_id, mime, bytes, source_url)
|
||||
values (${row.id}, ${c.mime}, ${c.bytes}, ${c.source_url ?? null})
|
||||
on conflict (item_id) do update set mime = excluded.mime, bytes = excluded.bytes,
|
||||
source_url = excluded.source_url, updated_at = now()`;
|
||||
stored++;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
console.log(
|
||||
`stored ${stored} covers${missing.length ? `; no item for: ${missing.join(', ')}` : ''}`
|
||||
);
|
||||
Reference in new issue
Block a user