Add searching feature
This commit is contained in:
@ -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
|
||||
}
|
||||
|
@ -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"]',
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user