summaryrefslogtreecommitdiff
path: root/app/scrapers/fever-shindaita.ts
blob: 71c31f6cde0384de58964ce47c8fddbf6b316c8e (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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
 * 新代田 FEVER — https://www.fever-popo.com
 *
 * Movable Type CMS。月別 URL: /schedule/YYYY/MM/
 * DOM 構造:
 *   <div class="entry-asset">
 *     <h2 class="eventtitle">26.05.01 (Fri)&nbsp;タイトル</h2>
 *     <meta property="og:url" content="https://www.fever-popo.com/schedule/.../MMDD.html">
 *     <h3><p>アーティスト1<br/>アーティスト2</p></h3>
 *     <div>OPEN HH:MM / START HH:MM</div>
 *     <div><p>ADV ¥XXXX (+1drink) / DOOR ¥XXXX (+1drink)</p></div>
 *     <img class="scpickup" src="..."> ← フライヤー画像
 */
import * as cheerio from "cheerio";
import type { Scraper, VenueMeta } from "./base";
import type { EventInput } from "~/lib/db.server";

export const venue: VenueMeta = {
  id: "fever-shindaita",
  name: "新代田 FEVER",
  url: "https://www.fever-popo.com",
  area: "新代田",
};

async function scrapeMonth(yyyymm: string): Promise<EventInput[]> {
  const [year, month] = yyyymm.split("-");
  const url = `${venue.url}/schedule/${year}/${month}/`;
  const res = await fetch(url);
  if (!res.ok) return [];
  const $ = cheerio.load(await res.text());
  const events: EventInput[] = [];

  $("div.entry-asset").each((_, el) => {
    const $el = $(el);

    // Title: "26.05.01 (Fri) タイトル"
    const h2Text = $el.find("h2.eventtitle").first().text();
    const titleMatch = h2Text.match(/^\d{2}\.\d{2}\.\d{2}\s+\([A-Za-z]+\)\s*(.+)$/);
    if (!titleMatch) return;
    const title = titleMatch[1].trim();
    if (!title) return;

    // Date from title prefix: "26.05.01"
    const dateMatch = h2Text.match(/^(\d{2})\.(\d{2})\.(\d{2})/);
    if (!dateMatch) return;
    const date = `20${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}`;

    // Source URL from og:url meta inside the entry
    const sourceUrl = $el.find("meta[property='og:url']").attr("content") ?? null;

    // Artists: first <h3><p> in body
    const $h3 = $el.find("div.asset-body h3").first();
    const artist = $h3.find("p").text()
      .split(/\n|<br\s*\/?>/i)
      .map((s) => s.replace(/<[^>]+>/g, "").trim())
      .filter(Boolean)
      .join("、") || null;

    // Time: div containing "OPEN" / "START"
    let openTime: string | null = null;
    let startTime: string | null = null;
    $el.find("div.asset-body div").each((_, d) => {
      const text = $(d).text();
      if (/OPEN/i.test(text) && /START/i.test(text)) {
        const om = text.match(/OPEN\s*(\d{1,2}:\d{2})/i);
        const sm = text.match(/START\s*(\d{1,2}:\d{2})/i);
        if (om) openTime = om[1];
        if (sm) startTime = sm[1];
      }
    });

    // Price: div after the time div
    let price: string | null = null;
    $el.find("div.asset-body div").each((_, d) => {
      const text = $(d).text().trim();
      if (/[¥¥]/.test(text) && /(ADV|DOOR|前売|当日)/i.test(text)) {
        price = text.replace(/\s+/g, " ").split("\n")[0].trim() || null;
      }
    });

    // Image
    const imageUrl = $el.find("img.scpickup").first().attr("src") ?? null;

    // Ticket URL
    const ticketUrl =
      $el.find("a[href*='eplus'], a[href*='pia'], a[href*='tiget'], a[href*='livepocket'], a[href*='t-dv.com']")
        .first().attr("href") ?? 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: sourceUrl,
    });
  });

  return events;
}

export const scraper: Scraper = {
  venue,
  async scrape(): Promise<EventInput[]> {
    const now = new Date();
    const thisMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
    const next = new Date(now.getFullYear(), now.getMonth() + 1, 1);
    const nextMonth = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`;

    const [a, b] = await Promise.all([scrapeMonth(thisMonth), scrapeMonth(nextMonth)]);
    return [...a, ...b];
  },
};