feat: migrate to Hono based implementation

This commit is contained in:
2025-06-04 10:49:24 +09:00
parent acdeb0ede9
commit baf1ad66a2

154
server.ts
View File

@ -1,101 +1,69 @@
import { serve } from "bun"; import { Hono } from "hono";
import { serve } from "@hono/node-server";
import fs from "fs"; import fs from "fs";
import path from "path"; 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"); const dbPath = path.join(projectRoot, "data/podcast.db");
// data ディレクトリが存在しない場合は作成
const dataDir = path.dirname(dbPath); const dataDir = path.dirname(dbPath);
if (!fs.existsSync(dataDir)) { if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true }); fs.mkdirSync(dataDir, { recursive: true });
} }
// データベースファイルが存在しない場合、空のファイルを作成し、スキーマを適用
const db = new Database(dbPath); const db = new Database(dbPath);
if (!fs.existsSync(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 frontendPublicDir = path.join(projectRoot, "frontend", "public");
const frontendBuildDir = path.join(projectRoot, "frontend", ".next"); // Next.jsアプリのビルド出力先 const frontendBuildDir = path.join(projectRoot, "frontend", ".next");
const podcastAudioDir = path.join(projectRoot, "static", "podcast_audio"); // TTSが音声ファイルを保存する場所 const podcastAudioDir = path.join(projectRoot, "static", "podcast_audio");
const generalPublicDir = path.join(projectRoot, "public"); // podcast.xml などが置かれる場所 const generalPublicDir = path.join(projectRoot, "public");
console.log(`Serving frontend static files from: ${frontendPublicDir}`); const app = new Hono();
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}`);
serve({ // APIルート
port: 3000, app.get("/api/feeds", async (c) => {
async fetch(req: Request): Promise<Response> { const rows = db
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") .query("SELECT feed_url FROM processed_feed_items GROUP BY feed_url")
.all() as { feed_url: string }[]; .all() as { feed_url: string }[];
return new Response(JSON.stringify(rows.map((r) => r.feed_url)), { return c.json(rows.map((r) => r.feed_url));
status: 200,
headers: { "Content-Type": "application/json" },
}); });
}
if (req.method === "POST") { app.post("/api/feeds", async (c) => {
try { try {
const { feedUrl }: { feedUrl: string } = await req.json(); const { feedUrl } = await c.req.json<{ feedUrl: string }>();
console.log("Received feedUrl to add:", feedUrl); console.log("Received feedUrl to add:", feedUrl);
// TODO: feedUrl をデータベースに保存する処理 // TODO: feedUrl をデータベースに保存する処理
return new Response(JSON.stringify({ result: "OK" }), { return c.json({ result: "OK" });
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (e) { } catch (e) {
return new Response(JSON.stringify({ error: "Invalid JSON body" }), { return c.json({ error: "Invalid JSON body" }, 400);
status: 400, }
headers: { "Content-Type": "application/json" },
}); });
}
}
}
if (pathname === "/api/episodes") { app.get("/api/episodes", (c) => {
if (req.method === "GET") { const episodes = db
const episodes = await db
.query("SELECT * FROM episodes ORDER BY pubDate DESC") .query("SELECT * FROM episodes ORDER BY pubDate DESC")
.all(); .all();
return new Response(JSON.stringify(episodes), { return c.json(episodes);
status: 200,
headers: { "Content-Type": "application/json" },
}); });
}
}
if (pathname.startsWith("/api/episodes/") && pathname.endsWith("/regenerate")) { app.post("/api/episodes/:id/regenerate", (c) => {
if (req.method === "POST") { const id = c.req.param("id");
const parts = pathname.split('/');
const id = parts[3];
console.log("Regeneration requested for episode ID:", id); console.log("Regeneration requested for episode ID:", id);
// TODO: 再生成ロジックを実装 // TODO: 再生成ロジックを実装
return new Response( return c.json({ result: `Regeneration requested for ${id}` });
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); // Next.jsのビルドファイル
app.get("/_next/*", async (c) => {
const assetPath = c.req.path.substring("/_next/".length);
const filePath = path.join(frontendBuildDir, "_next", assetPath); const filePath = path.join(frontendBuildDir, "_next", assetPath);
try { try {
const file = Bun.file(filePath); const file = Bun.file(filePath);
@ -105,62 +73,64 @@ serve({
else if (filePath.endsWith(".css")) contentType = "text/css; 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(".png")) contentType = "image/png";
else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) contentType = "image/jpeg"; else if (filePath.endsWith(".jpg") || filePath.endsWith(".jpeg")) contentType = "image/jpeg";
// 必要に応じて他のMIMEタイプを追加 return c.body(file, 200, { "Content-Type": contentType });
return new Response(file, { headers: { "Content-Type": contentType } });
} }
} catch (e) { } catch (e) {
console.error(`Error serving Next.js static file ${filePath}:`, e); console.error(`Error serving Next.js static file ${filePath}:`, e);
} }
} return c.notFound();
});
// Serve /podcast_audio/* from static/podcast_audio // podcast_audio
if (pathname.startsWith("/podcast_audio/")) { app.get("/podcast_audio/*", async (c) => {
const audioFileName = pathname.substring("/podcast_audio/".length); const audioFileName = c.req.path.substring("/podcast_audio/".length);
const audioFilePath = path.join(podcastAudioDir, audioFileName); const audioFilePath = path.join(podcastAudioDir, audioFileName);
try { try {
const file = Bun.file(audioFilePath); const file = Bun.file(audioFilePath);
if (await file.exists()) { if (await file.exists()) {
return new Response(file, { headers: { "Content-Type": "audio/mpeg" } }); return c.body(file, 200, { "Content-Type": "audio/mpeg" });
} }
} catch (e) { } catch (e) {
console.error(`Error serving audio file ${audioFilePath}:`, e); console.error(`Error serving audio file ${audioFilePath}:`, e);
} }
} return c.notFound();
});
// Serve /podcast.xml from generalPublicDir (project_root/public/podcast.xml) // podcast.xml
if (pathname === "/podcast.xml") { app.get("/podcast.xml", async (c) => {
const filePath = path.join(generalPublicDir, "podcast.xml"); const filePath = path.join(generalPublicDir, "podcast.xml");
try { try {
const file = Bun.file(filePath); const file = Bun.file(filePath);
if (await file.exists()) { if (await file.exists()) {
return new Response(file, { headers: { "Content-Type": "application/xml; charset=utf-8" } }); return c.body(file, 200, { "Content-Type": "application/xml; charset=utf-8" });
} }
} catch (e) { } catch (e) {
console.error(`Error serving podcast.xml ${filePath}:`, e); console.error(`Error serving podcast.xml ${filePath}:`, e);
} }
} return c.notFound();
});
// Next.jsの静的ファイルを提供するディレクトリのパスを指定 // フォールバックとして index.html
app.get("*", async (c) => {
const indexPath = path.join(frontendBuildDir, "server", "pages", "index.html"); const indexPath = path.join(frontendBuildDir, "server", "pages", "index.html");
// 通常のリクエストは静的ファイルで処理
try { try {
const file = Bun.file(indexPath); const file = Bun.file(indexPath);
if (await file.exists()) { if (await file.exists()) {
return new Response(file, { return c.body(file, 200, { "Content-Type": "text/html; charset=utf-8" });
headers: { "Content-Type": "text/html; charset=utf-8" }
});
} }
} catch (e) { } catch (e) {
console.error("Error serving index.html:", e); console.error("Error serving index.html:", e);
} }
return c.notFound();
return new Response("Not Found", { status: 404 });
},
error(error: Error): Response {
console.error("Server error:", error);
return new Response("Internal Server Error", { status: 500 });
},
}); });
console.log("Server is running on http://localhost:3000"); // サーバー起動
serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`);
}
);