summaryrefslogtreecommitdiff
path: root/app/scrapers/pitbar-nishiogikubo.ts
blob: 5c70023321a33fe694596de32452e2b499bcb4cf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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;
}