/**
* 吉祥寺 NEPO — https://nepo.co.jp/schedules/index/YYYY/MM/
*
* 月別一覧の構造:
*
*
* 料金は詳細ページにしかないため取得しない(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 {
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 {
// 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();
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);
}