/** * GET /api/openapi.json — OpenAPI 3.1 spec */ import type { Route } from "./+types/api.openapi"; const SPEC = { openapi: "3.1.0", info: { title: "Tokyo Livehouse Events API", version: "1.0.0", description: "東京・近郊のライブハウスのイベント情報を提供する REST API。" + "スクレイパーが定期取得したデータを SQLite に保存し、JSON で返します。", }, servers: [{ url: "/api", description: "本番 / ローカル共通" }], tags: [ { name: "events", description: "イベント情報" }, { name: "venues", description: "会場情報" }, { name: "scrape", description: "スクレイプ操作" }, ], paths: { "/events": { get: { tags: ["events"], summary: "イベント一覧", description: "条件でフィルタリングしたイベントをページネーション付きで返します。", operationId: "listEvents", parameters: [ { name: "date_from", in: "query", description: "開始日 (YYYY-MM-DD)。未指定時は当日。", schema: { type: "string", format: "date", example: "2026-05-01" }, }, { name: "date_to", in: "query", description: "終了日 (YYYY-MM-DD)。", schema: { type: "string", format: "date", example: "2026-05-31" }, }, { name: "venue_id", in: "query", description: "会場 ID で絞り込み。", schema: { type: "string", example: "liquid-room" }, }, { name: "keyword", in: "query", description: "タイトル・アーティスト名の部分一致。", schema: { type: "string", example: "メリー" }, }, { name: "area", in: "query", description: "エリア名で絞り込み (例: 渋谷、東高円寺)。", schema: { type: "string", example: "東高円寺" }, }, { name: "capacity_range", in: "query", description: "キャパシティ帯: small (≤100) / medium (101–299) / large (≥300)。", schema: { type: "string", enum: ["small", "medium", "large"] }, }, { name: "limit", in: "query", description: "取得件数 (1–100、デフォルト 60)。", schema: { type: "integer", minimum: 1, maximum: 100, default: 60 }, }, { name: "offset", in: "query", description: "スキップ件数 (デフォルト 0)。", schema: { type: "integer", minimum: 0, default: 0 }, }, ], responses: { "200": { description: "成功", content: { "application/json": { schema: { $ref: "#/components/schemas/EventListResponse" }, }, }, }, }, }, }, "/events/{id}": { get: { tags: ["events"], summary: "イベント詳細", operationId: "getEvent", parameters: [ { name: "id", in: "path", required: true, description: "イベント ID (整数)。", schema: { type: "integer", example: 42 }, }, ], responses: { "200": { description: "成功", content: { "application/json": { schema: { $ref: "#/components/schemas/EventDetailResponse" }, }, }, }, "400": { description: "id が不正" }, "404": { description: "イベントが見つからない" }, }, }, }, "/events/{id}/calendar.ics": { get: { tags: ["events"], summary: "iCalendar 形式でイベントをダウンロード", operationId: "getEventIcs", parameters: [ { name: "id", in: "path", required: true, schema: { type: "integer" }, }, ], responses: { "200": { description: ".ics ファイル", content: { "text/calendar": { schema: { type: "string" } } }, }, "404": { description: "イベントが見つからない" }, }, }, }, "/venues": { get: { tags: ["venues"], summary: "会場一覧", description: "登録済み会場の一覧とイベント件数を返します。", operationId: "listVenues", responses: { "200": { description: "成功", content: { "application/json": { schema: { $ref: "#/components/schemas/VenueListResponse" }, }, }, }, }, }, }, "/scrape": { get: { tags: ["scrape"], summary: "スクレイプ開始 (GET)", description: "`venue_id` を指定すると該当会場のみ、省略すると全会場をバックグラウンドでスクレイプします。", operationId: "startScrapeGet", parameters: [ { name: "venue_id", in: "query", description: "会場 ID。省略で全会場。", schema: { type: "string" }, }, ], responses: { "202": { description: "スクレイプ開始", content: { "application/json": { schema: { $ref: "#/components/schemas/ScrapeStartResponse" }, }, }, }, }, }, post: { tags: ["scrape"], summary: "スクレイプ開始 (POST)", operationId: "startScrapePost", requestBody: { content: { "application/x-www-form-urlencoded": { schema: { type: "object", properties: { venue_id: { type: "string", description: "省略で全会場" }, }, }, }, }, }, responses: { "202": { description: "スクレイプ開始", content: { "application/json": { schema: { $ref: "#/components/schemas/ScrapeStartResponse" }, }, }, }, }, }, }, "/scrape-status": { get: { tags: ["scrape"], summary: "スクレイプ状況確認", operationId: "getScrapeStatus", parameters: [ { name: "run_id", in: "query", description: "run_id を省略すると最新ランの状況を返します。", schema: { type: "string", format: "uuid" }, }, ], responses: { "200": { description: "成功", content: { "application/json": { schema: { $ref: "#/components/schemas/ScrapeStatusResponse" }, }, }, }, }, }, }, }, components: { schemas: { Event: { type: "object", properties: { id: { type: "integer" }, venue_id: { type: "string" }, venue_name: { type: "string" }, venue_area: { type: ["string", "null"] }, venue_url: { type: ["string", "null"] }, title: { type: "string" }, artist: { type: ["string", "null"] }, date: { type: "string", format: "date", example: "2026-05-15" }, start_time: { type: ["string", "null"], example: "19:00" }, open_time: { type: ["string", "null"], example: "18:30" }, ticket_url: { type: ["string", "null"], format: "uri" }, price: { type: ["string", "null"], example: "前売 ¥3,000 / 当日 ¥3,500" }, image_url: { type: ["string", "null"], format: "uri" }, description: { type: ["string", "null"] }, source_url: { type: ["string", "null"], format: "uri" }, fetched_at: { type: "string", format: "date-time" }, }, }, Venue: { type: "object", properties: { id: { type: "string", example: "liquid-room" }, name: { type: "string", example: "LIQUID ROOM" }, url: { type: "string", format: "uri" }, area: { type: ["string", "null"], example: "恵比寿" }, capacity: { type: ["integer", "null"], example: 1000 }, event_count: { type: "integer" }, }, }, EventListResponse: { type: "object", properties: { events: { type: "array", items: { $ref: "#/components/schemas/Event" } }, meta: { type: "object", properties: { limit: { type: "integer" }, offset: { type: "integer" }, count: { type: "integer", description: "今回返却した件数" }, }, }, }, }, EventDetailResponse: { type: "object", properties: { event: { $ref: "#/components/schemas/Event" }, }, }, VenueListResponse: { type: "object", properties: { venues: { type: "array", items: { $ref: "#/components/schemas/Venue" } }, }, }, ScrapeStartResponse: { type: "object", properties: { run_id: { type: "string", format: "uuid" }, status: { type: "string", enum: ["started"] }, }, }, ScrapeStatusResponse: { type: "object", properties: { running: { type: "boolean" }, results: { type: "array", items: { type: "object", properties: { run_id: { type: "string" }, venue_id: { type: "string" }, venue_name: { type: "string" }, status: { type: "string", enum: ["running", "ok", "error"] }, events_saved: { type: "integer" }, error: { type: ["string", "null"] }, started_at: { type: "string", format: "date-time" }, finished_at: { type: ["string", "null"], format: "date-time" }, }, }, }, }, }, }, }, } as const; export async function loader(_: Route.LoaderArgs) { return Response.json(SPEC, { headers: { "Cache-Control": "public, max-age=3600", "Access-Control-Allow-Origin": "*", }, }); }