summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/scrapers/index.ts2
-rw-r--r--app/scrapers/nepo-kichijoji.ts105
2 files changed, 107 insertions, 0 deletions
diff --git a/app/scrapers/index.ts b/app/scrapers/index.ts
index 79e8125..e6a364b 100644
--- a/app/scrapers/index.ts
+++ b/app/scrapers/index.ts
@@ -27,6 +27,7 @@ import { scraper as ripsHachioji } from "./rips-hachioji";
import { scraper as matchvoxHachioji } from "./matchvox-hachioji";
import { scraper as marzShinjuku } from "./marz-shinjuku";
import { scraper as ufoclubHigashikoenji } from "./ufoclub-higashikoenji";
+import { scraper as nepoKichijoji } from "./nepo-kichijoji";
export const ALL_SCRAPERS: Scraper[] = [
liquidRoom,
@@ -53,6 +54,7 @@ export const ALL_SCRAPERS: Scraper[] = [
matchvoxHachioji,
marzShinjuku,
ufoclubHigashikoenji,
+ nepoKichijoji,
];
export type { Scraper } from "./base";
diff --git a/app/scrapers/nepo-kichijoji.ts b/app/scrapers/nepo-kichijoji.ts
new file mode 100644
index 0000000..afab998
--- /dev/null
+++ b/app/scrapers/nepo-kichijoji.ts
@@ -0,0 +1,105 @@
+/**
+ * 吉祥寺 NEPO — https://nepo.co.jp/schedules/index/YYYY/MM/
+ *
+ * 月別一覧の構造:
+ * <div class="grid-item sche-item get-h">
+ * <a href="/schedules/view/ID">
+ * <div class="sche-img-inner"><img src="/img/Schedule/ID/image/..."></div>
+ * <p class="sche-day">DD</p>
+ * <dl class="inline-list"><dt>Open</dt><dt>Start</dt><dd>HH:MM</dd><dd>HH:MM</dd></dl>
+ * <h4>タイトル</h4>
+ * <dl class="inline-list lineup"><dt>Line Up</dt><dd>出演者</dd>...</dl>
+ * </a>
+ * </div>
+ *
+ * 料金は詳細ページにしかないため取得しない(1ページ数秒かかり全体が遅くなる)。
+ * チケット予約は /contacts/index/ID?tab=booking のフォーム。
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+const BASE = "https://nepo.co.jp";
+
+export const venue: VenueMeta = {
+ id: "nepo-kichijoji",
+ name: "吉祥寺 NEPO",
+ url: BASE,
+ area: "吉祥寺",
+};
+
+async function scrapeMonth(year: number, month: number): Promise<EventInput[]> {
+ const mm = String(month).padStart(2, "0");
+ const res = await fetch(`${BASE}/schedules/index/${year}/${mm}/`);
+ if (!res.ok) return [];
+ const $ = cheerio.load(await res.text());
+ const events: EventInput[] = [];
+
+ $(".grid-item.sche-item").each((_, el) => {
+ const $el = $(el);
+
+ const day = $el.find("p.sche-day").first().text().trim();
+ if (!/^\d{1,2}$/.test(day)) return;
+ const date = `${year}-${mm}-${day.padStart(2, "0")}`;
+
+ const title = $el.find("h4").first().text().trim();
+ if (!title) return;
+
+ const href = $el.find("a[href]").first().attr("href");
+ const source_url = href ? new URL(href, BASE).href : null;
+ const eventId = href?.match(/\/schedules\/view\/(\d+)/)?.[1];
+
+ const times = $el
+ .find("dl.inline-list:not(.lineup) dd")
+ .map((_, dd) => $(dd).text().trim())
+ .get();
+
+ const artists = $el
+ .find("dl.lineup dd")
+ .map((_, dd) => $(dd).text().trim())
+ .get()
+ .filter(Boolean);
+
+ const img = $el.find(".sche-img-inner img").attr("src");
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ artist: artists.length > 0 ? artists.join(" / ") : null,
+ date,
+ open_time: isTime(times[0]) ? times[0] : null,
+ start_time: isTime(times[1]) ? times[1] : null,
+ price: null,
+ ticket_url: eventId ? `${BASE}/contacts/index/${eventId}?tab=booking` : null,
+ image_url: img ? new URL(img, BASE).href : null,
+ source_url,
+ });
+ });
+
+ return events;
+}
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ // Scrape window is ~65 days, so cover this month + next two
+ const now = new Date();
+ const months = [0, 1, 2].map((offset) => {
+ const d = new Date(now.getFullYear(), now.getMonth() + offset, 1);
+ return { year: d.getFullYear(), month: d.getMonth() + 1 };
+ });
+ const perMonth = await Promise.all(months.map(({ year, month }) => scrapeMonth(year, month)));
+
+ const seen = new Set<string>();
+ return perMonth.flat().filter((e) => {
+ const key = `${e.date}|${e.title}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ },
+};
+
+function isTime(s: string | undefined): s is string {
+ return !!s && /^\d{1,2}:\d{2}$/.test(s);
+}