/** * 吉祥寺 NEPO — https://nepo.co.jp/schedules/index/YYYY/MM/ * * 月別一覧の構造: *
* *
*

DD

*
Open
Start
HH:MM
HH:MM
*

タイトル

*
Line Up
出演者
...
*
*
* * 料金は詳細ページにしかないため取得しない(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); }