/** * 東高円寺二万電圧 — https://den-atsu.com * * WordPress カスタムテーマ。月別スケジュールページ構造: *
■YYYY.M/D(day)
← 日付マーカー *タイトル
← タイトル (複数行あり) *アーティスト名
← 出演者 (複数行あり) *open.HH:MM start.HH:MM\nadv.Nyen door.Nyen\nチケット
*← イベント区切り */ import * as cheerio from "cheerio"; import type { Scraper, VenueMeta } from "./base"; import type { EventInput } from "~/lib/db.server"; export const venue: VenueMeta = { id: "den-atsu", name: "東高円寺二万電圧", url: "https://den-atsu.com", area: "東高円寺", capacity: 130, }; const TICKET_LINK_SELECTOR = 'a[href*="eplus"], a[href*="livepocket"], a[href*="tiget"], a[href*="pia.jp"], a[href*="ticket"]'; function parseHtml(html: string, year: number, month: number): EventInput[] { const $ = cheerio.load(html); const events: EventInput[] = []; const sourceUrl = `https://den-atsu.com/schedule/${year}-${month}-schedule/`; // Collect all
elements under the content section
const paras = $("div.inner p").toArray();
let i = 0;
while (i < paras.length) {
const $p = $(paras[i]);
const text = $p.text().trim();
// Date marker: ■YYYY.M/D(day)
const dateMatch = text.match(/^■(\d{4})\.(\d{1,2})\/(\d{1,2})/);
if (!dateMatch) {
i++;
continue;
}
const date = `${dateMatch[1]}-${dateMatch[2].padStart(2, "0")}-${dateMatch[3].padStart(2, "0")}`;
i++;
// Title: consecutive p.p1 containing red-colored spans
const titleParts: string[] = [];
while (i < paras.length) {
const $cur = $(paras[i]);
if ($cur.find("span[style*='color']").length === 0) break;
const part = $cur.text().trim();
if (part) titleParts.push(part);
i++;
}
const title = titleParts.join(" ").trim();
if (!title) continue;
// Artists: p.p1 or plain p without red spans, not time/price lines
const artistParts: string[] = [];
while (i < paras.length) {
const $cur = $(paras[i]);
const t = $cur.text().trim();
if (!t || t === " ") { i++; break; } // blank separator → done
if (t.match(/^■\d{4}/)) break; // next event
if ($cur.find("span[style*='color']").length > 0) break;
if (t.match(/^open\./i) || t.match(/^adv\./i)) break;
artistParts.push(t);
i++;
}
const artist = artistParts.join("、").trim() || null;
// Info line: open/start times, adv/door prices, ticket link
let openTime: string | null = null;
let startTime: string | null = null;
let price: string | null = null;
let ticketUrl: string | null = null;
while (i < paras.length) {
const $cur = $(paras[i]);
const t = $cur.text().trim();
if (!t || t === " ") { i++; break; }
if (t.match(/^■\d{4}/)) break;
const openMatch = t.match(/open\.(\d{1,2}:\d{2})/i);
const startMatch = t.match(/start\.(\d{1,2}:\d{2})/i);
const advMatch = t.match(/adv\.([\d,]+)yen/i);
const doorMatch = t.match(/door\.([\d,]+)yen/i);
if (openMatch) openTime = openMatch[1];
if (startMatch) startTime = startMatch[1];
if (advMatch && doorMatch) {
price = `前売 ¥${advMatch[1]} / 当日 ¥${doorMatch[1]}`;
} else if (advMatch) {
price = `前売 ¥${advMatch[1]}`;
}
if (!ticketUrl) {
ticketUrl = $cur.find(TICKET_LINK_SELECTOR).first().attr("href") ?? null;
}
i++;
}
events.push({
venue_id: venue.id,
title,
artist,
date,
open_time: openTime,
start_time: startTime,
price,
ticket_url: ticketUrl,
image_url: null,
source_url: sourceUrl,
});
}
return events;
}
export const scraper: Scraper = {
venue,
async scrape(): Promise