From eeb0d8de2912ef52af180195f3647d6c2bdb229d Mon Sep 17 00:00:00 2001 From: Satsuki Akiba Date: Sat, 7 Jun 2025 22:38:50 +0900 Subject: [PATCH] Update --- admin-server.ts | 109 +++++++++++++++++++++++++++++++++ services/database.ts | 143 +++++++++++++++++++++++++++++++------------ services/podcast.ts | 34 +++++++--- 3 files changed, 236 insertions(+), 50 deletions(-) diff --git a/admin-server.ts b/admin-server.ts index 1043f6b..f70bbd1 100644 --- a/admin-server.ts +++ b/admin-server.ts @@ -14,6 +14,7 @@ import { getFeedRequests, updateFeedRequestStatus, } from "./services/database.js"; +import { Database } from "bun:sqlite"; import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js"; import { batchScheduler } from "./services/batch-scheduler.js"; @@ -204,6 +205,114 @@ app.get("/api/admin/episodes/simple", async (c) => { } }); +// Database diagnostic endpoint +app.get("/api/admin/db-diagnostic", async (c) => { + try { + const db = new Database(config.paths.dbPath); + + // 1. Check episodes table + const episodeCount = db.prepare("SELECT COUNT(*) as count FROM episodes").get() as any; + + // 2. Check articles table + const articleCount = db.prepare("SELECT COUNT(*) as count FROM articles").get() as any; + + // 3. Check feeds table + const feedCount = db.prepare("SELECT COUNT(*) as count FROM feeds").get() as any; + const activeFeedCount = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 1").get() as any; + const inactiveFeedCount = db.prepare("SELECT COUNT(*) as count FROM feeds WHERE active = 0 OR active IS NULL").get() as any; + + // 4. Check orphaned episodes + const orphanedEpisodes = db.prepare(` + SELECT e.id, e.title, e.article_id + FROM episodes e + LEFT JOIN articles a ON e.article_id = a.id + WHERE a.id IS NULL + `).all() as any[]; + + // 5. Check orphaned articles + const orphanedArticles = db.prepare(` + SELECT a.id, a.title, a.feed_id + FROM articles a + LEFT JOIN feeds f ON a.feed_id = f.id + WHERE f.id IS NULL + `).all() as any[]; + + // 6. Check episodes with articles but feeds are inactive + const episodesInactiveFeeds = db.prepare(` + SELECT e.id, e.title, f.active, f.title as feed_title + FROM episodes e + JOIN articles a ON e.article_id = a.id + JOIN feeds f ON a.feed_id = f.id + WHERE f.active = 0 OR f.active IS NULL + `).all() as any[]; + + // 7. Test the JOIN query + const joinResult = db.prepare(` + SELECT COUNT(*) as count + FROM episodes e + JOIN articles a ON e.article_id = a.id + JOIN feeds f ON a.feed_id = f.id + WHERE f.active = 1 + `).get() as any; + + // 8. Sample feed details + const sampleFeeds = db.prepare(` + SELECT id, title, url, active, created_at + FROM feeds + ORDER BY created_at DESC + LIMIT 5 + `).all() as any[]; + + // 9. Sample episode-article-feed chain + const sampleChain = db.prepare(` + SELECT + e.id as episode_id, e.title as episode_title, + a.id as article_id, a.title as article_title, + f.id as feed_id, f.title as feed_title, f.active + FROM episodes e + LEFT JOIN articles a ON e.article_id = a.id + LEFT JOIN feeds f ON a.feed_id = f.id + ORDER BY e.created_at DESC + LIMIT 5 + `).all() as any[]; + + db.close(); + + const diagnosticResult = { + counts: { + episodes: episodeCount.count, + articles: articleCount.count, + feeds: feedCount.count, + activeFeeds: activeFeedCount.count, + inactiveFeeds: inactiveFeedCount.count, + }, + orphaned: { + episodes: orphanedEpisodes.length, + episodeDetails: orphanedEpisodes.slice(0, 3), + articles: orphanedArticles.length, + articleDetails: orphanedArticles.slice(0, 3), + }, + episodesFromInactiveFeeds: { + count: episodesInactiveFeeds.length, + details: episodesInactiveFeeds.slice(0, 3), + }, + joinQuery: { + episodesWithActiveFeeds: joinResult.count, + }, + samples: { + feeds: sampleFeeds, + episodeChain: sampleChain, + }, + timestamp: new Date().toISOString(), + }; + + return c.json(diagnosticResult); + } catch (error) { + console.error("Error running database diagnostic:", error); + return c.json({ error: "Failed to run database diagnostic", details: error.message }, 500); + } +}); + // Feed requests management app.get("/api/admin/feed-requests", async (c) => { try { diff --git a/services/database.ts b/services/database.ts index de3fe23..3fdd692 100644 --- a/services/database.ts +++ b/services/database.ts @@ -4,59 +4,88 @@ import crypto from "crypto"; import { config } from "./config.js"; // Database integrity fixes function -function performDatabaseIntegrityFixes(db: Database): void { +export function performDatabaseIntegrityFixes(db: Database): void { console.log("🔧 Performing database integrity checks..."); - + try { // Fix 1: Set active flag to 1 for feeds where it's NULL - const nullActiveFeeds = db.prepare("UPDATE feeds SET active = 1 WHERE active IS NULL").run(); + const nullActiveFeeds = db + .prepare("UPDATE feeds SET active = 1 WHERE active IS NULL") + .run(); if (nullActiveFeeds.changes > 0) { - console.log(`✅ Fixed ${nullActiveFeeds.changes} feeds with NULL active flag`); + console.log( + `✅ Fixed ${nullActiveFeeds.changes} feeds with NULL active flag`, + ); } // Fix 2: Fix orphaned articles (articles referencing non-existent feeds) - const orphanedArticles = db.prepare(` + const orphanedArticles = db + .prepare( + ` SELECT a.id, a.link, a.title FROM articles a LEFT JOIN feeds f ON a.feed_id = f.id WHERE f.id IS NULL - `).all() as any[]; + `, + ) + .all() as any[]; if (orphanedArticles.length > 0) { - console.log(`🔍 Found ${orphanedArticles.length} orphaned articles, attempting to fix...`); - + console.log( + `🔍 Found ${orphanedArticles.length} orphaned articles, attempting to fix...`, + ); + for (const article of orphanedArticles) { // Try to match article to feed based on URL domain const articleDomain = extractDomain(article.link); if (articleDomain) { - const matchingFeed = db.prepare(` + const matchingFeed = db + .prepare( + ` SELECT id FROM feeds WHERE url LIKE ? OR url LIKE ? ORDER BY created_at DESC LIMIT 1 - `).get(`%${articleDomain}%`, `%${articleDomain.replace('www.', '')}%`) as any; + `, + ) + .get( + `%${articleDomain}%`, + `%${articleDomain.replace("www.", "")}%`, + ) as any; if (matchingFeed) { - db.prepare("UPDATE articles SET feed_id = ? WHERE id = ?") - .run(matchingFeed.id, article.id); - console.log(`✅ Fixed article "${article.title}" -> feed ${matchingFeed.id}`); + db.prepare("UPDATE articles SET feed_id = ? WHERE id = ?").run( + matchingFeed.id, + article.id, + ); + console.log( + `✅ Fixed article "${article.title}" -> feed ${matchingFeed.id}`, + ); } else { - console.log(`⚠️ Could not find matching feed for article: ${article.title} (${articleDomain})`); + console.log( + `⚠️ Could not find matching feed for article: ${article.title} (${articleDomain})`, + ); } } } } // Fix 3: Ensure all episodes have valid article references - const orphanedEpisodes = db.prepare(` + const orphanedEpisodes = db + .prepare( + ` SELECT e.id, e.title, e.article_id FROM episodes e LEFT JOIN articles a ON e.article_id = a.id WHERE a.id IS NULL - `).all() as any[]; + `, + ) + .all() as any[]; if (orphanedEpisodes.length > 0) { - console.log(`⚠️ Found ${orphanedEpisodes.length} episodes with invalid article references`); + console.log( + `⚠️ Found ${orphanedEpisodes.length} episodes with invalid article references`, + ); // We could delete these or try to fix them, but for now just log } @@ -89,7 +118,7 @@ function initializeDatabase(): Database { } const db = new Database(config.paths.dbPath); - + // Enable WAL mode for better concurrent access db.exec("PRAGMA journal_mode = WAL;"); db.exec("PRAGMA synchronous = NORMAL;"); @@ -259,6 +288,17 @@ export async function saveFeed( createdAt, feed.active !== undefined ? (feed.active ? 1 : 0) : 1, // Default to active=1 if not specified ); + + try { + performDatabaseIntegrityFixes(db); + console.log(`Feed saved: ${feed.url}`); + } catch (error) { + console.error( + "Error performing integrity fixes after saving feed:", + error, + ); + } + return id; } catch (error) { console.error("Error saving feed:", error); @@ -336,7 +376,9 @@ export async function fetchActiveFeeds(): Promise { } // Get episodes with feed information for enhanced display -export async function fetchEpisodesWithFeedInfo(): Promise { +export async function fetchEpisodesWithFeedInfo(): Promise< + EpisodeWithFeedInfo[] +> { try { const stmt = db.prepare(` SELECT @@ -386,7 +428,9 @@ export async function fetchEpisodesWithFeedInfo(): Promise { +export async function fetchEpisodesByFeedId( + feedId: string, +): Promise { try { const stmt = db.prepare(` SELECT @@ -436,7 +480,9 @@ export async function fetchEpisodesByFeedId(feedId: string): Promise { +export async function fetchEpisodeWithSourceInfo( + episodeId: string, +): Promise { try { const stmt = db.prepare(` SELECT @@ -487,9 +533,7 @@ export async function fetchEpisodeWithSourceInfo(episodeId: string): Promise { try { - const stmt = db.prepare( - "SELECT * FROM feeds ORDER BY created_at DESC", - ); + const stmt = db.prepare("SELECT * FROM feeds ORDER BY created_at DESC"); const rows = stmt.all() as any[]; return rows.map((row) => ({ @@ -511,7 +555,7 @@ export async function deleteFeed(feedId: string): Promise { try { // Start transaction db.exec("BEGIN TRANSACTION"); - + // Delete all episodes for articles belonging to this feed const deleteEpisodesStmt = db.prepare(` DELETE FROM episodes @@ -520,17 +564,19 @@ export async function deleteFeed(feedId: string): Promise { ) `); deleteEpisodesStmt.run(feedId); - + // Delete all articles for this feed - const deleteArticlesStmt = db.prepare("DELETE FROM articles WHERE feed_id = ?"); + const deleteArticlesStmt = db.prepare( + "DELETE FROM articles WHERE feed_id = ?", + ); deleteArticlesStmt.run(feedId); - + // Delete the feed itself const deleteFeedStmt = db.prepare("DELETE FROM feeds WHERE id = ?"); const result = deleteFeedStmt.run(feedId); - + db.exec("COMMIT"); - + return result.changes > 0; } catch (error) { db.exec("ROLLBACK"); @@ -539,7 +585,10 @@ export async function deleteFeed(feedId: string): Promise { } } -export async function toggleFeedActive(feedId: string, active: boolean): Promise { +export async function toggleFeedActive( + feedId: string, + active: boolean, +): Promise { try { const stmt = db.prepare("UPDATE feeds SET active = ? WHERE id = ?"); const result = stmt.run(active ? 1 : 0, feedId); @@ -816,7 +865,7 @@ export interface TTSQueueItem { retryCount: number; createdAt: string; lastAttemptedAt?: string; - status: 'pending' | 'processing' | 'failed'; + status: "pending" | "processing" | "failed"; } export async function addToQueue( @@ -840,7 +889,9 @@ export async function addToQueue( } } -export async function getQueueItems(limit: number = 10): Promise { +export async function getQueueItems( + limit: number = 10, +): Promise { try { const stmt = db.prepare(` SELECT * FROM tts_queue @@ -867,7 +918,7 @@ export async function getQueueItems(limit: number = 10): Promise export async function updateQueueItemStatus( queueId: string, - status: 'pending' | 'processing' | 'failed', + status: "pending" | "processing" | "failed", lastAttemptedAt?: string, ): Promise { try { @@ -897,7 +948,7 @@ export interface FeedRequest { url: string; requestedBy?: string; requestMessage?: string; - status: 'pending' | 'approved' | 'rejected'; + status: "pending" | "approved" | "rejected"; createdAt: string; reviewedAt?: string; reviewedBy?: string; @@ -905,7 +956,7 @@ export interface FeedRequest { } export async function submitFeedRequest( - request: Omit + request: Omit, ): Promise { const id = crypto.randomUUID(); const createdAt = new Date().toISOString(); @@ -914,7 +965,13 @@ export async function submitFeedRequest( const stmt = db.prepare( "INSERT INTO feed_requests (id, url, requested_by, request_message, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)", ); - stmt.run(id, request.url, request.requestedBy || null, request.requestMessage || null, createdAt); + stmt.run( + id, + request.url, + request.requestedBy || null, + request.requestMessage || null, + createdAt, + ); console.log(`Feed request submitted: ${request.url}`); return id; } catch (error) { @@ -928,7 +985,7 @@ export async function getFeedRequests(status?: string): Promise { const sql = status ? "SELECT * FROM feed_requests WHERE status = ? ORDER BY created_at DESC" : "SELECT * FROM feed_requests ORDER BY created_at DESC"; - + const stmt = db.prepare(sql); const rows = status ? stmt.all(status) : stmt.all(); @@ -951,7 +1008,7 @@ export async function getFeedRequests(status?: string): Promise { export async function updateFeedRequestStatus( requestId: string, - status: 'approved' | 'rejected', + status: "approved" | "rejected", reviewedBy?: string, adminNotes?: string, ): Promise { @@ -960,7 +1017,13 @@ export async function updateFeedRequestStatus( const stmt = db.prepare( "UPDATE feed_requests SET status = ?, reviewed_at = ?, reviewed_by = ?, admin_notes = ? WHERE id = ?", ); - const result = stmt.run(status, reviewedAt, reviewedBy || null, adminNotes || null, requestId); + const result = stmt.run( + status, + reviewedAt, + reviewedBy || null, + adminNotes || null, + requestId, + ); return result.changes > 0; } catch (error) { console.error("Error updating feed request status:", error); diff --git a/services/podcast.ts b/services/podcast.ts index 627bb31..1c4cacc 100644 --- a/services/podcast.ts +++ b/services/podcast.ts @@ -1,6 +1,10 @@ import { promises as fs } from "fs"; import { dirname } from "path"; -import { Episode, fetchAllEpisodes } from "./database.js"; +import { + Episode, + fetchAllEpisodes, + performDatabaseIntegrityFixes, +} from "./database.js"; import path from "node:path"; import fsSync from "node:fs"; import { config } from "./config.js"; @@ -17,17 +21,20 @@ function escapeXml(text: string): string { function createItemXml(episode: Episode): string { const fileUrl = `${config.podcast.baseUrl}/podcast_audio/${path.basename(episode.audioPath)}`; const pubDate = new Date(episode.createdAt).toUTCString(); - + let fileSize = 0; try { - const audioPath = path.join(config.paths.podcastAudioDir, episode.audioPath); + const audioPath = path.join( + config.paths.podcastAudioDir, + episode.audioPath, + ); if (fsSync.existsSync(audioPath)) { fileSize = fsSync.statSync(audioPath).size; } } catch (error) { console.warn(`Could not get file size for ${episode.audioPath}:`, error); } - + return ` <![CDATA[${escapeXml(episode.title)}]]> @@ -45,11 +52,14 @@ function createItemXml(episode: Episode): string { export async function updatePodcastRSS(): Promise { try { const episodes: Episode[] = await fetchAllEpisodes(); - + // Filter episodes to only include those with valid audio files - const validEpisodes = episodes.filter(episode => { + const validEpisodes = episodes.filter((episode) => { try { - const audioPath = path.join(config.paths.podcastAudioDir, episode.audioPath); + const audioPath = path.join( + config.paths.podcastAudioDir, + episode.audioPath, + ); return fsSync.existsSync(audioPath); } catch (error) { console.warn(`Audio file not found for episode: ${episode.title}`); @@ -57,7 +67,9 @@ export async function updatePodcastRSS(): Promise { } }); - console.log(`Found ${episodes.length} episodes, ${validEpisodes.length} with valid audio files`); + console.log( + `Found ${episodes.length} episodes, ${validEpisodes.length} with valid audio files`, + ); const lastBuildDate = new Date().toUTCString(); const itemsXml = validEpisodes.map(createItemXml).join("\n"); @@ -81,8 +93,10 @@ export async function updatePodcastRSS(): Promise { // Ensure directory exists await fs.mkdir(dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, rssXml); - - console.log(`RSS feed updated with ${validEpisodes.length} episodes (audio files verified)`); + + console.log( + `RSS feed updated with ${validEpisodes.length} episodes (audio files verified)`, + ); } catch (error) { console.error("Error updating podcast RSS:", error); throw error;