/** * 新宿MARZ — http://www.marz.jp/schedule/YYYY/MM/ * * DOM構造: *
*
*

タイトル

*
*

【LIVE】
アーティスト名

...
*

open HH:MM / start HH:MM
adv¥XXX / door¥XXX
...

*
*
*/ import * as cheerio from "cheerio"; import type { Scraper, VenueMeta } from "./base"; import type { EventInput } from "~/lib/db.server"; export const venue: VenueMeta = { id: "marz-shinjuku", name: "新宿MARZ", url: "http://www.marz.jp", area: "新宿", capacity: 200, }; async function scrapeMonth(year: number, month: number): Promise { const url = `http://www.marz.jp/schedule/${year}/${String(month).padStart(2, "0")}/`; const res = await fetch(url); // 404/500 = schedule not yet published for that month if (!res.ok) return []; const $ = cheerio.load(await res.text()); const events: EventInput[] = []; $("article").each((_, el) => { const $el = $(el); const date = $el.find("time[datetime]").attr("datetime"); if (!date) return; const title = $el.find("h1 a").first().text().trim(); if (!title) return; const source_url = $el.find("a[href]").first().attr("href") ?? null; const image_url = $el.find(".img img").attr("src") ?? null; const ticket_url = $el .find(".entrybody a[href]") .filter((_, a) => { const h = $(a).attr("href") ?? ""; return h.startsWith("http") && !h.includes("marz.jp"); }) .first() .attr("href") ?? null; // Artist lines: clone entrybody, replace
with \n, filter out labels/ticket lines const $body = $el.find(".entrybody").clone(); $body.find("br").replaceWith("\n"); const artistLines = $body .text() .split("\n") .map((l) => l.trim()) .filter( (l) => l && !l.includes("【") && !l.startsWith("■") && !l.startsWith("※") ); const artist = artistLines.length > 0 ? artistLines.join(" / ") : null; // Time and price from entryex const $ex = $el.find(".entryex").clone(); $ex.find("br").replaceWith("\n"); const exLines = $ex .text() .split("\n") .map((l) => l.trim()) .filter(Boolean); const timeLine = exLines[0] ?? ""; const openMatch = timeLine.match(/open\s+(\d{1,2}:\d{2})/i); const startMatch = timeLine.match(/start\s+(\d{1,2}:\d{2})/i); const priceLine = exLines.find((l, i) => i > 0 && /[¥¥]|adv|door/i.test(l)) ?? null; events.push({ venue_id: venue.id, title, artist, date, open_time: openMatch?.[1] ?? null, start_time: startMatch?.[1] ?? null, price: priceLine, ticket_url, image_url, source_url, }); }); return events; } export const scraper: Scraper = { venue, async scrape(): Promise { const now = new Date(); const next = new Date(now.getFullYear(), now.getMonth() + 1, 1); const [thisMonth, nextMonth] = await Promise.all([ scrapeMonth(now.getFullYear(), now.getMonth() + 1), scrapeMonth(next.getFullYear(), next.getMonth() + 1), ]); const seen = new Set(); return [...thisMonth, ...nextMonth].filter((e) => { const key = `${e.date}|${e.title}`; if (seen.has(key)) return false; seen.add(key); return true; }); }, };