summaryrefslogtreecommitdiff
path: root/app/scrapers/fever-shindaita.ts
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-07 19:27:50 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-07 19:27:50 +0900
commitd5e975b601e70adf901c8e1eb7e61f0388941195 (patch)
treef1778ff15b6540b44c354cb76c44aac795448c4a /app/scrapers/fever-shindaita.ts
parentbffc2c74408ff7163cea0c0392dfc4b15c620a5f (diff)
Add 5 new venue scrapers; extract artist info for WARP, shibuya-o, MOON STEP, mod
New scrapers: Fever 下北沢, Nine Spices 下北沢, 西荻窪 JAM, mod 柴崎, 中野 MOON STEP Artist extraction added/fixed: - warp-kichijoji: parse div.w-flyer (clone + remove nested notes-wrapper) - shibuya-o: rewrite to scrape each sub-venue; artist from li.p-scheduled-card__artist-item - moon-step-nakano: parse 出演 section from WordPress API description HTML - mod-shibasaki: fetch individual event pages in parallel; handle live:/出演:/・ bullet formats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/scrapers/fever-shindaita.ts')
-rw-r--r--app/scrapers/fever-shindaita.ts117
1 files changed, 117 insertions, 0 deletions
diff --git a/app/scrapers/fever-shindaita.ts b/app/scrapers/fever-shindaita.ts
new file mode 100644
index 0000000..71c31f6
--- /dev/null
+++ b/app/scrapers/fever-shindaita.ts
@@ -0,0 +1,117 @@
+/**
+ * 新代田 FEVER — https://www.fever-popo.com
+ *
+ * Movable Type CMS。月別 URL: /schedule/YYYY/MM/
+ * DOM 構造:
+ * <div class="entry-asset">
+ * <h2 class="eventtitle">26.05.01 (Fri)&nbsp;タイトル</h2>
+ * <meta property="og:url" content="https://www.fever-popo.com/schedule/.../MMDD.html">
+ * <h3><p>アーティスト1<br/>アーティスト2</p></h3>
+ * <div>OPEN HH:MM / START HH:MM</div>
+ * <div><p>ADV ¥XXXX (+1drink) / DOOR ¥XXXX (+1drink)</p></div>
+ * <img class="scpickup" src="..."> ← フライヤー画像
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "fever-shindaita",
+ name: "新代田 FEVER",
+ url: "https://www.fever-popo.com",
+ area: "新代田",
+};
+
+async function scrapeMonth(yyyymm: string): Promise<EventInput[]> {
+ const [year, month] = yyyymm.split("-");
+ const url = `${venue.url}/schedule/${year}/${month}/`;
+ const res = await fetch(url);
+ if (!res.ok) return [];
+ const $ = cheerio.load(await res.text());
+ const events: EventInput[] = [];
+
+ $("div.entry-asset").each((_, el) => {
+ const $el = $(el);
+
+ // Title: "26.05.01 (Fri) タイトル"
+ const h2Text = $el.find("h2.eventtitle").first().text();
+ const titleMatch = h2Text.match(/^\d{2}\.\d{2}\.\d{2}\s+\([A-Za-z]+\)\s*(.+)$/);
+ if (!titleMatch) return;
+ const title = titleMatch[1].trim();
+ if (!title) return;
+
+ // Date from title prefix: "26.05.01"
+ const dateMatch = h2Text.match(/^(\d{2})\.(\d{2})\.(\d{2})/);
+ if (!dateMatch) return;
+ const date = `20${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;
+
+ // Source URL from og:url meta inside the entry
+ const sourceUrl = $el.find("meta[property='og:url']").attr("content") ?? null;
+
+ // Artists: first <h3><p> in body
+ const $h3 = $el.find("div.asset-body h3").first();
+ const artist = $h3.find("p").text()
+ .split(/\n|<br\s*\/?>/i)
+ .map((s) => s.replace(/<[^>]+>/g, "").trim())
+ .filter(Boolean)
+ .join("、") || null;
+
+ // Time: div containing "OPEN" / "START"
+ let openTime: string | null = null;
+ let startTime: string | null = null;
+ $el.find("div.asset-body div").each((_, d) => {
+ const text = $(d).text();
+ if (/OPEN/i.test(text) && /START/i.test(text)) {
+ const om = text.match(/OPEN\s*(\d{1,2}:\d{2})/i);
+ const sm = text.match(/START\s*(\d{1,2}:\d{2})/i);
+ if (om) openTime = om[1];
+ if (sm) startTime = sm[1];
+ }
+ });
+
+ // Price: div after the time div
+ let price: string | null = null;
+ $el.find("div.asset-body div").each((_, d) => {
+ const text = $(d).text().trim();
+ if (/[¥¥]/.test(text) && /(ADV|DOOR|前売|当日)/i.test(text)) {
+ price = text.replace(/\s+/g, " ").split("\n")[0].trim() || null;
+ }
+ });
+
+ // Image
+ const imageUrl = $el.find("img.scpickup").first().attr("src") ?? null;
+
+ // Ticket URL
+ const ticketUrl =
+ $el.find("a[href*='eplus'], a[href*='pia'], a[href*='tiget'], a[href*='livepocket'], a[href*='t-dv.com']")
+ .first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist,
+ date,
+ open_time: openTime,
+ start_time: startTime,
+ price,
+ ticket_url: ticketUrl,
+ image_url: imageUrl,
+ source_url: sourceUrl,
+ });
+ });
+
+ return events;
+}
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const now = new Date();
+ const thisMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
+ const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
+ const nextMonth = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`;
+
+ const [a, b] = await Promise.all([scrapeMonth(thisMonth), scrapeMonth(nextMonth)]);
+ return [...a, ...b];
+ },
+};