summaryrefslogtreecommitdiff
path: root/app/scrapers/pitbar-nishiogikubo.ts
diff options
context:
space:
mode:
Diffstat (limited to 'app/scrapers/pitbar-nishiogikubo.ts')
-rw-r--r--app/scrapers/pitbar-nishiogikubo.ts101
1 files changed, 101 insertions, 0 deletions
diff --git a/app/scrapers/pitbar-nishiogikubo.ts b/app/scrapers/pitbar-nishiogikubo.ts
new file mode 100644
index 0000000..5c70023
--- /dev/null
+++ b/app/scrapers/pitbar-nishiogikubo.ts
@@ -0,0 +1,101 @@
+/**
+ * Pitbar 西荻窪 — http://freecalend.com/open/mem25771_date{YYYYMM}
+ *
+ * スケジュールは Ameblo (https://ameblo.jp/pitbar/) 経由で
+ * freecalend.com に掲載されているが、自動リクエストをブロックしている。
+ *
+ * 代替案:
+ * - User-Agent を設定したヘッドレスブラウザで freecalend を取得
+ * - 公式 Instagram / X (@pitbar_nishiogi) の投稿を取得
+ * - 手動でイベントを登録する管理画面を用意する
+ *
+ * 月ごとの URL パターン: http://freecalend.com/open/mem25771_date{YYYYMM}
+ */
+import type { Scraper, VenueMeta } from "./base";
+import type { EventInput } from "~/lib/db.server";
+
+export const venue: VenueMeta = {
+ id: "pitbar-nishiogikubo",
+ name: "Pitbar 西荻窪",
+ url: "https://ameblo.jp/pitbar",
+ area: "西荻窪",
+};
+
+const FREECALEND_MEMBER = "25771";
+
+export const scraper: Scraper = {
+ venue,
+ async scrape(): Promise<EventInput[]> {
+ const months = upcomingMonths(2);
+ const events: EventInput[] = [];
+
+ for (const ym of months) {
+ const url = `http://freecalend.com/open/mem${FREECALEND_MEMBER}_date${ym}`;
+ const res = await fetch(url, {
+ headers: {
+ "User-Agent":
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/124 Safari/537.36",
+ Referer: "https://ameblo.jp/pitbar/",
+ },
+ redirect: "follow",
+ });
+ if (!res.ok) continue;
+
+ const html = await res.text();
+ if (!html.trim()) continue;
+
+ // freecalend は HTML テーブルカレンダー形式
+ // <td class="day_..."> 内にイベント名と時刻が入る
+ const { load } = await import("cheerio");
+ const $ = load(html);
+
+ $("td[class*='day_']").each((_, el) => {
+ const $el = $(el);
+ const text = $el.text().trim();
+ if (!text || /^\d+$/.test(text)) return; // 日付のみのセルはスキップ
+
+ const dayMatch = $el.attr("class")?.match(/day_(\d+)/);
+ if (!dayMatch) return;
+ const day = dayMatch[1].padStart(2, "0");
+ const date = `${ym.slice(0, 4)}-${ym.slice(4)}-${day}`;
+
+ const lines = text.split(/[\n\r]+/).map((l) => l.trim()).filter(Boolean);
+ const title = lines[0] ?? text.slice(0, 100);
+
+ const timeMatch = text.match(/(\d{1,2}:\d{2})/g);
+ const openTime = timeMatch?.[0] ?? null;
+ const startTime = timeMatch?.[1] ?? null;
+
+ events.push({
+ venue_id: venue.id,
+ title,
+ date,
+ open_time: openTime,
+ start_time: startTime,
+ source_url: url,
+ });
+ });
+ }
+
+ if (events.length === 0) {
+ throw new Error(
+ "Pitbar freecalend からデータを取得できませんでした。" +
+ "freecalend.com が自動リクエストをブロックしている可能性があります。"
+ );
+ }
+
+ return events;
+ },
+};
+
+function upcomingMonths(count: number): string[] {
+ const months: string[] = [];
+ const now = new Date();
+ for (let i = 0; i < count; i++) {
+ const d = new Date(now.getFullYear(), now.getMonth() + i, 1);
+ const y = d.getFullYear();
+ const m = String(d.getMonth() + 1).padStart(2, "0");
+ months.push(`${y}${m}`);
+ }
+ return months;
+}