summaryrefslogtreecommitdiff
path: root/scripts/add.ts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/add.ts')
-rw-r--r--scripts/add.ts159
1 files changed, 159 insertions, 0 deletions
diff --git a/scripts/add.ts b/scripts/add.ts
new file mode 100644
index 0000000..ca53dda
--- /dev/null
+++ b/scripts/add.ts
@@ -0,0 +1,159 @@
+#!/usr/bin/env npx tsx
+/**
+ * CLI for adding bands/artists directly to the DB (no server required).
+ *
+ * Usage:
+ * npx tsx scripts/add.ts band --name "Name" [--slug x] [--area X] [--status active|hiatus|disbanded] [--description X] [--message X]
+ * npx tsx scripts/add.ts artist --name "Name" [--slug x] [--message X]
+ * npx tsx scripts/add.ts import --file data.json
+ *
+ * JSON import format (data.json):
+ * {
+ * "artists": [{ "name": "...", "links": [{ "label": "Twitter", "url": "..." }] }],
+ * "bands": [{
+ * "name": "...", "area": "...", "status": "active",
+ * "artists": [{ "name": "...", "role": "Vocal" }],
+ * "links": [{ "label": "Twitter", "url": "..." }]
+ * }]
+ * }
+ *
+ * Artist references in bands can use { "id": "<uuid>" } or { "name": "..." } (looked up by name).
+ * Artists listed under "artists" are created first so band references by name work.
+ * Run from the project root so whois.db is found automatically.
+ */
+
+import { readFileSync } from "fs";
+import { createArtist, createBand, listArtists, toSlug } from "../app/lib/db.server";
+
+type LinkInput = { label: string; url: string };
+type ArtistRef = { id?: string; name?: string; role?: string | null };
+
+function parseFlags(argv: string[]): Record<string, string> {
+ const flags: Record<string, string> = {};
+ for (let i = 0; i < argv.length; i++) {
+ if (argv[i].startsWith("--")) {
+ const key = argv[i].slice(2);
+ flags[key] = argv[i + 1] ?? "true";
+ i++;
+ }
+ }
+ return flags;
+}
+
+const [, , command, ...rest] = process.argv;
+
+if (!command || command === "--help" || command === "-h") {
+ console.log(`Usage:
+ npx tsx scripts/add.ts band --name "Name" [--slug x] [--area X] [--status active|hiatus|disbanded] [--description X] [--message X]
+ npx tsx scripts/add.ts artist --name "Name" [--slug x] [--message X]
+ npx tsx scripts/add.ts import --file data.json`);
+ process.exit(0);
+}
+
+if (command === "artist") {
+ const flags = parseFlags(rest);
+ const name = flags.name?.trim();
+ if (!name) { console.error("Error: --name is required"); process.exit(1); }
+ const slug = flags.slug?.trim() || toSlug(name);
+ const id = crypto.randomUUID();
+ const artist = createArtist({
+ id, slug, name,
+ links: flags.links ? (JSON.parse(flags.links) as LinkInput[]) : [],
+ message: flags.message || "CLI import",
+ ip_address: "cli",
+ });
+ console.log(`Created artist: ${artist.name}`);
+ console.log(` id: ${artist.id}`);
+ console.log(` url: /artists/of/${artist.id}`);
+
+} else if (command === "band") {
+ const flags = parseFlags(rest);
+ const name = flags.name?.trim();
+ if (!name) { console.error("Error: --name is required"); process.exit(1); }
+ const slug = flags.slug?.trim() || toSlug(name);
+ const id = crypto.randomUUID();
+ try {
+ const band = createBand({
+ id, slug, name,
+ area: flags.area || null,
+ description: flags.description || null,
+ status: flags.status || "active",
+ links: flags.links ? (JSON.parse(flags.links) as LinkInput[]) : [],
+ artists: flags.artists ? (JSON.parse(flags.artists) as { id: string; role: string | null }[]) : [],
+ message: flags.message || "CLI import",
+ ip_address: "cli",
+ });
+ console.log(`Created band: ${band.name}`);
+ console.log(` id: ${band.id}`);
+ console.log(` url: /bands/of/${band.id}`);
+ } catch (e) {
+ if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) {
+ console.error(`Error: slug "${slug}" is already in use`);
+ process.exit(1);
+ }
+ throw e;
+ }
+
+} else if (command === "import") {
+ const flags = parseFlags(rest);
+ if (!flags.file) { console.error("Error: --file is required"); process.exit(1); }
+
+ const data = JSON.parse(readFileSync(flags.file, "utf-8")) as {
+ artists?: { name: string; slug?: string; links?: LinkInput[]; message?: string }[];
+ bands?: { name: string; slug?: string; area?: string; description?: string; status?: string; links?: LinkInput[]; artists?: ArtistRef[]; message?: string }[];
+ };
+
+ // Build name→id map from existing artists, then create new ones
+ const nameToId = new Map<string, string>(listArtists().map((a) => [a.name, a.id]));
+
+ for (const a of data.artists ?? []) {
+ const name = a.name?.trim();
+ if (!name) continue;
+ if (nameToId.has(name)) { console.log(`Skip existing artist: ${name}`); continue; }
+ const id = crypto.randomUUID();
+ createArtist({
+ id, slug: a.slug?.trim() || toSlug(name), name,
+ links: a.links || [],
+ message: a.message || "JSON import",
+ ip_address: "cli",
+ });
+ nameToId.set(name, id);
+ console.log(`Created artist: ${name} (${id})`);
+ }
+
+ for (const b of data.bands ?? []) {
+ const name = b.name?.trim();
+ if (!name) continue;
+ const id = crypto.randomUUID();
+ const artists = (b.artists ?? [])
+ .map((ar) => {
+ const artistId = ar.id ?? (ar.name ? nameToId.get(ar.name) : undefined);
+ if (!artistId) { console.warn(` Warning: artist not found: "${ar.name}"`); return null; }
+ return { id: artistId, role: ar.role ?? null };
+ })
+ .filter((x): x is { id: string; role: string | null } => x !== null);
+ try {
+ createBand({
+ id, slug: b.slug?.trim() || toSlug(name), name,
+ area: b.area || null,
+ description: b.description || null,
+ status: b.status || "active",
+ links: b.links || [],
+ artists,
+ message: b.message || "JSON import",
+ ip_address: "cli",
+ });
+ console.log(`Created band: ${name} (${id})`);
+ } catch (e) {
+ if (e instanceof Error && e.message.includes("UNIQUE constraint failed: bands.slug")) {
+ console.error(`Skip band "${name}": slug already in use`);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+} else {
+ console.error(`Unknown command: "${command}". Use band, artist, or import.`);
+ process.exit(1);
+}