/**
* 吉祥寺 WARP — http://warp.rinky.info/schedules
*
* WordPress カスタムテーマ。年月は:
*
{
const res = await fetch("http://warp.rinky.info/schedules");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const $ = cheerio.load(await res.text());
const events: EventInput[] = [];
// Extract year + month from 2026
05
const h3Text = $("h3").first().text().trim(); // e.g. "2026\n05"
const yearMonthMatch = h3Text.match(/(\d{4})\D*(\d{2})/);
if (!yearMonthMatch) return events;
const year = yearMonthMatch[1];
const month = yearMonthMatch[2];
$("article.schedules-box").each((_, el) => {
const $el = $(el);
// Day from article id: "box-03-23546" → "03"
const id = $el.attr("id") ?? "";
const dayMatch = id.match(/^box-(\d{2})-/);
if (!dayMatch) return;
const day = dayMatch[1];
const date = `${year}-${month}-${day}`;
const title = $el.find("h4").first().text().replace(/
/gi, " ").trim();
if (!title) return;
// First notes-wrapper contains OPEN/START times
const $notes = $el.find("section.notes-wrapper p");
const timeStrong = $notes.eq(0).find("span.strong").text().trim();
// e.g. "18:30 / 19:00"
const [openTime, startTime] = timeStrong.split("/").map((s) => s.trim());
// Second
contains ADV/DOOR price
const priceStrong = $notes.eq(1).find("span.strong").text().trim();
// e.g. "¥3,000 / ¥3,500"
const price = priceStrong !== "TBA / TBA" && priceStrong ? priceStrong : null;
// Image: prefer data-src (lazy), fall back to noscript img src
const $flyer = $el.find("section.flyer img").first();
const rawImg =
$flyer.attr("data-src") ??
$el.find("section.flyer noscript img").first().attr("src") ??
null;
// Strip ShortPixel CDN prefix if present
const imageUrl = rawImg
? rawImg.replace(/^https?:\/\/sp-ao\.shortpixel\.ai\/client\/[^/]+\//, "")
: null;
// Artists in
separated by
// notes-wrapper and detail-texts are nested inside w-flyer — clone and strip them
const $wFlyer = $el.find("div.w-flyer").first().clone();
$wFlyer.find("section.notes-wrapper, div.detail-texts").remove();
$wFlyer.find("br").replaceWith("\n");
const rawArtist = $wFlyer.text();
const artistLines: string[] = [];
for (const raw of rawArtist.split("\n")) {
const l = raw.trim();
if (!l) {
if (artistLines.length > 0) break; // stop at first blank line after artists
continue;
}
if (/^[■▼◼▶◆]|チケット|ticket|TICKET|予約|http|\d{1,2}:\d{2}|[¥¥]/i.test(l)) break;
artistLines.push(l);
}
const artist = artistLines.length > 0 ? artistLines.join(" / ") : null;
events.push({
venue_id: venue.id,
title,
artist,
date,
open_time: isTime(openTime) ? openTime : null,
start_time: isTime(startTime) ? startTime : null,
price,
ticket_url: $el.find("a[href*='livepocket'], a[href*='eplus'], a[href*='pia']").first().attr("href") ?? null,
image_url: imageUrl,
source_url: null,
});
});
return events;
},
};
function isTime(s: string | undefined): boolean {
return !!s && /^\d{2}:\d{2}$/.test(s.trim());
}