diff options
| author | yyamashita <yyamashita@mosquit.one> | 2026-05-08 03:50:45 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@mosquit.one> | 2026-05-08 03:50:45 +0900 |
| commit | ae6f6f7f74fd4df7704f963d2f1fdd1f3100668f (patch) | |
| tree | 11eaf19d5880cbfed32cd41fd2f1a565af50503b | |
| parent | d116d4cee456f7d8f5fea535742e90a75b05d814 (diff) | |
Add capacity filter for live houses (~100 / 100~300 / 300~)
- Add capacity field to VenueMeta and all 17 scrapers (researched values)
- Add capacity column to venues table with auto-migration for existing DBs
- Add capacity_range filter to queryEvents (small/medium/large)
- Add capacity selector to FilterBar UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | app/components/FilterBar.tsx | 17 | ||||
| -rw-r--r-- | app/lib/db.server.ts | 36 | ||||
| -rw-r--r-- | app/lib/scraper-runner.server.ts | 4 | ||||
| -rw-r--r-- | app/routes/events._index.tsx | 5 | ||||
| -rw-r--r-- | app/scrapers/base.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/club-quattro.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/fad-yokohama.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/fever-shindaita.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/flat-nishiogikubo.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/liquid-room.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/meets-otsuka.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/mod-shibasaki.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/moon-step-nakano.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/navey-floor.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/nine-spices.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/nishieifuku-jam.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/pitbar-nishiogikubo.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/shibuya-o.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/shimokitazawa-era.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/shinjuku-loft.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/warp-kichijoji.ts | 1 | ||||
| -rw-r--r-- | app/scrapers/www-shibuya.ts | 1 |
22 files changed, 67 insertions, 13 deletions
diff --git a/app/components/FilterBar.tsx b/app/components/FilterBar.tsx index fd7be72..7b8ca0c 100644 --- a/app/components/FilterBar.tsx +++ b/app/components/FilterBar.tsx @@ -63,6 +63,21 @@ export default function FilterBar({ venues, defaultDateFrom, defaultDateTo }: Pr /> </div> + {/* Capacity */} + <div className="flex flex-col gap-1"> + <label className="text-xs text-gray-400">キャパシティ</label> + <select + name="capacity_range" + defaultValue={searchParams.get("capacity_range") ?? ""} + className="rounded-md bg-gray-800 border border-gray-700 px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500" + > + <option value="">すべて</option> + <option value="small">〜100人(小箱)</option> + <option value="medium">100〜300人(中箱)</option> + <option value="large">300人〜(大箱)</option> + </select> + </div> + <button type="submit" className="rounded-md bg-gray-700 px-4 py-1.5 text-sm font-medium hover:bg-gray-600 transition-colors" @@ -83,5 +98,5 @@ export default function FilterBar({ venues, defaultDateFrom, defaultDateTo }: Pr } function hasFilters(params: URLSearchParams): boolean { - return ["keyword", "venue_id", "date_from", "date_to"].some((k) => params.get(k)); + return ["keyword", "venue_id", "date_from", "date_to", "capacity_range"].some((k) => params.get(k)); } diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts index 6da5a1c..a4671b4 100644 --- a/app/lib/db.server.ts +++ b/app/lib/db.server.ts @@ -22,10 +22,11 @@ function getDb(): Database.Database { function initSchema(db: Database.Database) { db.exec(` CREATE TABLE IF NOT EXISTS venues ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - url TEXT NOT NULL, - area TEXT + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + url TEXT NOT NULL, + area TEXT, + capacity INTEGER ); CREATE TABLE IF NOT EXISTS events ( @@ -63,6 +64,13 @@ function initSchema(db: Database.Database) { CREATE INDEX IF NOT EXISTS idx_scrape_logs_run_id ON scrape_logs(run_id); CREATE INDEX IF NOT EXISTS idx_scrape_logs_venue_id ON scrape_logs(venue_id); `); + + // Migration: add capacity column to existing venues tables + try { + db.exec("ALTER TABLE venues ADD COLUMN capacity INTEGER"); + } catch { + // Column already exists — ignore + } } export interface Venue { @@ -70,6 +78,7 @@ export interface Venue { name: string; url: string; area: string | null; + capacity: number | null; event_count?: number; } @@ -110,13 +119,14 @@ export function upsertVenue( id: string, name: string, url: string, - area?: string + area?: string, + capacity?: number ) { getDb() .prepare( - "INSERT OR REPLACE INTO venues (id, name, url, area) VALUES (?, ?, ?, ?)" + "INSERT OR REPLACE INTO venues (id, name, url, area, capacity) VALUES (?, ?, ?, ?, ?)" ) - .run(id, name, url, area ?? null); + .run(id, name, url, area ?? null, capacity ?? null); } export function upsertEvent(raw: EventInput) { @@ -154,17 +164,20 @@ export function upsertEvent(raw: EventInput) { .run(event); } +export type CapacityRange = "small" | "medium" | "large"; + export interface QueryEventsParams { date_from?: string; date_to?: string; venue_id?: string; keyword?: string; + capacity_range?: CapacityRange; limit?: number; offset?: number; } export function queryEvents(params: QueryEventsParams = {}): Event[] { - const { date_from, date_to, venue_id, keyword, limit = 60, offset = 0 } = + const { date_from, date_to, venue_id, keyword, capacity_range, limit = 60, offset = 0 } = params; const clauses: string[] = []; @@ -186,6 +199,13 @@ export function queryEvents(params: QueryEventsParams = {}): Event[] { clauses.push("(e.title LIKE ? OR e.artist LIKE ?)"); args.push(`%${keyword}%`, `%${keyword}%`); } + if (capacity_range === "small") { + clauses.push("v.capacity <= 100"); + } else if (capacity_range === "medium") { + clauses.push("v.capacity > 100 AND v.capacity < 300"); + } else if (capacity_range === "large") { + clauses.push("v.capacity >= 300"); + } const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""; diff --git a/app/lib/scraper-runner.server.ts b/app/lib/scraper-runner.server.ts index 012ff95..8392ead 100644 --- a/app/lib/scraper-runner.server.ts +++ b/app/lib/scraper-runner.server.ts @@ -58,7 +58,7 @@ export async function runAllScrapers(run_id = randomUUID()): Promise<ScrapeResul for (const scraper of ALL_SCRAPERS) { const { venue } = scraper; - upsertVenue(venue.id, venue.name, venue.url, venue.area); + upsertVenue(venue.id, venue.name, venue.url, venue.area, venue.capacity); const logId = insertScrapeLog(run_id, venue.id, venue.name); try { @@ -91,7 +91,7 @@ export async function runScraper(venueId: string, run_id = randomUUID()): Promis } const { venue } = scraper; - upsertVenue(venue.id, venue.name, venue.url, venue.area); + upsertVenue(venue.id, venue.name, venue.url, venue.area, venue.capacity); const logId = insertScrapeLog(run_id, venue.id, venue.name); try { diff --git a/app/routes/events._index.tsx b/app/routes/events._index.tsx index f2e1737..cb1a019 100644 --- a/app/routes/events._index.tsx +++ b/app/routes/events._index.tsx @@ -1,6 +1,6 @@ import { useLoaderData, useSearchParams, Link } from "react-router"; import type { Route } from "./+types/events._index"; -import { queryEvents, getVenues } from "~/lib/db.server"; +import { queryEvents, getVenues, type CapacityRange } from "~/lib/db.server"; import EventCard from "~/components/EventCard"; import EventListRow from "~/components/EventListRow"; import FilterBar from "~/components/FilterBar"; @@ -23,11 +23,12 @@ export async function loader({ request }: Route.LoaderArgs) { const date_to = url.searchParams.get("date_to") ?? defaultTo; const venue_id = url.searchParams.get("venue_id") ?? undefined; const keyword = url.searchParams.get("keyword") ?? undefined; + const capacity_range = (url.searchParams.get("capacity_range") ?? undefined) as CapacityRange | undefined; const page = Math.max(1, parseInt(url.searchParams.get("page") ?? "1", 10)); const limit = 30; const offset = (page - 1) * limit; - const events = queryEvents({ date_from, date_to, venue_id, keyword, limit, offset }); + const events = queryEvents({ date_from, date_to, venue_id, keyword, capacity_range, limit, offset }); const venues = getVenues(); return { events, venues, page, hasMore: events.length === limit, date_from, date_to }; diff --git a/app/scrapers/base.ts b/app/scrapers/base.ts index 512fcbb..8369797 100644 --- a/app/scrapers/base.ts +++ b/app/scrapers/base.ts @@ -5,6 +5,7 @@ export interface VenueMeta { name: string; url: string; area: string; + capacity?: number; } export interface Scraper { diff --git a/app/scrapers/club-quattro.ts b/app/scrapers/club-quattro.ts index 946b9a4..10b60e9 100644 --- a/app/scrapers/club-quattro.ts +++ b/app/scrapers/club-quattro.ts @@ -7,6 +7,7 @@ export const venue: VenueMeta = { name: "CLUB QUATTRO", url: "https://www.club-quattro.com", area: "渋谷", + capacity: 750, }; export const scraper: Scraper = { diff --git a/app/scrapers/fad-yokohama.ts b/app/scrapers/fad-yokohama.ts index e1aa95c..a01ea0d 100644 --- a/app/scrapers/fad-yokohama.ts +++ b/app/scrapers/fad-yokohama.ts @@ -9,6 +9,7 @@ export const venue: VenueMeta = { name: "F.A.D YOKOHAMA", url: "http://www.fad-music.com/fad/", area: "横浜", + capacity: 380, }; function getMonthContext(html: string): { diff --git a/app/scrapers/fever-shindaita.ts b/app/scrapers/fever-shindaita.ts index 71c31f6..62c2e2c 100644 --- a/app/scrapers/fever-shindaita.ts +++ b/app/scrapers/fever-shindaita.ts @@ -20,6 +20,7 @@ export const venue: VenueMeta = { name: "新代田 FEVER", url: "https://www.fever-popo.com", area: "新代田", + capacity: 300, }; async function scrapeMonth(yyyymm: string): Promise<EventInput[]> { diff --git a/app/scrapers/flat-nishiogikubo.ts b/app/scrapers/flat-nishiogikubo.ts index da6752f..50ba688 100644 --- a/app/scrapers/flat-nishiogikubo.ts +++ b/app/scrapers/flat-nishiogikubo.ts @@ -21,6 +21,7 @@ export const venue: VenueMeta = { name: "FLAT 西荻窪", url: "https://www.flat.rinky.info", area: "西荻窪", + capacity: 80, }; const SCHEDULE_URL = "https://www.flat.rinky.info/schedule"; diff --git a/app/scrapers/liquid-room.ts b/app/scrapers/liquid-room.ts index f577ee6..1eeade6 100644 --- a/app/scrapers/liquid-room.ts +++ b/app/scrapers/liquid-room.ts @@ -7,6 +7,7 @@ export const venue: VenueMeta = { name: "LIQUID ROOM", url: "https://www.liquidroom.net", area: "恵比寿", + capacity: 1000, }; export const scraper: Scraper = { diff --git a/app/scrapers/meets-otsuka.ts b/app/scrapers/meets-otsuka.ts index 57cf120..0b56251 100644 --- a/app/scrapers/meets-otsuka.ts +++ b/app/scrapers/meets-otsuka.ts @@ -18,6 +18,7 @@ export const venue: VenueMeta = { name: "Meets 大塚", url: "https://meets.rinky.info", area: "大塚", + capacity: 100, }; export const scraper: Scraper = { diff --git a/app/scrapers/mod-shibasaki.ts b/app/scrapers/mod-shibasaki.ts index 0e2a96b..7642805 100644 --- a/app/scrapers/mod-shibasaki.ts +++ b/app/scrapers/mod-shibasaki.ts @@ -21,6 +21,7 @@ export const venue: VenueMeta = { name: "shibasaki mod", url: "https://shibasakimod.com", area: "柴崎", + capacity: 80, }; const SCHEDULE_URL = "https://shibasakimod.com/schedule"; diff --git a/app/scrapers/moon-step-nakano.ts b/app/scrapers/moon-step-nakano.ts index e67e128..cc2a0f0 100644 --- a/app/scrapers/moon-step-nakano.ts +++ b/app/scrapers/moon-step-nakano.ts @@ -15,6 +15,7 @@ export const venue: VenueMeta = { name: "中野 MOON STEP", url: "https://nakano-dynamite.com/moonstep", area: "中野", + capacity: 200, }; const API_URL = "https://nakano-dynamite.com/moonstep/wp-json/tribe/events/v1/events"; diff --git a/app/scrapers/navey-floor.ts b/app/scrapers/navey-floor.ts index 806193e..14736da 100644 --- a/app/scrapers/navey-floor.ts +++ b/app/scrapers/navey-floor.ts @@ -7,6 +7,7 @@ export const venue: VenueMeta = { name: "navey floor", url: "https://navey-floor.com", area: "赤坂", + capacity: 150, }; function parseNaveyDate(text: string): string | null { diff --git a/app/scrapers/nine-spices.ts b/app/scrapers/nine-spices.ts index f4afa3d..5d60ed4 100644 --- a/app/scrapers/nine-spices.ts +++ b/app/scrapers/nine-spices.ts @@ -18,6 +18,7 @@ export const venue: VenueMeta = { name: "Nine Spices", url: "https://9spices.rinky.info", area: "新宿", + capacity: 200, }; export const scraper: Scraper = { diff --git a/app/scrapers/nishieifuku-jam.ts b/app/scrapers/nishieifuku-jam.ts index c93b051..7408e02 100644 --- a/app/scrapers/nishieifuku-jam.ts +++ b/app/scrapers/nishieifuku-jam.ts @@ -17,6 +17,7 @@ export const venue: VenueMeta = { name: "西永福JAM", url: "https://jam.rinky.info", area: "西永福", + capacity: 250, }; export const scraper: Scraper = { diff --git a/app/scrapers/pitbar-nishiogikubo.ts b/app/scrapers/pitbar-nishiogikubo.ts index 54d25d5..2553002 100644 --- a/app/scrapers/pitbar-nishiogikubo.ts +++ b/app/scrapers/pitbar-nishiogikubo.ts @@ -20,6 +20,7 @@ export const venue: VenueMeta = { name: "Pitbar 西荻窪", url: "https://ameblo.jp/pitbar", area: "西荻窪", + capacity: 100, }; const CALENDAR_URL = "http://freecalend.com/open/mem25771"; diff --git a/app/scrapers/shibuya-o.ts b/app/scrapers/shibuya-o.ts index 3d6f192..c674cfc 100644 --- a/app/scrapers/shibuya-o.ts +++ b/app/scrapers/shibuya-o.ts @@ -20,6 +20,7 @@ export const venue: VenueMeta = { name: "渋谷 O-EAST / O-WEST / O-Crest / O-nest", url: "https://shibuya-o.com", area: "渋谷", + capacity: 1300, }; const SUB_VENUES = ["east", "west", "crest", "nest"]; diff --git a/app/scrapers/shimokitazawa-era.ts b/app/scrapers/shimokitazawa-era.ts index a35f8e2..3678a57 100644 --- a/app/scrapers/shimokitazawa-era.ts +++ b/app/scrapers/shimokitazawa-era.ts @@ -7,6 +7,7 @@ export const venue: VenueMeta = { name: "下北沢ERA", url: "http://s-era.jp", area: "下北沢", + capacity: 200, }; export const scraper: Scraper = { diff --git a/app/scrapers/shinjuku-loft.ts b/app/scrapers/shinjuku-loft.ts index d5602e7..837d6e5 100644 --- a/app/scrapers/shinjuku-loft.ts +++ b/app/scrapers/shinjuku-loft.ts @@ -7,6 +7,7 @@ export const venue: VenueMeta = { name: "新宿 LOFT", url: "https://www.loft-prj.co.jp", area: "新宿", + capacity: 500, }; export const scraper: Scraper = { diff --git a/app/scrapers/warp-kichijoji.ts b/app/scrapers/warp-kichijoji.ts index 8929fef..765d1fc 100644 --- a/app/scrapers/warp-kichijoji.ts +++ b/app/scrapers/warp-kichijoji.ts @@ -24,6 +24,7 @@ export const venue: VenueMeta = { name: "吉祥寺 WARP", url: "http://warp.rinky.info", area: "吉祥寺", + capacity: 180, }; export const scraper: Scraper = { diff --git a/app/scrapers/www-shibuya.ts b/app/scrapers/www-shibuya.ts index d561332..2c85080 100644 --- a/app/scrapers/www-shibuya.ts +++ b/app/scrapers/www-shibuya.ts @@ -7,6 +7,7 @@ export const venue: VenueMeta = { name: "WWW / WWW X", url: "https://www-shibuya.jp", area: "渋谷", + capacity: 700, }; export const scraper: Scraper = { |
