diff --git a/admin-server.ts b/admin-server.ts
index 36af725..9e006ce 100644
--- a/admin-server.ts
+++ b/admin-server.ts
@@ -8,8 +8,8 @@ import { batchScheduler } from "./services/batch-scheduler.js";
import { config, validateConfig } from "./services/config.js";
import { closeBrowser } from "./services/content-extractor.js";
import {
- deleteFeed,
deleteEpisode,
+ deleteFeed,
fetchAllEpisodes,
fetchEpisodesWithArticles,
getAllCategories,
@@ -412,9 +412,9 @@ app.get("/api/admin/db-diagnostic", async (c) => {
} catch (error) {
console.error("Error running database diagnostic:", error);
return c.json(
- {
- error: "Failed to run database diagnostic",
- details: error instanceof Error ? error.message : String(error)
+ {
+ error: "Failed to run database diagnostic",
+ details: error instanceof Error ? error.message : String(error),
},
500,
);
@@ -702,14 +702,14 @@ app.get("/index.html", serveAdminIndex);
app.get("*", serveAdminIndex);
// 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);
});
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index bd2cceb..9d01031 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -9,9 +9,13 @@ import RSSEndpoints from "./components/RSSEndpoints";
function App() {
const location = useLocation();
- const isMainPage = ["/", "/feeds", "/categories", "/feed-requests", "/rss-endpoints"].includes(
- location.pathname,
- );
+ const isMainPage = [
+ "/",
+ "/feeds",
+ "/categories",
+ "/feed-requests",
+ "/rss-endpoints",
+ ].includes(location.pathname);
return (
diff --git a/frontend/src/components/CategoryList.tsx b/frontend/src/components/CategoryList.tsx
index de634a8..b6b5e8a 100644
--- a/frontend/src/components/CategoryList.tsx
+++ b/frontend/src/components/CategoryList.tsx
@@ -70,7 +70,6 @@ function CategoryList() {
return groupedFeeds[category]?.length || 0;
};
-
if (loading) {
return
読み込み中...
;
}
@@ -138,7 +137,10 @@ function CategoryList() {
{groupedFeeds[category]?.slice(0, 3).map((feed) => (
-
+
{feed.title || feed.url}
@@ -387,4 +389,4 @@ function CategoryList() {
);
}
-export default CategoryList;
\ No newline at end of file
+export default CategoryList;
diff --git a/frontend/src/components/EpisodeDetail.tsx b/frontend/src/components/EpisodeDetail.tsx
index 168006d..abd46ac 100644
--- a/frontend/src/components/EpisodeDetail.tsx
+++ b/frontend/src/components/EpisodeDetail.tsx
@@ -182,11 +182,17 @@ function EpisodeDetail() {
{/* Image metadata */}
-
+
-
+
{/* Twitter Card metadata */}
@@ -195,8 +201,14 @@ function EpisodeDetail() {
name="twitter:description"
content={episode.description || `${episode.title}のエピソード詳細`}
/>
-
-
+
+
{/* Audio-specific metadata */}
([]);
- const [filteredEpisodes, setFilteredEpisodes] = useState
([]);
+ const [filteredEpisodes, setFilteredEpisodes] = useState<
+ EpisodeWithFeedInfo[]
+ >([]);
const [categories, setCategories] = useState([]);
const [selectedCategory, setSelectedCategory] = useState("");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [isSearching, setIsSearching] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [currentAudio, setCurrentAudio] = useState(null);
@@ -46,8 +50,12 @@ function EpisodeList() {
}, [useDatabase]);
useEffect(() => {
- filterEpisodesByCategory();
- }, [episodes, selectedCategory]);
+ if (searchQuery.trim()) {
+ performSearch();
+ } else {
+ filterEpisodesByCategory();
+ }
+ }, [episodes, selectedCategory, searchQuery]);
const fetchEpisodes = async () => {
try {
@@ -127,12 +135,41 @@ function EpisodeList() {
}
};
+ const performSearch = async () => {
+ try {
+ setIsSearching(true);
+ setError(null);
+
+ const searchParams = new URLSearchParams({
+ q: searchQuery.trim(),
+ });
+
+ if (selectedCategory) {
+ searchParams.append("category", selectedCategory);
+ }
+
+ const response = await fetch(`/api/episodes/search?${searchParams}`);
+ if (!response.ok) {
+ throw new Error("検索に失敗しました");
+ }
+
+ const data = await response.json();
+ setFilteredEpisodes(data.episodes || []);
+ } catch (err) {
+ console.error("Search error:", err);
+ setError(err instanceof Error ? err.message : "検索エラーが発生しました");
+ setFilteredEpisodes([]);
+ } finally {
+ setIsSearching(false);
+ }
+ };
+
const filterEpisodesByCategory = () => {
if (!selectedCategory) {
setFilteredEpisodes(episodes);
} else {
- const filtered = episodes.filter(episode =>
- episode.feedCategory === selectedCategory
+ const filtered = episodes.filter(
+ (episode) => episode.feedCategory === selectedCategory,
);
setFilteredEpisodes(filtered);
}
@@ -197,19 +234,53 @@ function EpisodeList() {
return {error}
;
}
- if (filteredEpisodes.length === 0 && episodes.length > 0 && selectedCategory) {
- return (
-
-
カテゴリ「{selectedCategory}」のエピソードがありません
-
-
- );
+ if (filteredEpisodes.length === 0 && episodes.length > 0) {
+ if (searchQuery.trim()) {
+ return (
+
+
「{searchQuery}」の検索結果がありません
+ {selectedCategory && (
+
カテゴリ「{selectedCategory}」内で検索しています
+ )}
+
+
+ {selectedCategory && (
+
+ )}
+
+
+ );
+ } else if (selectedCategory) {
+ return (
+
+
カテゴリ「{selectedCategory}」のエピソードがありません
+
+
+ );
+ }
}
if (episodes.length === 0) {
@@ -235,22 +306,79 @@ function EpisodeList() {
-
エピソード一覧 ({selectedCategory ? `${filteredEpisodes.length}/${episodes.length}` : episodes.length}件)
-
+
+
+ エピソード一覧 (
+ {searchQuery
+ ? `検索結果: ${filteredEpisodes.length}件`
+ : selectedCategory
+ ? `${filteredEpisodes.length}/${episodes.length}件`
+ : `${episodes.length}件`}
+ )
+
+
+
+ データソース: {useDatabase ? "データベース" : "XML"}
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ style={{
+ flex: "1",
+ minWidth: "200px",
+ padding: "8px 12px",
+ fontSize: "14px",
+ border: "1px solid #ccc",
+ borderRadius: "4px",
+ }}
+ />
+ {searchQuery && (
+
+ )}
{categories.length > 0 && (
)}
-
- データソース: {useDatabase ? "データベース" : "XML"}
-
-
+ {isSearching && (
+ 検索中...
+ )}
@@ -309,7 +434,13 @@ function EpisodeList() {
{episode.feedTitle}
{episode.feedCategory && (
-
+
({episode.feedCategory})
)}
diff --git a/frontend/src/components/FeedList.tsx b/frontend/src/components/FeedList.tsx
index 133726b..e538b77 100644
--- a/frontend/src/components/FeedList.tsx
+++ b/frontend/src/components/FeedList.tsx
@@ -63,8 +63,8 @@ function FeedList() {
if (!selectedCategory) {
setFilteredFeeds(feeds);
} else {
- const filtered = feeds.filter(feed =>
- feed.category === selectedCategory
+ const filtered = feeds.filter(
+ (feed) => feed.category === selectedCategory,
);
setFilteredFeeds(filtered);
}
@@ -125,7 +125,13 @@ function FeedList() {
alignItems: "center",
}}
>
- フィード一覧 ({selectedCategory ? `${filteredFeeds.length}/${feeds.length}` : feeds.length}件)
+
+ フィード一覧 (
+ {selectedCategory
+ ? `${filteredFeeds.length}/${feeds.length}`
+ : feeds.length}
+ 件)
+
{categories.length > 0 && (
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 5357b03..3101b56 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -402,7 +402,7 @@ body {
background: none;
border: none;
color: #495057;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
font-size: 0.9rem;
word-break: break-all;
flex: 1;
@@ -419,7 +419,8 @@ body {
flex-shrink: 0;
}
-.copy-btn, .open-btn {
+.copy-btn,
+.open-btn {
background: #3498db;
color: white;
border: none;
@@ -435,7 +436,8 @@ body {
justify-content: center;
}
-.copy-btn:hover, .open-btn:hover {
+.copy-btn:hover,
+.open-btn:hover {
background: #2980b9;
transform: scale(1.05);
}
@@ -497,26 +499,26 @@ body {
.rss-endpoints {
padding: 0.5rem;
}
-
+
.rss-endpoints-grid {
grid-template-columns: 1fr;
}
-
+
.endpoint-url {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
-
+
.endpoint-url code {
min-width: unset;
text-align: center;
}
-
+
.endpoint-actions {
justify-content: center;
}
-
+
.usage-cards {
grid-template-columns: 1fr;
}
diff --git a/scripts/fetch_and_generate.ts b/scripts/fetch_and_generate.ts
index e5545b9..ea1a21b 100644
--- a/scripts/fetch_and_generate.ts
+++ b/scripts/fetch_and_generate.ts
@@ -2,7 +2,10 @@ import crypto from "crypto";
import fs from "fs/promises";
import Parser from "rss-parser";
import { config } from "../services/config.js";
-import { enhanceArticleContent, closeBrowser } from "../services/content-extractor.js";
+import {
+ closeBrowser,
+ enhanceArticleContent,
+} from "../services/content-extractor.js";
import {
getFeedById,
getFeedByUrl,
@@ -13,8 +16,8 @@ import {
saveFeed,
} from "../services/database.js";
import {
- openAI_ClassifyFeed,
openAI_ClassifyEpisode,
+ openAI_ClassifyFeed,
openAI_GeneratePodcastContent,
} from "../services/llm.js";
import { updatePodcastRSS } from "../services/podcast.js";
@@ -426,7 +429,7 @@ async function processRetryQueue(abortSignal?: AbortSignal): Promise {
closeError,
);
}
-
+
// Close Puppeteer browser on exit
await closeBrowser();
}
diff --git a/server.ts b/server.ts
index 97917b1..d6de888 100644
--- a/server.ts
+++ b/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}`
- );
+ html = html.replace(/.*?<\/title>/, `${title}`);
// Add OG metadata before closing
const ogMetadata = `
-
+
-
-
+
+
@@ -303,25 +309,28 @@ async function serveEpisodePage(c: any, episodeId: string) {
-
+
-
-
+
+
-
+
- ${episode.articlePubDate && episode.articlePubDate !== episode.createdAt ?
- `` : ''}
- ${episode.feedTitle ? `` : ''}
+ ${
+ episode.articlePubDate && episode.articlePubDate !== episode.createdAt
+ ? ``
+ : ""
+ }
+ ${episode.feedTitle ? `` : ""}
`;
- html = html.replace('', `${ogMetadata}`);
+ html = html.replace("", `${ogMetadata}`);
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);
});
diff --git a/services/batch-scheduler.ts b/services/batch-scheduler.ts
index c1ef80d..5a9a386 100644
--- a/services/batch-scheduler.ts
+++ b/services/batch-scheduler.ts
@@ -103,9 +103,12 @@ class BatchScheduler {
}
// Episode category migration
- const { migrateEpisodesWithCategories, getEpisodeCategoryMigrationStatus } =
- await import("./database.js");
- const episodeMigrationStatus = await getEpisodeCategoryMigrationStatus();
+ const {
+ migrateEpisodesWithCategories,
+ getEpisodeCategoryMigrationStatus,
+ } = await import("./database.js");
+ const episodeMigrationStatus =
+ await getEpisodeCategoryMigrationStatus();
if (!episodeMigrationStatus.migrationComplete) {
console.log("🔄 Running episode category migration...");
@@ -117,10 +120,7 @@ class BatchScheduler {
this.migrationCompleted = true;
} catch (migrationError) {
- console.error(
- "❌ Error during category migrations:",
- migrationError,
- );
+ console.error("❌ Error during category migrations:", migrationError);
// Don't fail the entire batch process due to migration error
this.migrationCompleted = true; // Mark as completed to avoid retrying every batch
}
diff --git a/services/content-extractor.ts b/services/content-extractor.ts
index 79fe32a..c10f2c6 100644
--- a/services/content-extractor.ts
+++ b/services/content-extractor.ts
@@ -67,7 +67,7 @@ export async function extractArticleContent(
}
// Wait for potential dynamic content
- await new Promise(resolve => setTimeout(resolve, 2000));
+ await new Promise((resolve) => setTimeout(resolve, 2000));
// Extract content using page.evaluate
const extractedData = await page.evaluate(() => {
@@ -112,7 +112,9 @@ export async function extractArticleContent(
"";
// Extract description
- const descriptionMeta = document.querySelector('meta[name="description"]');
+ const descriptionMeta = document.querySelector(
+ 'meta[name="description"]',
+ );
const ogDescriptionMeta = document.querySelector(
'meta[property="og:description"]',
);
diff --git a/services/database.ts b/services/database.ts
index 631087f..815c298 100644
--- a/services/database.ts
+++ b/services/database.ts
@@ -218,7 +218,9 @@ function initializeDatabase(): Database {
// Ensure the category column exists in episodes
const episodeInfos = db.prepare("PRAGMA table_info(episodes);").all();
- const hasEpisodeCategory = episodeInfos.some((col: any) => col.name === "category");
+ const hasEpisodeCategory = episodeInfos.some(
+ (col: any) => col.name === "category",
+ );
if (!hasEpisodeCategory) {
db.exec("ALTER TABLE episodes ADD COLUMN category TEXT DEFAULT NULL;");
@@ -581,6 +583,88 @@ export async function fetchEpisodeWithSourceInfo(
}
}
+// Search episodes with feed information
+export async function searchEpisodesWithFeedInfo(
+ query: string,
+ category?: string,
+): Promise {
+ try {
+ let whereClause = `
+ WHERE f.active = 1
+ AND (
+ e.title LIKE ?
+ OR e.description LIKE ?
+ OR a.title LIKE ?
+ OR a.description LIKE ?
+ OR a.content LIKE ?
+ )
+ `;
+
+ const searchPattern = `%${query}%`;
+ const params = [
+ searchPattern,
+ searchPattern,
+ searchPattern,
+ searchPattern,
+ searchPattern,
+ ];
+
+ if (category) {
+ whereClause += " AND f.category = ?";
+ params.push(category);
+ }
+
+ const stmt = db.prepare(`
+ SELECT
+ e.id,
+ e.title,
+ e.description,
+ e.audio_path as audioPath,
+ e.duration,
+ e.file_size as fileSize,
+ e.category,
+ e.created_at as createdAt,
+ e.article_id as articleId,
+ a.title as articleTitle,
+ a.link as articleLink,
+ a.pub_date as articlePubDate,
+ f.id as feedId,
+ f.title as feedTitle,
+ f.url as feedUrl,
+ f.category as feedCategory
+ FROM episodes e
+ JOIN articles a ON e.article_id = a.id
+ JOIN feeds f ON a.feed_id = f.id
+ ${whereClause}
+ ORDER BY e.created_at DESC
+ `);
+
+ const rows = stmt.all(...params) as any[];
+
+ return rows.map((row) => ({
+ id: row.id,
+ title: row.title,
+ description: row.description,
+ audioPath: row.audioPath,
+ duration: row.duration,
+ fileSize: row.fileSize,
+ category: row.category,
+ createdAt: row.createdAt,
+ articleId: row.articleId,
+ articleTitle: row.articleTitle,
+ articleLink: row.articleLink,
+ articlePubDate: row.articlePubDate,
+ feedId: row.feedId,
+ feedTitle: row.feedTitle,
+ feedUrl: row.feedUrl,
+ feedCategory: row.feedCategory,
+ }));
+ } catch (error) {
+ console.error("Error searching episodes with feed info:", error);
+ throw error;
+ }
+}
+
export async function getAllFeedsIncludingInactive(): Promise {
try {
const stmt = db.prepare("SELECT * FROM feeds ORDER BY created_at DESC");
@@ -1309,7 +1393,9 @@ export async function deleteEpisode(episodeId: string): Promise {
}
// Episode category management functions
-export async function getEpisodesByCategory(category?: string): Promise {
+export async function getEpisodesByCategory(
+ category?: string,
+): Promise {
try {
let stmt;
let rows;
@@ -1458,7 +1544,10 @@ export async function getEpisodeCategoryStats(): Promise<{
}
}
-export async function updateEpisodeCategory(episodeId: string, category: string): Promise {
+export async function updateEpisodeCategory(
+ episodeId: string,
+ category: string,
+): Promise {
try {
const stmt = db.prepare("UPDATE episodes SET category = ? WHERE id = ?");
const result = stmt.run(category, episodeId);
@@ -1519,10 +1608,7 @@ export async function migrateEpisodesWithCategories(): Promise {
// Add a small delay to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
- console.error(
- `❌ Failed to classify episode ${episode.title}:`,
- error,
- );
+ console.error(`❌ Failed to classify episode ${episode.title}:`, error);
errorCount++;
// Set a default category for failed classifications
diff --git a/services/llm.ts b/services/llm.ts
index 577f409..a484a39 100644
--- a/services/llm.ts
+++ b/services/llm.ts
@@ -155,16 +155,17 @@ export async function openAI_ClassifyEpisode(
// Build the text for classification based on available data
let textForClassification = `タイトル: ${title}`;
-
+
if (description && description.trim()) {
textForClassification += `\n説明: ${description}`;
}
-
+
if (content && content.trim()) {
const maxContentLength = 1500;
- const truncatedContent = content.length > maxContentLength
- ? content.substring(0, maxContentLength) + "..."
- : content;
+ const truncatedContent =
+ content.length > maxContentLength
+ ? content.substring(0, maxContentLength) + "..."
+ : content;
textForClassification += `\n内容: ${truncatedContent}`;
}
diff --git a/services/podcast.ts b/services/podcast.ts
index 46f1c63..934df25 100644
--- a/services/podcast.ts
+++ b/services/podcast.ts
@@ -3,10 +3,10 @@ import fsSync from "node:fs";
import path from "node:path";
import { dirname } from "path";
import { config } from "./config.js";
-import {
- fetchEpisodesWithFeedInfo,
- getEpisodesByCategory,
- fetchEpisodesByFeedId
+import {
+ fetchEpisodesByFeedId,
+ fetchEpisodesWithFeedInfo,
+ getEpisodesByCategory,
} from "./database.js";
function escapeXml(text: string): string {
@@ -86,14 +86,14 @@ function filterValidEpisodes(episodes: any[]): any[] {
// Generate RSS XML from episodes
function generateRSSXml(
- episodes: any[],
- title: string,
- description: string,
- link?: string
+ episodes: any[],
+ title: string,
+ description: string,
+ link?: string,
): string {
const lastBuildDate = new Date().toUTCString();
const itemsXml = episodes.map(createItemXml).join("\n");
-
+
return `
@@ -121,9 +121,9 @@ export async function updatePodcastRSS(): Promise {
const outputPath = path.join(config.paths.publicDir, "podcast.xml");
const rssXml = generateRSSXml(
- validEpisodes,
- config.podcast.title,
- config.podcast.description
+ validEpisodes,
+ config.podcast.title,
+ config.podcast.description,
);
// Ensure directory exists
@@ -151,7 +151,7 @@ export async function generateCategoryRSS(category: string): Promise {
const title = `${config.podcast.title} - ${category}`;
const description = `${config.podcast.description} カテゴリ: ${category}`;
-
+
return generateRSSXml(validEpisodes, title, description);
} catch (error) {
console.error(`Error generating category RSS for "${category}":`, error);
@@ -162,9 +162,15 @@ export async function generateCategoryRSS(category: string): Promise {
export async function saveCategoryRSS(category: string): Promise {
try {
const rssXml = await generateCategoryRSS(category);
- const safeCategory = category.replace(/[^a-zA-Z0-9\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g, "_");
- const outputPath = path.join(config.paths.publicDir, `podcast_category_${safeCategory}.xml`);
-
+ const safeCategory = category.replace(
+ /[^a-zA-Z0-9\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/g,
+ "_",
+ );
+ const outputPath = path.join(
+ config.paths.publicDir,
+ `podcast_category_${safeCategory}.xml`,
+ );
+
// Ensure directory exists
await fs.mkdir(dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, rssXml);
@@ -187,10 +193,11 @@ export async function generateFeedRSS(feedId: string): Promise {
);
// Use feed info for RSS metadata if available
- const feedTitle = validEpisodes.length > 0 ? validEpisodes[0].feedTitle : "Unknown Feed";
+ const feedTitle =
+ validEpisodes.length > 0 ? validEpisodes[0].feedTitle : "Unknown Feed";
const title = `${config.podcast.title} - ${feedTitle}`;
const description = `${config.podcast.description} フィード: ${feedTitle}`;
-
+
return generateRSSXml(validEpisodes, title, description);
} catch (error) {
console.error(`Error generating feed RSS for "${feedId}":`, error);
@@ -201,8 +208,11 @@ export async function generateFeedRSS(feedId: string): Promise {
export async function saveFeedRSS(feedId: string): Promise {
try {
const rssXml = await generateFeedRSS(feedId);
- const outputPath = path.join(config.paths.publicDir, `podcast_feed_${feedId}.xml`);
-
+ const outputPath = path.join(
+ config.paths.publicDir,
+ `podcast_feed_${feedId}.xml`,
+ );
+
// Ensure directory exists
await fs.mkdir(dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, rssXml);