summaryrefslogtreecommitdiff
path: root/app/scrapers/club-quattro.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/scrapers/club-quattro.ts')
-rw-r--r--app/scrapers/club-quattro.ts78
1 files changed, 78 insertions, 0 deletions
diff --git a/app/scrapers/club-quattro.ts b/app/scrapers/club-quattro.ts
new file mode 100644
index 0000000..ae903bc
--- /dev/null
+++ b/app/scrapers/club-quattro.ts
@@ -0,0 +1,78 @@
+/**
+ * Club Quattro 渋谷 — https://www.club-quattro.com/shibuya/schedule/
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "club-quattro",
+ name: "CLUB QUATTRO",
+ url: "https://www.club-quattro.com",
+ area: "渋谷",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://www.club-quattro.com/shibuya/schedule/");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const events: EventInput[] = [];
+
+ $(".schedule-list__item, .c-event, li.event").each((_, el) => {
+ const $el = $(el);
+
+ const title = $el.find(".schedule-list__title, .event-name, h3, h2").first().text().trim();
+ if (!title) return;
+
+ const rawDate =
+ $el.find(".schedule-list__date, .event-date, time").first().text().trim() ||
+ $el.find("time").attr("datetime") ||
+ "";
+ const date = parseJapaneseDate(rawDate);
+ if (!date) return;
+
+ const timeText = $el.find(".schedule-list__time, .time-info").first().text();
+ const openMatch = timeText.match(/OPEN[:: ]*(\d{2}:\d{2})/i);
+ const startMatch = timeText.match(/START[:: ]*(\d{2}:\d{2})/i);
+
+ const detailHref = $el.find("a[href]").first().attr("href") ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist: $el.find(".schedule-list__artist, .artist-name").first().text().trim() || null,
+ date,
+ open_time: openMatch?.[1] ?? null,
+ start_time: startMatch?.[1] ?? null,
+ ticket_url:
+ $el.find("a[href*='eplus'], a[href*='pia'], a[href*='ticket']").first().attr("href") ?? null,
+ image_url: $el.find("img").first().attr("src")
+ ? absoluteUrl($el.find("img").first().attr("src")!, venue.url)
+ : null,
+ source_url: detailHref ? absoluteUrl(detailHref, venue.url) : null,
+ });
+ });
+
+ return events;
+ },
+};
+
+function parseJapaneseDate(raw: string): string | null {
+ const m =
+ raw.match(/(\d{4})[./年](\d{1,2})[./月](\d{1,2})/) ||
+ raw.match(/(\d{1,2})[./月](\d{1,2})/);
+ if (!m) return null;
+ if (m.length === 4) {
+ return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
+ }
+ const year = new Date().getFullYear();
+ return `${year}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
+}
+
+function absoluteUrl(url: string, base: string): string {
+ if (url.startsWith("http")) return url;
+ return url.startsWith("/") ? base + url : `${base}/${url}`;
+}