Add searching feature

This commit is contained in:
2025-06-08 21:53:45 +09:00
parent cd0e4065fc
commit b7f3ca6a27
16 changed files with 564 additions and 194 deletions

View File

@ -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
}

View File

@ -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"]',
);

View File

@ -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<EpisodeWithFeedInfo[]> {
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<Feed[]> {
try {
const stmt = db.prepare("SELECT * FROM feeds ORDER BY created_at DESC");
@ -1309,7 +1393,9 @@ export async function deleteEpisode(episodeId: string): Promise<boolean> {
}
// Episode category management functions
export async function getEpisodesByCategory(category?: string): Promise<EpisodeWithFeedInfo[]> {
export async function getEpisodesByCategory(
category?: string,
): Promise<EpisodeWithFeedInfo[]> {
try {
let stmt;
let rows;
@ -1458,7 +1544,10 @@ export async function getEpisodeCategoryStats(): Promise<{
}
}
export async function updateEpisodeCategory(episodeId: string, category: string): Promise<boolean> {
export async function updateEpisodeCategory(
episodeId: string,
category: string,
): Promise<boolean> {
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<void> {
// 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

View File

@ -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}`;
}

View File

@ -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 `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
@ -121,9 +121,9 @@ export async function updatePodcastRSS(): Promise<void> {
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<string> {
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<string> {
export async function saveCategoryRSS(category: string): Promise<void> {
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<string> {
);
// 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<string> {
export async function saveFeedRSS(feedId: string): Promise<void> {
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);