From 999b7a3133fccc3f120ac5ae24c02bb38a4ecce9 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Sat, 26 Sep 2026 00:09:06 +0900 Subject: Add scraper for 吉祥寺NEPO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parses the monthly schedule pages (this month + next two) for date, title, line-up, open/start times and flyer image. Price is only on per-event detail pages, which are slow (~3s each), so it is skipped. Co-Authored-By: Claude Opus 5.5 --- app/scrapers/index.ts | 2 + app/scrapers/nepo-kichijoji.ts | 105 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 app/scrapers/nepo-kichijoji.ts (limited to 'app/scrapers') 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/ + * + * 月別一覧の構造: + * + * + * 料金は詳細ページにしかないため取得しない(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); +} -- cgit v1.3