summaryrefslogtreecommitdiff
path: root/app/scrapers/nine-spices.ts
blob: f4afa3d4f10450e87f61cbad07f96fe1da6f17ab (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
/**
 * Nine Spices (新宿) — https://9spices.rinky.info/schedule/
 *
 * WordPress ベースの独自テーマ。構造:
 *   <div class="event-cont-par YYYY-MM-DD">
 *     <h3 class="event-title sch"><a href="...">タイトル</a></h3>
 *     <div class="event-leftcol" itemprop="startDate" content="YYYY-MM-DDThh:mm">
 *     <div class="sch-actlist"><span class="actlist-name">アーティスト</span></div>
 *     <div class="sch-time"><div><span>OPEN</span><span>hh:mm</span></div><div><span>START</span>...</div></div>
 *     <div class="sch-price"><div><span>ADV</span><span>¥XXX</span></div></div>
 */
import * as cheerio from "cheerio";
import type { Scraper, VenueMeta } from "./base";
import type { EventInput } from "~/lib/db.server";

export const venue: VenueMeta = {
  id: "nine-spices",
  name: "Nine Spices",
  url: "https://9spices.rinky.info",
  area: "新宿",
};

export const scraper: Scraper = {
  venue,
  async scrape(): Promise<EventInput[]> {
    const res = await fetch("https://9spices.rinky.info/schedule/");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const $ = cheerio.load(await res.text());
    const events: EventInput[] = [];

    $("div[class*='event-cont-par']").each((_, el) => {
      const $el = $(el);

      // class="event-cont-par 2026-05-01" → extract date
      const classAttr = $el.attr("class") ?? "";
      const dateMatch = classAttr.match(/(\d{4}-\d{2}-\d{2})/);
      if (!dateMatch) return;
      const date = dateMatch[1];

      const $titleLink = $el.find("h3.event-title a").first();
      const title = $titleLink.text().trim();
      if (!title) return;

      const sourceUrl = $titleLink.attr("href") ?? null;

      const artist = $el.find("span.actlist-name")
        .map((_, s) => $(s).text().trim())
        .get()
        .join("、") || null;

      // <div class="sch-time"><div><span>OPEN</span><span>18:30</span></div>...
      let openTime: string | null = null;
      let startTime: string | null = null;
      $el.find("div.sch-time div").each((_, row) => {
        const spans = $(row).find("span");
        const label = spans.eq(0).text().trim().toUpperCase();
        const value = spans.eq(1).text().trim();
        if (label === "OPEN") openTime = value || null;
        if (label === "START") startTime = value || null;
      });

      // <div class="sch-price"><div><span>ADV</span><span>¥2,500</span></div>...
      const priceParts: string[] = [];
      $el.find("div.sch-price div").each((_, row) => {
        const spans = $(row).find("span");
        const label = spans.eq(0).text().trim();
        const value = spans.eq(1).text().trim();
        if (label && value) priceParts.push(`${label} ${value}`);
      });
      const price = priceParts.length ? priceParts.join(" / ") : null;

      const imageUrl = $el.find("img.wp-post-image").first().attr("src") ?? 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: openTime,
        start_time: startTime,
        price,
        ticket_url: ticketUrl,
        image_url: imageUrl,
        source_url: sourceUrl,
      });
    });

    return events;
  },
};