/** * 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(); }, };