From e9e576abd9d6c6030aa4bb290e869890831488ad Mon Sep 17 00:00:00 2001 From: yyamashita Date: Mon, 11 May 2026 00:06:52 +0900 Subject: Add lists feature (band recommendation lists with history) New lists, list_entries, list_revisions tables; full CRUD routes under /lists; nav link in root. Co-Authored-By: Claude Sonnet 4.6 --- scripts/seed-mosquitone.ts | 137 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 scripts/seed-mosquitone.ts (limited to 'scripts/seed-mosquitone.ts') diff --git a/scripts/seed-mosquitone.ts b/scripts/seed-mosquitone.ts new file mode 100644 index 0000000..e3d532d --- /dev/null +++ b/scripts/seed-mosquitone.ts @@ -0,0 +1,137 @@ +#!/usr/bin/env npx tsx +/** + * Seeds a "mosquitone と共演したバンド" list by fetching Gatsby page-data JSON. + * Usage: npx tsx scripts/seed-mosquitone.ts + * Override DB path: DB_PATH=/path/to/whois.db npx tsx scripts/seed-mosquitone.ts + */ + +import { createBandList, getBandListBySlug, toSlug } from "../app/lib/db.server"; + +interface LiveEvent { + date: string; + venueName: string; + description: string; + title: string; + published?: boolean; + [key: string]: unknown; +} + +interface PageData { + result?: { + data?: { + amplify?: { + listEvents?: { + items: LiveEvent[]; + }; + }; + }; + }; +} + +const PAGE_DATA_URL = "https://www.mosquit.one/page-data/live/page-data.json"; + +async function main() { + console.log("Fetching mosquit.one live data..."); + const res = await fetch(PAGE_DATA_URL); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const json = await res.json() as PageData; + + const items = json?.result?.data?.amplify?.listEvents?.items ?? []; + console.log(`Found ${items.length} events`); + + // band_name → [{ date, venue }] + const coPerformers = new Map(); + + for (const event of items) { + const rawDesc = event.description ?? ""; + const date = event.date ?? ""; + const venue = (event.venueName ?? "").trim(); + + // Split on actual newlines or literal \n, then find the performer line + // (some events have OPEN/START on the first line and performers on the second) + const lines = rawDesc.split(/\n|\\n/); + const perfLine = lines.find((l) => + !l.includes("://") && ( // skip URL-containing lines + l.includes("//") || (l.split("/").length > 2 && !/^\d/.test(l.trim())) + ) + ); + if (!perfLine) continue; + + // Remove "出演:" / "出演者" prefix + const cleaned = perfLine.replace(/^(出演者?[::]?\s*)/u, "").trim(); + + // Must still contain // or " / " after cleanup + if (!cleaned.includes("//") && !cleaned.includes(" / ")) continue; + + // Split by " // " or " / " + const parts = cleaned.split(/\s*\/\/\s*|\s*\/\s*/); + + for (const raw of parts) { + // Remove parenthetical suffixes like "(東京)" "(いわき)" + let name = raw.replace(/[\((][^))]*[\))]/gu, "").trim(); + // Remove any remaining literal \n and opening-paren-without-close + name = name.replace(/\\n/g, "").replace(/[\((].*$/, "").trim(); + // Remove stray closing parentheses + name = name.replace(/[\))]/g, "").trim(); + + // Validation filters + if (!name || name.length < 2) continue; + if (/mosquitone/iu.test(name)) continue; + if (/^\d/.test(name)) continue; // starts with number (dates etc.) + if (/\d{1,2}:\d{2}/.test(name)) continue; // contains time HH:MM + if (/^(OPEN|START)/i.test(name)) continue; + if (/出演|二日間|開催|開場/.test(name)) continue; + if (/\.(com|net|jp|org|co)\b/.test(name)) continue; // domain names + if (name.length > 60) continue; + + const appearances = coPerformers.get(name) ?? []; + appearances.push({ date, venue }); + coPerformers.set(name, appearances); + } + } + + console.log(`Parsed ${coPerformers.size} co-performers`); + + // Build entries sorted by band name + const entries = [...coPerformers.entries()] + .sort(([a], [b]) => a.localeCompare(b, "ja")) + .map(([band_name, appearances]) => { + appearances.sort((a, b) => a.date.localeCompare(b.date)); + const note = appearances + .map(({ date, venue }) => { + const displayDate = date.replace(/-/g, "/"); + return venue ? `${displayDate} に ${venue} で共演` : `${displayDate} に共演`; + }) + .join("、"); + return { band_name, note }; + }); + + const title = "mosquitone と共演したバンド"; + const slug = toSlug(title); + + // Check for existing list + const existing = getBandListBySlug(slug); + if (existing) { + console.log(`List already exists (slug: "${slug}"), skipping.`); + console.log(` url: /lists/of/${existing.id}`); + return; + } + + const id = crypto.randomUUID(); + const list = createBandList({ + id, + slug, + title, + description: "mosquitone の共演バンドを自動収集したリストです。", + entries, + message: "seed-mosquitone スクリプトによる自動生成", + ip_address: "cli", + }); + + console.log(`Created list: ${list.title}`); + console.log(` id: ${list.id}`); + console.log(` entries: ${entries.length}件`); + console.log(` url: /lists/of/${list.id}`); +} + +main().catch((e) => { console.error(e); process.exit(1); }); -- cgit v1.2.3