diff options
| author | yyamashita <yyamashita@mosquit.one> | 2026-05-06 22:07:53 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@mosquit.one> | 2026-05-06 22:07:53 +0900 |
| commit | be55729482296663da8c96723bfd22080e6762c1 (patch) | |
| tree | fcd94b1dc5c55f3a80796c90a555863d13fc9a95 /app/lib/markdown-writer.server.ts | |
| parent | 014b29bc22b1c207a03dd560051ecdd5df63f0b1 (diff) | |
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 <noreply@anthropic.com>
Diffstat (limited to 'app/lib/markdown-writer.server.ts')
| -rw-r--r-- | app/lib/markdown-writer.server.ts | 80 |
1 files changed, 80 insertions, 0 deletions
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/<venue-id>.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, " "); +} |
