From be55729482296663da8c96723bfd22080e6762c1 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Wed, 6 May 2026 22:07:53 +0900 Subject: Add Tokyo livehouse event aggregator service Full-stack React Router v7 app that scrapes event listings from major Tokyo live venues (Liquid Room, WWW/WWW X, Shibuya O-EAST, Shinjuku LOFT, Club Quattro) and stores them in SQLite for browsing and search. - Modular scraper architecture: add a new venue by dropping a file in app/scrapers/ and registering it in index.ts - Routes: /events (filter by keyword/venue/date), /events/:id, /venues, GET /api/scrape - EventCard shows artist, date/time, venue, ticket URL, and fee - Post-scrape per-venue Markdown files generated to events/ (dev reference) - /add-livehouse Claude Code skill defined in .claude/commands/ Co-Authored-By: Claude Sonnet 4.6 --- app/lib/markdown-writer.server.ts | 80 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/lib/markdown-writer.server.ts (limited to 'app/lib/markdown-writer.server.ts') diff --git a/app/lib/markdown-writer.server.ts b/app/lib/markdown-writer.server.ts new file mode 100644 index 0000000..cfef315 --- /dev/null +++ b/app/lib/markdown-writer.server.ts @@ -0,0 +1,80 @@ +/** + * Generates a Markdown summary file per venue after scraping. + * Files are written to events/.md in the project root. + */ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { queryEvents } from "./db.server"; +import type { Event } from "./db.server"; + +const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "../../"); +const EVENTS_DIR = path.join(ROOT, "events"); + +export function generateVenueMarkdown(venueId: string): void { + const events = queryEvents({ venue_id: venueId, limit: 200 }); + if (events.length === 0) return; + + fs.mkdirSync(EVENTS_DIR, { recursive: true }); + + const venueName = events[0].venue_name; + const venueArea = events[0].venue_area ?? ""; + const now = new Date().toISOString().slice(0, 10); + + const lines: string[] = [ + `# ${venueName}(${venueArea})イベント情報`, + ``, + `> 最終更新: ${now} `, + `> データソース: スクレイパー自動取得`, + ``, + `| 日付 | 出演者 | タイトル | 時間 | 料金 | URL |`, + `| ---- | ------ | -------- | ---- | ---- | --- |`, + ]; + + for (const ev of events) { + const date = formatDate(ev.date); + const artist = escape(ev.artist ?? "未定"); + const title = escape(ev.title); + const time = buildTime(ev.open_time, ev.start_time); + const fee = escape(ev.price ?? ""); + const url = ev.ticket_url + ? `[チケット](${ev.ticket_url})` + : ev.source_url + ? `[詳細](${ev.source_url})` + : ""; + + lines.push(`| ${date} | ${artist} | ${title} | ${time} | ${fee} | ${url} |`); + } + + lines.push(``); + lines.push(`---`); + lines.push(`*このファイルは自動生成されます。手動編集は次回更新時に上書きされます。*`); + lines.push(``); + + const filePath = path.join(EVENTS_DIR, `${venueId}.md`); + fs.writeFileSync(filePath, lines.join("\n"), "utf-8"); +} + +export function generateAllVenueMarkdown(venueIds: string[]): void { + for (const id of venueIds) { + generateVenueMarkdown(id); + } +} + +function formatDate(iso: string): string { + const [y, m, d] = iso.split("-"); + const days = ["日", "月", "火", "水", "木", "金", "土"]; + const dayIdx = new Date(`${iso}T00:00:00`).getDay(); + return `${y}/${m}/${d}(${days[dayIdx]})`; +} + +function buildTime(open: string | null, start: string | null): string { + const parts: string[] = []; + if (open) parts.push(`OPEN ${open}`); + if (start) parts.push(`START ${start}`); + return parts.join(" / ") || ""; +} + +function escape(s: string): string { + return s.replace(/\|/g, "\\|").replace(/\n/g, " "); +} -- cgit v1.2.3