/** * 新代田 FEVER — https://www.fever-popo.com * * Movable Type CMS。月別 URL: /schedule/YYYY/MM/ * DOM 構造: *
*

26.05.01 (Fri) タイトル

* *

アーティスト1
アーティスト2

*
OPEN HH:MM / START HH:MM
*

ADV ¥XXXX (+1drink) / DOOR ¥XXXX (+1drink)

* ← フライヤー画像 */ 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: "新代田", capacity: 300, }; async function scrapeMonth(yyyymm: string): Promise { 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

in body const $h3 = $el.find("div.asset-body h3").first(); const artist = $h3.find("p").text() .split(/\n|/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 { 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]; }, };