From 00c4851e3a716ee30d435bea20e65af55288da6f Mon Sep 17 00:00:00 2001 From: yyamashita Date: Thu, 14 May 2026 23:53:51 +0900 Subject: Add BuzzFront YOKOHAMA scraper Co-Authored-By: Claude Sonnet 4.6 --- app/scrapers/buzzfront-yokohama.ts | 111 +++++++++++++++++++++++++++++++++++++ app/scrapers/index.ts | 2 + 2 files changed, 113 insertions(+) create mode 100644 app/scrapers/buzzfront-yokohama.ts (limited to 'app') diff --git a/app/scrapers/buzzfront-yokohama.ts b/app/scrapers/buzzfront-yokohama.ts new file mode 100644 index 0000000..d206e1d --- /dev/null +++ b/app/scrapers/buzzfront-yokohama.ts @@ -0,0 +1,111 @@ +/** + * BuzzFront YOKOHAMA — https://buzzfront.net + * + * meets-otsuka と同じ omatsuri.tech プラットフォーム。構造: + *
+ *

タイトル

+ *

アーティスト

+ *

OPEN 19:00 / START 19:30

+ * 前売券(税込) + * 3,000円 + *
+ */ +import * as cheerio from "cheerio"; +import type { Scraper, VenueMeta } from "./base"; +import type { EventInput } from "~/lib/db.server"; + +export const venue: VenueMeta = { + id: "buzzfront-yokohama", + name: "BuzzFront YOKOHAMA", + url: "https://buzzfront.net", + area: "横浜", + capacity: 300, +}; + +function parseHtml(html: string): EventInput[] { + const $ = cheerio.load(html); + const events: EventInput[] = []; + + $("div.blog-entry.event-wrap").each((_, el) => { + const $el = $(el); + + const date = $el.attr("event-date") ?? ""; + if (!date.match(/^\d{4}-\d{2}-\d{2}$/)) return; + + const $link = $el.find("h2 a").first(); + const title = $link.text().trim(); + if (!title) return; + + const detailPath = $link.attr("href") ?? null; + const sourceUrl = detailPath ? `${venue.url}${detailPath}` : null; + + const artist = + $el.find("p.act span").map((_, s) => $(s).text().trim()).get().join("、") || null; + + const timeText = $el.find("p.time").first().text(); + const openMatch = timeText.match(/OPEN\s*(\d{2}:\d{2})/i); + const startMatch = timeText.match(/START\s*(\d{2}:\d{2})/i); + + const advLabel = $el.find("span.ticket-price__label").filter((_, s) => + $(s).text().includes("前売") + ).first(); + const advAmount = advLabel.next("span.ticket-price__amount").text().trim(); + const doorLabel = $el.find("span.ticket-price__label").filter((_, s) => + $(s).text().includes("当日") + ).first(); + const doorAmount = doorLabel.next("span.ticket-price__amount").text().trim(); + const price = + advAmount && doorAmount + ? `前売 ${advAmount} / 当日 ${doorAmount}` + : advAmount || doorAmount || null; + + const bgStyle = $el.find("div.image-bg").attr("style") ?? ""; + const imgMatch = bgStyle.match(/url\(["']?([^"')]+)["']?\)/); + const imageUrl = imgMatch?.[1] ?? 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: openMatch?.[1] ?? null, + start_time: startMatch?.[1] ?? null, + price, + ticket_url: ticketUrl, + image_url: imageUrl, + source_url: sourceUrl, + }); + }); + + 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); + const ym = `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, "0")}`; + return `https://buzzfront.net/events?date=${encodeURIComponent(ym)}`; + }); + + 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; + }); + }, +}; diff --git a/app/scrapers/index.ts b/app/scrapers/index.ts index d765248..fa0df9b 100644 --- a/app/scrapers/index.ts +++ b/app/scrapers/index.ts @@ -22,6 +22,7 @@ import { scraper as naveyFloor } from "./navey-floor"; import { scraper as shimokitazawaEra } from "./shimokitazawa-era"; import { scraper as duoMusicExchange } from "./duo-music-exchange"; import { scraper as denAtsu } from "./den-atsu"; +import { scraper as buzzfrontYokohama } from "./buzzfront-yokohama"; export const ALL_SCRAPERS: Scraper[] = [ liquidRoom, @@ -43,6 +44,7 @@ export const ALL_SCRAPERS: Scraper[] = [ shimokitazawaEra, duoMusicExchange, denAtsu, + buzzfrontYokohama, ]; export type { Scraper } from "./base"; -- cgit v1.2.3