/**
* RIPS 八王子 — http://rips.rinkydink.info/rips/schedule
*
* WordPress + My Calendar プラグイン。
* イベント構造:
*
* -
* 5/1(金)
*
*
*
タイトル
*
*
アーティスト1
アーティスト2
*
OPEN HH:MM START HH:MM
adv:XXXX door:XXXX
*
*
*
*
*
* ?yr=YYYY&month=M&dy&cid=... でページネーション
*/
import * as cheerio from "cheerio";
import type { Scraper, VenueMeta } from "./base";
import type { EventInput } from "~/lib/db.server";
export const venue: VenueMeta = {
id: "rips-hachioji",
name: "RIPS 八王子",
url: "http://rips.rinkydink.info/rips",
area: "八王子",
};
const SCHEDULE_BASE = "http://rips.rinkydink.info/rips/schedule";
const CID = "mc-35df894f2fa16157a96dd95b6cded5c1";
function parseHtml(html: string): EventInput[] {
const $ = cheerio.load(html);
const events: EventInput[] = [];
$("ul.mc-list > li[id^='list-']").each((_, li) => {
const $li = $(li);
const dateMatch = $li.attr("id")?.match(/^list-(\d{4}-\d{2}-\d{2})$/);
if (!dateMatch) return;
const date = dateMatch[1];
$li.find("div.list-event").each((_, eventEl) => {
const $event = $(eventEl);
const title = $event.find("h3.event-title").text().trim();
if (!title) return;
let artist: string | null = null;
let open_time: string | null = null;
let start_time: string | null = null;
let price: string | null = null;
let image_url: string | null = null;
const $desc = $event.find("div.longdesc.description");
if ($desc.length) {
image_url = $desc.find("img").first().attr("src") ?? null;
const paragraphs: string[] = [];
$desc.find("p").each((_, p) => {
const $p = $(p).clone();
$p.find("br").replaceWith("\n");
const text = $p.text().trim();
if (text) paragraphs.push(text);
});
const timeParaIdx = paragraphs.findIndex((p) =>
/(?:open|pen)\s+\d{1,2}:\d{2}/i.test(p)
);
if (timeParaIdx !== -1) {
const timePara = paragraphs[timeParaIdx];
const openMatch = timePara.match(/(?:open|pen)\s+(\d{1,2}:\d{2})/i);
const startMatch = timePara.match(/start\s+(\d{1,2}:\d{2})/i);
if (openMatch) open_time = openMatch[1];
if (startMatch) start_time = startMatch[1];
const priceLines = timePara
.split("\n")
.map((l) => l.trim())
.filter((l) => /adv|door/i.test(l));
if (priceLines.length) price = priceLines.join(" / ");
const artistLines = paragraphs
.filter((_, i) => i !== timeParaIdx)
.flatMap((p) => p.split("\n").map((l) => l.trim()))
.filter((l) => l && !/^出演$/.test(l));
if (artistLines.length) artist = artistLines.join("、");
} else if (paragraphs.length) {
const artistLines = paragraphs
.flatMap((p) => p.split("\n").map((l) => l.trim()))
.filter((l) => l && !/^出演$/.test(l));
if (artistLines.length) artist = artistLines.join("、");
}
}
events.push({
venue_id: venue.id,
title,
artist,
date,
open_time,
start_time,
price,
image_url,
ticket_url:
$event
.find(
"a[href*='livepocket'], a[href*='eplus'], a[href*='pia'], a[href*='l-tike'], a[href*='tiget']"
)
.first()
.attr("href") ?? null,
source_url: SCHEDULE_BASE,
});
});
});
return events;
}
export const scraper: Scraper = {
venue,
async scrape(): Promise {
const now = new Date();
const urls = [0, 1, 2].map((offset) => {
const d = new Date(now.getFullYear(), now.getMonth() + offset, 1);
return `${SCHEDULE_BASE}?yr=${d.getFullYear()}&month=${d.getMonth() + 1}&dy&cid=${CID}`;
});
const htmls = await Promise.all(
urls.map((url) => fetch(url).then((r) => (r.ok ? r.text() : "")))
);
const seen = new Set();
return htmls
.flatMap(parseHtml)
.filter((e) => {
const key = `${e.date}|${e.title}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
},
};