summaryrefslogtreecommitdiff
path: root/app/scrapers/marz-shinjuku.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/scrapers/marz-shinjuku.ts')
-rw-r--r--app/scrapers/marz-shinjuku.ts117
1 files changed, 117 insertions, 0 deletions
diff --git a/app/scrapers/marz-shinjuku.ts b/app/scrapers/marz-shinjuku.ts
new file mode 100644
index 0000000..06c0bcc
--- /dev/null
+++ b/app/scrapers/marz-shinjuku.ts
@@ -0,0 +1,117 @@
+/**
+ * 新宿MARZ — http://www.marz.jp/schedule/YYYY/MM/
+ *
+ * DOM構造:
+ * <article id="eXXXX">
+ * <a href="detail-url"><div class="img"><img src="..."><time datetime="YYYY-MM-DD"></div></a>
+ * <h1><a href="...">タイトル</a></h1>
+ * <div class="desc">
+ * <div class="entrybody"><p>【LIVE】<br/>アーティスト名</p> ...</div>
+ * <div class="entryex"><p>open HH:MM / start HH:MM<br/>adv¥XXX / door¥XXX<br/>...</p></div>
+ * </div>
+ * </article>
+ */
+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<EventInput[]> {
+ const url = `http://www.marz.jp/schedule/${year}/${String(month).padStart(2, "0")}/`;
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
+ 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 <br> 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<EventInput[]> {
+ 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<string>();
+ return [...thisMonth, ...nextMonth].filter((e) => {
+ const key = `${e.date}|${e.title}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ },
+};