summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryyamashita <yyamashita@mosquit.one>2026-05-08 03:50:45 +0900
committeryyamashita <yyamashita@mosquit.one>2026-05-08 03:50:45 +0900
commitae6f6f7f74fd4df7704f963d2f1fdd1f3100668f (patch)
tree11eaf19d5880cbfed32cd41fd2f1a565af50503b
parentd116d4cee456f7d8f5fea535742e90a75b05d814 (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.tsx17
-rw-r--r--app/lib/db.server.ts36
-rw-r--r--app/lib/scraper-runner.server.ts4
-rw-r--r--app/routes/events._index.tsx5
-rw-r--r--app/scrapers/base.ts1
-rw-r--r--app/scrapers/club-quattro.ts1
-rw-r--r--app/scrapers/fad-yokohama.ts1
-rw-r--r--app/scrapers/fever-shindaita.ts1
-rw-r--r--app/scrapers/flat-nishiogikubo.ts1
-rw-r--r--app/scrapers/liquid-room.ts1
-rw-r--r--app/scrapers/meets-otsuka.ts1
-rw-r--r--app/scrapers/mod-shibasaki.ts1
-rw-r--r--app/scrapers/moon-step-nakano.ts1
-rw-r--r--app/scrapers/navey-floor.ts1
-rw-r--r--app/scrapers/nine-spices.ts1
-rw-r--r--app/scrapers/nishieifuku-jam.ts1
-rw-r--r--app/scrapers/pitbar-nishiogikubo.ts1
-rw-r--r--app/scrapers/shibuya-o.ts1
-rw-r--r--app/scrapers/shimokitazawa-era.ts1
-rw-r--r--app/scrapers/shinjuku-loft.ts1
-rw-r--r--app/scrapers/warp-kichijoji.ts1
-rw-r--r--app/scrapers/www-shibuya.ts1
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 = {