Add searching feature
This commit is contained in:
132
server.ts
132
server.ts
@ -128,15 +128,18 @@ app.get("/podcast/category/:category.xml", async (c) => {
|
||||
return c.notFound();
|
||||
}
|
||||
const { generateCategoryRSS } = await import("./services/podcast.js");
|
||||
|
||||
|
||||
const rssXml = await generateCategoryRSS(category);
|
||||
|
||||
|
||||
return c.body(rssXml, 200, {
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error generating category RSS for "${c.req.param("category")}":`, error);
|
||||
console.error(
|
||||
`Error generating category RSS for "${c.req.param("category")}":`,
|
||||
error,
|
||||
);
|
||||
return c.notFound();
|
||||
}
|
||||
});
|
||||
@ -149,15 +152,18 @@ app.get("/podcast/feed/:feedId.xml", async (c) => {
|
||||
return c.notFound();
|
||||
}
|
||||
const { generateFeedRSS } = await import("./services/podcast.js");
|
||||
|
||||
|
||||
const rssXml = await generateFeedRSS(feedId);
|
||||
|
||||
|
||||
return c.body(rssXml, 200, {
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=3600", // Cache for 1 hour
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error generating feed RSS for "${c.req.param("feedId")}":`, error);
|
||||
console.error(
|
||||
`Error generating feed RSS for "${c.req.param("feedId")}":`,
|
||||
error,
|
||||
);
|
||||
return c.notFound();
|
||||
}
|
||||
});
|
||||
@ -206,9 +212,11 @@ async function serveIndex(c: any) {
|
||||
async function serveEpisodePage(c: any, episodeId: string) {
|
||||
try {
|
||||
// First, try to get episode from database
|
||||
const { fetchEpisodeWithSourceInfo } = await import("./services/database.js");
|
||||
const { fetchEpisodeWithSourceInfo } = await import(
|
||||
"./services/database.js"
|
||||
);
|
||||
let episode = null;
|
||||
|
||||
|
||||
try {
|
||||
episode = await fetchEpisodeWithSourceInfo(episodeId);
|
||||
} catch (error) {
|
||||
@ -267,34 +275,32 @@ async function serveEpisodePage(c: any, episodeId: string) {
|
||||
}
|
||||
|
||||
let html = await file.text();
|
||||
|
||||
|
||||
// Extract the base URL
|
||||
const protocol = c.req.header("x-forwarded-proto") || "http";
|
||||
const host = c.req.header("host") || "localhost:3000";
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
|
||||
|
||||
// Replace title and add OG metadata
|
||||
const title = `${episode.title} - Voice RSS Summary`;
|
||||
const description = episode.description || `${episode.title}のエピソード詳細`;
|
||||
const description =
|
||||
episode.description || `${episode.title}のエピソード詳細`;
|
||||
const episodeUrl = `${baseUrl}/episode/${episodeId}`;
|
||||
const imageUrl = `${baseUrl}/default-thumbnail.svg`;
|
||||
const audioUrl = episode.audioPath.startsWith('/')
|
||||
? `${baseUrl}${episode.audioPath}`
|
||||
const audioUrl = episode.audioPath.startsWith("/")
|
||||
? `${baseUrl}${episode.audioPath}`
|
||||
: `${baseUrl}/podcast_audio/${episode.audioPath}`;
|
||||
|
||||
// Replace the title
|
||||
html = html.replace(
|
||||
/<title>.*?<\/title>/,
|
||||
`<title>${title}</title>`
|
||||
);
|
||||
html = html.replace(/<title>.*?<\/title>/, `<title>${title}</title>`);
|
||||
|
||||
// Add OG metadata before closing </head>
|
||||
const ogMetadata = `
|
||||
<meta name="description" content="${description.replace(/"/g, '"')}" />
|
||||
<meta name="description" content="${description.replace(/"/g, """)}" />
|
||||
|
||||
<!-- OpenGraph metadata -->
|
||||
<meta property="og:title" content="${episode.title.replace(/"/g, '"')}" />
|
||||
<meta property="og:description" content="${description.replace(/"/g, '"')}" />
|
||||
<meta property="og:title" content="${episode.title.replace(/"/g, """)}" />
|
||||
<meta property="og:description" content="${description.replace(/"/g, """)}" />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content="${episodeUrl}" />
|
||||
<meta property="og:site_name" content="Voice RSS Summary" />
|
||||
@ -303,25 +309,28 @@ async function serveEpisodePage(c: any, episodeId: string) {
|
||||
<meta property="og:image:type" content="image/svg+xml" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="${episode.title.replace(/"/g, '"')} - Voice RSS Summary" />
|
||||
<meta property="og:image:alt" content="${episode.title.replace(/"/g, """)} - Voice RSS Summary" />
|
||||
<meta property="og:audio" content="${audioUrl}" />
|
||||
<meta property="og:audio:type" content="audio/mpeg" />
|
||||
|
||||
<!-- Twitter Card metadata -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="${episode.title.replace(/"/g, '"')}" />
|
||||
<meta name="twitter:description" content="${description.replace(/"/g, '"')}" />
|
||||
<meta name="twitter:title" content="${episode.title.replace(/"/g, """)}" />
|
||||
<meta name="twitter:description" content="${description.replace(/"/g, """)}" />
|
||||
<meta name="twitter:image" content="${imageUrl}" />
|
||||
<meta name="twitter:image:alt" content="${episode.title.replace(/"/g, '"')} - Voice RSS Summary" />
|
||||
<meta name="twitter:image:alt" content="${episode.title.replace(/"/g, """)} - Voice RSS Summary" />
|
||||
|
||||
<!-- Article metadata -->
|
||||
<meta property="article:published_time" content="${episode.createdAt}" />
|
||||
${episode.articlePubDate && episode.articlePubDate !== episode.createdAt ?
|
||||
`<meta property="article:modified_time" content="${episode.articlePubDate}" />` : ''}
|
||||
${episode.feedTitle ? `<meta property="article:section" content="${episode.feedTitle.replace(/"/g, '"')}" />` : ''}
|
||||
${
|
||||
episode.articlePubDate && episode.articlePubDate !== episode.createdAt
|
||||
? `<meta property="article:modified_time" content="${episode.articlePubDate}" />`
|
||||
: ""
|
||||
}
|
||||
${episode.feedTitle ? `<meta property="article:section" content="${episode.feedTitle.replace(/"/g, """)}" />` : ""}
|
||||
`;
|
||||
|
||||
html = html.replace('</head>', `${ogMetadata}</head>`);
|
||||
html = html.replace("</head>", `${ogMetadata}</head>`);
|
||||
|
||||
return c.body(html, 200, { "Content-Type": "text/html; charset=utf-8" });
|
||||
} catch (error) {
|
||||
@ -334,7 +343,6 @@ app.get("/", serveIndex);
|
||||
|
||||
app.get("/index.html", serveIndex);
|
||||
|
||||
|
||||
// API endpoints for frontend
|
||||
app.get("/api/episodes", async (c) => {
|
||||
try {
|
||||
@ -517,7 +525,9 @@ app.get("/api/feeds/by-category", async (c) => {
|
||||
|
||||
app.get("/api/feeds/grouped-by-category", async (c) => {
|
||||
try {
|
||||
const { getFeedsGroupedByCategory } = await import("./services/database.js");
|
||||
const { getFeedsGroupedByCategory } = await import(
|
||||
"./services/database.js"
|
||||
);
|
||||
const groupedFeeds = await getFeedsGroupedByCategory();
|
||||
return c.json({ groupedFeeds });
|
||||
} catch (error) {
|
||||
@ -558,6 +568,26 @@ app.get("/api/episodes-with-feed-info", async (c) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/episodes/search", async (c) => {
|
||||
try {
|
||||
const query = c.req.query("q");
|
||||
const category = c.req.query("category");
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
return c.json({ error: "Search query is required" }, 400);
|
||||
}
|
||||
|
||||
const { searchEpisodesWithFeedInfo } = await import(
|
||||
"./services/database.js"
|
||||
);
|
||||
const episodes = await searchEpisodesWithFeedInfo(query.trim(), category);
|
||||
return c.json({ episodes, query, category });
|
||||
} catch (error) {
|
||||
console.error("Error searching episodes:", error);
|
||||
return c.json({ error: "Failed to search episodes" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/episode-with-source/:episodeId", async (c) => {
|
||||
try {
|
||||
const episodeId = c.req.param("episodeId");
|
||||
@ -629,12 +659,17 @@ app.get("/api/episodes/by-category", async (c) => {
|
||||
|
||||
app.get("/api/episodes/grouped-by-category", async (c) => {
|
||||
try {
|
||||
const { getEpisodesGroupedByCategory } = await import("./services/database.js");
|
||||
const { getEpisodesGroupedByCategory } = await import(
|
||||
"./services/database.js"
|
||||
);
|
||||
const groupedEpisodes = await getEpisodesGroupedByCategory();
|
||||
return c.json({ groupedEpisodes });
|
||||
} catch (error) {
|
||||
console.error("Error fetching episodes grouped by category:", error);
|
||||
return c.json({ error: "Failed to fetch episodes grouped by category" }, 500);
|
||||
return c.json(
|
||||
{ error: "Failed to fetch episodes grouped by category" },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -652,14 +687,13 @@ app.get("/api/episode-category-stats", async (c) => {
|
||||
// RSS endpoints information API
|
||||
app.get("/api/rss-endpoints", async (c) => {
|
||||
try {
|
||||
const {
|
||||
getAllEpisodeCategories,
|
||||
fetchActiveFeeds
|
||||
} = await import("./services/database.js");
|
||||
|
||||
const { getAllEpisodeCategories, fetchActiveFeeds } = await import(
|
||||
"./services/database.js"
|
||||
);
|
||||
|
||||
const [episodeCategories, activeFeeds] = await Promise.all([
|
||||
getAllEpisodeCategories(),
|
||||
fetchActiveFeeds()
|
||||
getAllEpisodeCategories(),
|
||||
fetchActiveFeeds(),
|
||||
]);
|
||||
|
||||
const protocol = c.req.header("x-forwarded-proto") || "http";
|
||||
@ -670,19 +704,19 @@ app.get("/api/rss-endpoints", async (c) => {
|
||||
main: {
|
||||
title: "全エピソード",
|
||||
url: `${baseUrl}/podcast.xml`,
|
||||
description: "すべてのエピソードを含むメインRSSフィード"
|
||||
description: "すべてのエピソードを含むメインRSSフィード",
|
||||
},
|
||||
categories: episodeCategories.map(category => ({
|
||||
categories: episodeCategories.map((category) => ({
|
||||
title: `カテゴリ: ${category}`,
|
||||
url: `${baseUrl}/podcast/category/${encodeURIComponent(category)}.xml`,
|
||||
description: `「${category}」カテゴリのエピソードのみ`
|
||||
description: `「${category}」カテゴリのエピソードのみ`,
|
||||
})),
|
||||
feeds: activeFeeds.map(feed => ({
|
||||
feeds: activeFeeds.map((feed) => ({
|
||||
title: `フィード: ${feed.title || feed.url}`,
|
||||
url: `${baseUrl}/podcast/feed/${feed.id}.xml`,
|
||||
description: `「${feed.title || feed.url}」からのエピソードのみ`,
|
||||
feedCategory: feed.category
|
||||
}))
|
||||
feedCategory: feed.category,
|
||||
})),
|
||||
};
|
||||
|
||||
return c.json({ endpoints });
|
||||
@ -705,14 +739,14 @@ app.get("*", serveIndex);
|
||||
console.log("🔄 Batch scheduler initialized and ready");
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n🛑 Received SIGINT. Graceful shutdown...');
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\n🛑 Received SIGINT. Graceful shutdown...");
|
||||
await closeBrowser();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
console.log('\n🛑 Received SIGTERM. Graceful shutdown...');
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\n🛑 Received SIGTERM. Graceful shutdown...");
|
||||
await closeBrowser();
|
||||
process.exit(0);
|
||||
});
|
||||
|
Reference in New Issue
Block a user