From d5e975b601e70adf901c8e1eb7e61f0388941195 Mon Sep 17 00:00:00 2001 From: yyamashita Date: Thu, 7 May 2026 19:27:50 +0900 Subject: Add 5 new venue scrapers; extract artist info for WARP, shibuya-o, MOON STEP, mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New scrapers: Fever 下北沢, Nine Spices 下北沢, 西荻窪 JAM, mod 柴崎, 中野 MOON STEP Artist extraction added/fixed: - warp-kichijoji: parse div.w-flyer (clone + remove nested notes-wrapper) - shibuya-o: rewrite to scrape each sub-venue; artist from li.p-scheduled-card__artist-item - moon-step-nakano: parse 出演 section from WordPress API description HTML - mod-shibasaki: fetch individual event pages in parallel; handle live:/出演:/・ bullet formats Co-Authored-By: Claude Sonnet 4.6 --- app/scrapers/nine-spices.ts | 94 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 app/scrapers/nine-spices.ts (limited to 'app/scrapers/nine-spices.ts') diff --git a/app/scrapers/nine-spices.ts b/app/scrapers/nine-spices.ts new file mode 100644 index 0000000..f4afa3d --- /dev/null +++ b/app/scrapers/nine-spices.ts @@ -0,0 +1,94 @@ +/** + * Nine Spices (新宿) — https://9spices.rinky.info/schedule/ + * + * WordPress ベースの独自テーマ。構造: + *
+ *

タイトル

+ *
+ *
アーティスト
+ *
OPENhh:mm
START...
+ *
ADV¥XXX
+ */ +import * as cheerio from "cheerio"; +import type { Scraper, VenueMeta } from "./base"; +import type { EventInput } from "~/lib/db.server"; + +export const venue: VenueMeta = { + id: "nine-spices", + name: "Nine Spices", + url: "https://9spices.rinky.info", + area: "新宿", +}; + +export const scraper: Scraper = { + venue, + async scrape(): Promise { + const res = await fetch("https://9spices.rinky.info/schedule/"); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const $ = cheerio.load(await res.text()); + const events: EventInput[] = []; + + $("div[class*='event-cont-par']").each((_, el) => { + const $el = $(el); + + // class="event-cont-par 2026-05-01" → extract date + const classAttr = $el.attr("class") ?? ""; + const dateMatch = classAttr.match(/(\d{4}-\d{2}-\d{2})/); + if (!dateMatch) return; + const date = dateMatch[1]; + + const $titleLink = $el.find("h3.event-title a").first(); + const title = $titleLink.text().trim(); + if (!title) return; + + const sourceUrl = $titleLink.attr("href") ?? null; + + const artist = $el.find("span.actlist-name") + .map((_, s) => $(s).text().trim()) + .get() + .join("、") || null; + + //
OPEN18:30
... + let openTime: string | null = null; + let startTime: string | null = null; + $el.find("div.sch-time div").each((_, row) => { + const spans = $(row).find("span"); + const label = spans.eq(0).text().trim().toUpperCase(); + const value = spans.eq(1).text().trim(); + if (label === "OPEN") openTime = value || null; + if (label === "START") startTime = value || null; + }); + + //
ADV¥2,500
... + const priceParts: string[] = []; + $el.find("div.sch-price div").each((_, row) => { + const spans = $(row).find("span"); + const label = spans.eq(0).text().trim(); + const value = spans.eq(1).text().trim(); + if (label && value) priceParts.push(`${label} ${value}`); + }); + const price = priceParts.length ? priceParts.join(" / ") : null; + + const imageUrl = $el.find("img.wp-post-image").first().attr("src") ?? null; + + const ticketUrl = + $el.find("a[href*='livepocket'], a[href*='eplus'], a[href*='pia'], a[href*='tiget'], a[href*='ticket']") + .first().attr("href") ?? null; + + events.push({ + venue_id: venue.id, + title, + artist, + date, + open_time: openTime, + start_time: startTime, + price, + ticket_url: ticketUrl, + image_url: imageUrl, + source_url: sourceUrl, + }); + }); + + return events; + }, +}; -- cgit v1.2.3