/**
* duo MUSIC EXCHANGE — https://duomusicexchange.com
*
* 月別HTML: /schedule/YYYY/index_YYYY-MM.html
* DOM構造:
*
* 01
*
*
アーティスト名
*
イベントタイトル
*
* - OPEN/START
- 18:00 / 19:00
* - ADV./DOOR
- ¥3,000 / ¥3,500
* - Ticket.
- ...
*
*
*
*/
import * as cheerio from "cheerio";
import type { Scraper, VenueMeta } from "./base";
import type { EventInput } from "~/lib/db.server";
export const venue: VenueMeta = {
id: "duo-music-exchange",
name: "duo MUSIC EXCHANGE",
url: "https://duomusicexchange.com",
area: "渋谷",
capacity: 700,
};
async function scrapeMonth(year: number, month: number): Promise {
const mm = String(month).padStart(2, "0");
const url = `${venue.url}/schedule/${year}/index_${year}-${mm}.html`;
const res = await fetch(url);
if (!res.ok) return [];
const $ = cheerio.load(await res.text());
const events: EventInput[] = [];
$("section#daybox").each((_, el) => {
const $el = $(el);
const dayStr = $el.find(".date .day").first().text().trim();
const day = parseInt(dayStr, 10);
if (!day) return;
const date = `${year}-${mm}-${String(day).padStart(2, "0")}`;
const artist = $el.find(".sche-details .artist").first().text().trim() || null;
const title = $el.find(".sche-details .details-title").first().text().trim();
if (!title) return;
let openTime: string | null = null;
let startTime: string | null = null;
let price: string | null = null;
let ticketUrl: string | null = null;
$el.find("dl.row dt").each((_, dt) => {
const label = $(dt).text().trim();
const $dd = $(dt).next("dd");
if (/OPEN/i.test(label)) {
const times = $dd.text().trim().match(/(\d{1,2}:\d{2})/g) ?? [];
openTime = times[0] ?? null;
startTime = times[1] ?? null;
} else if (/ADV/i.test(label)) {
price = $dd.text().trim() || null;
} else if (/Ticket/i.test(label)) {
ticketUrl = $dd.find("a[href]").first().attr("href") ?? null;
}
});
const imgSrc = $el.find("img").first().attr("src") ?? null;
const imageUrl = imgSrc
? (imgSrc.startsWith("http") ? imgSrc : `${venue.url}/schedule/${year}/${imgSrc}`)
: 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: url,
});
});
return events;
}
export const scraper: Scraper = {
venue,
async scrape(): Promise {
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 results = await Promise.all(months.map(({ year, month }) => scrapeMonth(year, month)));
return results.flat();
},
};