#!/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); });