summaryrefslogtreecommitdiff
path: root/app/scrapers/meets-otsuka.ts
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-06 22:20:00 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-06 22:20:00 +0900
commitf817604858891edb79e26459dae884b158774db1 (patch)
tree7a4cc1cd7f1091d2eece430e5d5de7d02d987669 /app/scrapers/meets-otsuka.ts
parent079176fae1d513d68b53c57274f3ae2864f352fc (diff)
Add 4 new venue scrapers: Meets 大塚, WARP 吉祥寺, FLAT 西荻窪, Pitbar 西荻窪
meets-otsuka: rinky.info プラットフォーム。div.blog-entry.event-wrap[event-date] から日付・タイトル・出演者・時間・価格・チケットURLを取得。 warp-kichijoji: WordPress カスタムテーマ。<h3>YYYY<br/><span>MM</span></h3> で 年月を取得、article.schedules-box から各イベントをパース。 flat-nishiogikubo: Wix サイトのため JS レンダリング必須。エラーを返す プレースホルダー実装(Playwright 等への移行が必要)。 pitbar-nishiogikubo: freecalend.com (mem25771) から取得を試みるが、 ボット遮断のため現状はエラー。URL パターン・代替策をコメントに記載。 SCRAPE_TARGETS.md に状態列(✅/⚠️)を追加。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/scrapers/meets-otsuka.ts')
-rw-r--r--app/scrapers/meets-otsuka.ts79
1 files changed, 79 insertions, 0 deletions
diff --git a/app/scrapers/meets-otsuka.ts b/app/scrapers/meets-otsuka.ts
new file mode 100644
index 0000000..57cf120
--- /dev/null
+++ b/app/scrapers/meets-otsuka.ts
@@ -0,0 +1,79 @@
+/**
+ * Meets 大塚 — https://meets.rinky.info/events
+ *
+ * rinky.info プラットフォーム。イベントは以下の構造:
+ * <div class="blog-entry event-wrap" event-date="YYYY-MM-DD">
+ * <h2><a href="/events/ID">タイトル</a></h2>
+ * <p class="act"><span>アーティスト</span></p>
+ * <p class="time">OPEN 18:30 / START 19:00</p>
+ * <span class="ticket-price__label">価格</span>
+ * <div class="image-bg" style="background-image: url(...)">
+ */
+import * as cheerio from "cheerio";
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "meets-otsuka",
+ name: "Meets 大塚",
+ url: "https://meets.rinky.info",
+ area: "大塚",
+};
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const res = await fetch("https://meets.rinky.info/events");
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const $ = cheerio.load(await res.text());
+ 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 price = $el.find("span.ticket-price__label").first().text().trim() || null;
+
+ // background-image: url("...")
+ 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*='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;
+ },
+};