From baf1ad66a2cafe90da5fb45fa14c23aa742eee9b Mon Sep 17 00:00:00 2001 From: "Satsuki Akiba (aider)" Date: Wed, 4 Jun 2025 10:49:24 +0900 Subject: [PATCH] feat: migrate to Hono based implementation --- server.ts | 266 ++++++++++++++++++++++++------------------------------ 1 file changed, 118 insertions(+), 148 deletions(-) diff --git a/server.ts b/server.ts index ce1f8dd..e825a8e 100644 --- a/server.ts +++ b/server.ts @@ -1,166 +1,136 @@ -import { serve } from "bun"; +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; import fs from "fs"; import path from "path"; -import { Database } from "bun:sqlite"; // bun:sqlite は非同期が基本 +import { Database } from "bun:sqlite"; -const projectRoot = import.meta.dirname; // server.ts がプロジェクトルートにあると仮定 +const projectRoot = import.meta.dirname; +// データベースパスの設定 const dbPath = path.join(projectRoot, "data/podcast.db"); - -// data ディレクトリが存在しない場合は作成 const dataDir = path.dirname(dbPath); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } - -// データベースファイルが存在しない場合、空のファイルを作成し、スキーマを適用 const db = new Database(dbPath); if (!fs.existsSync(dbPath)) { - fs.closeSync(fs.openSync(dbPath, "w")); // 空のDBファイルを作成 + fs.closeSync(fs.openSync(dbPath, "w")); } -await db.exec(fs.readFileSync(path.join(projectRoot, "schema.sql"), "utf-8")); +db.exec(fs.readFileSync(path.join(projectRoot, "schema.sql"), "utf-8")); -// 静的ファイルを提供するディレクトリのパス設定 -// services/tts.ts の出力先は ../static/podcast_audio であり、 -// services/podcast.ts の出力先は ../public/podcast.xml であることを考慮 +// 静的ファイルパスの設定 const frontendPublicDir = path.join(projectRoot, "frontend", "public"); -const frontendBuildDir = path.join(projectRoot, "frontend", ".next"); // Next.jsアプリのビルド出力先 -const podcastAudioDir = path.join(projectRoot, "static", "podcast_audio"); // TTSが音声ファイルを保存する場所 -const generalPublicDir = path.join(projectRoot, "public"); // podcast.xml などが置かれる場所 +const frontendBuildDir = path.join(projectRoot, "frontend", ".next"); +const podcastAudioDir = path.join(projectRoot, "static", "podcast_audio"); +const generalPublicDir = path.join(projectRoot, "public"); -console.log(`Serving frontend static files from: ${frontendPublicDir}`); -console.log(`Serving frontend build artifacts from: ${frontendBuildDir}`); -console.log(`Serving podcast audio from: ${podcastAudioDir}`); -console.log(`Serving general public files (e.g., podcast.xml) from: ${generalPublicDir}`); +const app = new Hono(); -serve({ - port: 3000, - async fetch(req: Request): Promise { - const url = new URL(req.url); - const pathname = url.pathname; - - // API Routes - if (pathname === "/api/feeds") { - if (req.method === "GET") { - const rows = await db - .query("SELECT feed_url FROM processed_feed_items GROUP BY feed_url") - .all() as { feed_url: string }[]; - return new Response(JSON.stringify(rows.map((r) => r.feed_url)), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } - if (req.method === "POST") { - try { - const { feedUrl }: { feedUrl: string } = await req.json(); - console.log("Received feedUrl to add:", feedUrl); - // TODO: feedUrl をデータベースに保存する処理 - return new Response(JSON.stringify({ result: "OK" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } catch (e) { - return new Response(JSON.stringify({ error: "Invalid JSON body" }), { - status: 400, - headers: { "Content-Type": "application/json" }, - }); - } - } - } - - if (pathname === "/api/episodes") { - if (req.method === "GET") { - const episodes = await db - .query("SELECT * FROM episodes ORDER BY pubDate DESC") - .all(); - return new Response(JSON.stringify(episodes), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); - } - } - - if (pathname.startsWith("/api/episodes/") && pathname.endsWith("/regenerate")) { - if (req.method === "POST") { - const parts = pathname.split('/'); - const id = parts[3]; - console.log("Regeneration requested for episode ID:", id); - // TODO: 再生成ロジックを実装 - return new Response( - JSON.stringify({ result: `Regeneration requested for ${id}` }), - { status: 200, headers: { "Content-Type": "application/json" } }, - ); - } - } - - // Next.jsの静的ファイルを提供 - if (pathname.startsWith("/_next/")) { - const assetPath = pathname.substring("/_next/".length); - const filePath = path.join(frontendBuildDir, "_next", assetPath); - try { - const file = Bun.file(filePath); - if (await file.exists()) { - let contentType = "application/octet-stream"; - if (filePath.endsWith(".js")) contentType = "application/javascript; charset=utf-8"; - else if (filePath.endsWith(".css")) contentType = "text/css; charset=utf-8"; - else if (filePath.endsWith(".png")) contentType = "image/png"; - else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) contentType = "image/jpeg"; - // 必要に応じて他のMIMEタイプを追加 - return new Response(file, { headers: { "Content-Type": contentType } }); - } - } catch (e) { - console.error(`Error serving Next.js static file ${filePath}:`, e); - } - } - - // Serve /podcast_audio/* from static/podcast_audio - if (pathname.startsWith("/podcast_audio/")) { - const audioFileName = pathname.substring("/podcast_audio/".length); - const audioFilePath = path.join(podcastAudioDir, audioFileName); - try { - const file = Bun.file(audioFilePath); - if (await file.exists()) { - return new Response(file, { headers: { "Content-Type": "audio/mpeg" } }); - } - } catch (e) { - console.error(`Error serving audio file ${audioFilePath}:`, e); - } - } - - // Serve /podcast.xml from generalPublicDir (project_root/public/podcast.xml) - if (pathname === "/podcast.xml") { - const filePath = path.join(generalPublicDir, "podcast.xml"); - try { - const file = Bun.file(filePath); - if (await file.exists()) { - return new Response(file, { headers: { "Content-Type": "application/xml; charset=utf-8" } }); - } - } catch (e) { - console.error(`Error serving podcast.xml ${filePath}:`, e); - } - } - - // Next.jsの静的ファイルを提供するディレクトリのパスを指定 - const indexPath = path.join(frontendBuildDir, "server", "pages", "index.html"); - - // 通常のリクエストは静的ファイルで処理 - try { - const file = Bun.file(indexPath); - if (await file.exists()) { - return new Response(file, { - headers: { "Content-Type": "text/html; charset=utf-8" } - }); - } - } catch (e) { - console.error("Error serving index.html:", e); - } - - return new Response("Not Found", { status: 404 }); - }, - error(error: Error): Response { - console.error("Server error:", error); - return new Response("Internal Server Error", { status: 500 }); - }, +// APIルート +app.get("/api/feeds", async (c) => { + const rows = db + .query("SELECT feed_url FROM processed_feed_items GROUP BY feed_url") + .all() as { feed_url: string }[]; + return c.json(rows.map((r) => r.feed_url)); }); -console.log("Server is running on http://localhost:3000"); +app.post("/api/feeds", async (c) => { + try { + const { feedUrl } = await c.req.json<{ feedUrl: string }>(); + console.log("Received feedUrl to add:", feedUrl); + // TODO: feedUrl をデータベースに保存する処理 + return c.json({ result: "OK" }); + } catch (e) { + return c.json({ error: "Invalid JSON body" }, 400); + } +}); + +app.get("/api/episodes", (c) => { + const episodes = db + .query("SELECT * FROM episodes ORDER BY pubDate DESC") + .all(); + return c.json(episodes); +}); + +app.post("/api/episodes/:id/regenerate", (c) => { + const id = c.req.param("id"); + console.log("Regeneration requested for episode ID:", id); + // TODO: 再生成ロジックを実装 + return c.json({ result: `Regeneration requested for ${id}` }); +}); + +// 静的ファイルの処理 + +// Next.jsのビルドファイル +app.get("/_next/*", async (c) => { + const assetPath = c.req.path.substring("/_next/".length); + const filePath = path.join(frontendBuildDir, "_next", assetPath); + try { + const file = Bun.file(filePath); + if (await file.exists()) { + let contentType = "application/octet-stream"; + if (filePath.endsWith(".js")) contentType = "application/javascript; charset=utf-8"; + else if (filePath.endsWith(".css")) contentType = "text/css; charset=utf-8"; + else if (filePath.endsWith(".png")) contentType = "image/png"; + else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) contentType = "image/jpeg"; + return c.body(file, 200, { "Content-Type": contentType }); + } + } catch (e) { + console.error(`Error serving Next.js static file ${filePath}:`, e); + } + return c.notFound(); +}); + +// podcast_audio +app.get("/podcast_audio/*", async (c) => { + const audioFileName = c.req.path.substring("/podcast_audio/".length); + const audioFilePath = path.join(podcastAudioDir, audioFileName); + try { + const file = Bun.file(audioFilePath); + if (await file.exists()) { + return c.body(file, 200, { "Content-Type": "audio/mpeg" }); + } + } catch (e) { + console.error(`Error serving audio file ${audioFilePath}:`, e); + } + return c.notFound(); +}); + +// podcast.xml +app.get("/podcast.xml", async (c) => { + const filePath = path.join(generalPublicDir, "podcast.xml"); + try { + const file = Bun.file(filePath); + if (await file.exists()) { + return c.body(file, 200, { "Content-Type": "application/xml; charset=utf-8" }); + } + } catch (e) { + console.error(`Error serving podcast.xml ${filePath}:`, e); + } + return c.notFound(); +}); + +// フォールバックとして index.html +app.get("*", async (c) => { + const indexPath = path.join(frontendBuildDir, "server", "pages", "index.html"); + try { + const file = Bun.file(indexPath); + if (await file.exists()) { + return c.body(file, 200, { "Content-Type": "text/html; charset=utf-8" }); + } + } catch (e) { + console.error("Error serving index.html:", e); + } + return c.notFound(); +}); + +// サーバー起動 +serve( + { + fetch: app.fetch, + port: 3000, + }, + (info) => { + console.log(`Server is running on http://localhost:${info.port}`); + } +);