Update
This commit is contained in:
		
							
								
								
									
										109
									
								
								admin-server.ts
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								admin-server.ts
									
									
									
									
									
								
							@@ -14,6 +14,7 @@ import {
 | 
				
			|||||||
  getFeedRequests,
 | 
					  getFeedRequests,
 | 
				
			||||||
  updateFeedRequestStatus,
 | 
					  updateFeedRequestStatus,
 | 
				
			||||||
} from "./services/database.js";
 | 
					} from "./services/database.js";
 | 
				
			||||||
 | 
					import { Database } from "bun:sqlite";
 | 
				
			||||||
import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
 | 
					import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
 | 
				
			||||||
import { batchScheduler } from "./services/batch-scheduler.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
 | 
					// Feed requests management
 | 
				
			||||||
app.get("/api/admin/feed-requests", async (c) => {
 | 
					app.get("/api/admin/feed-requests", async (c) => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,59 +4,88 @@ import crypto from "crypto";
 | 
				
			|||||||
import { config } from "./config.js";
 | 
					import { config } from "./config.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Database integrity fixes function
 | 
					// Database integrity fixes function
 | 
				
			||||||
function performDatabaseIntegrityFixes(db: Database): void {
 | 
					export function performDatabaseIntegrityFixes(db: Database): void {
 | 
				
			||||||
  console.log("🔧 Performing database integrity checks...");
 | 
					  console.log("🔧 Performing database integrity checks...");
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Fix 1: Set active flag to 1 for feeds where it's NULL
 | 
					    // 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) {
 | 
					    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)
 | 
					    // 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 
 | 
					      SELECT a.id, a.link, a.title 
 | 
				
			||||||
      FROM articles a 
 | 
					      FROM articles a 
 | 
				
			||||||
      LEFT JOIN feeds f ON a.feed_id = f.id 
 | 
					      LEFT JOIN feeds f ON a.feed_id = f.id 
 | 
				
			||||||
      WHERE f.id IS NULL
 | 
					      WHERE f.id IS NULL
 | 
				
			||||||
    `).all() as any[];
 | 
					    `,
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .all() as any[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (orphanedArticles.length > 0) {
 | 
					    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) {
 | 
					      for (const article of orphanedArticles) {
 | 
				
			||||||
        // Try to match article to feed based on URL domain
 | 
					        // Try to match article to feed based on URL domain
 | 
				
			||||||
        const articleDomain = extractDomain(article.link);
 | 
					        const articleDomain = extractDomain(article.link);
 | 
				
			||||||
        if (articleDomain) {
 | 
					        if (articleDomain) {
 | 
				
			||||||
          const matchingFeed = db.prepare(`
 | 
					          const matchingFeed = db
 | 
				
			||||||
 | 
					            .prepare(
 | 
				
			||||||
 | 
					              `
 | 
				
			||||||
            SELECT id FROM feeds 
 | 
					            SELECT id FROM feeds 
 | 
				
			||||||
            WHERE url LIKE ? OR url LIKE ? 
 | 
					            WHERE url LIKE ? OR url LIKE ? 
 | 
				
			||||||
            ORDER BY created_at DESC 
 | 
					            ORDER BY created_at DESC 
 | 
				
			||||||
            LIMIT 1
 | 
					            LIMIT 1
 | 
				
			||||||
          `).get(`%${articleDomain}%`, `%${articleDomain.replace('www.', '')}%`) as any;
 | 
					          `,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .get(
 | 
				
			||||||
 | 
					              `%${articleDomain}%`,
 | 
				
			||||||
 | 
					              `%${articleDomain.replace("www.", "")}%`,
 | 
				
			||||||
 | 
					            ) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (matchingFeed) {
 | 
					          if (matchingFeed) {
 | 
				
			||||||
            db.prepare("UPDATE articles SET feed_id = ? WHERE id = ?")
 | 
					            db.prepare("UPDATE articles SET feed_id = ? WHERE id = ?").run(
 | 
				
			||||||
              .run(matchingFeed.id, article.id);
 | 
					              matchingFeed.id,
 | 
				
			||||||
            console.log(`✅ Fixed article "${article.title}" -> feed ${matchingFeed.id}`);
 | 
					              article.id,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            console.log(
 | 
				
			||||||
 | 
					              `✅ Fixed article "${article.title}" -> feed ${matchingFeed.id}`,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
          } else {
 | 
					          } 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
 | 
					    // 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 
 | 
					      SELECT e.id, e.title, e.article_id 
 | 
				
			||||||
      FROM episodes e 
 | 
					      FROM episodes e 
 | 
				
			||||||
      LEFT JOIN articles a ON e.article_id = a.id 
 | 
					      LEFT JOIN articles a ON e.article_id = a.id 
 | 
				
			||||||
      WHERE a.id IS NULL
 | 
					      WHERE a.id IS NULL
 | 
				
			||||||
    `).all() as any[];
 | 
					    `,
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      .all() as any[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (orphanedEpisodes.length > 0) {
 | 
					    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
 | 
					      // 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);
 | 
					  const db = new Database(config.paths.dbPath);
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  // Enable WAL mode for better concurrent access
 | 
					  // Enable WAL mode for better concurrent access
 | 
				
			||||||
  db.exec("PRAGMA journal_mode = WAL;");
 | 
					  db.exec("PRAGMA journal_mode = WAL;");
 | 
				
			||||||
  db.exec("PRAGMA synchronous = NORMAL;");
 | 
					  db.exec("PRAGMA synchronous = NORMAL;");
 | 
				
			||||||
@@ -259,6 +288,17 @@ export async function saveFeed(
 | 
				
			|||||||
      createdAt,
 | 
					      createdAt,
 | 
				
			||||||
      feed.active !== undefined ? (feed.active ? 1 : 0) : 1, // Default to active=1 if not specified
 | 
					      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;
 | 
					    return id;
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error("Error saving feed:", error);
 | 
					    console.error("Error saving feed:", error);
 | 
				
			||||||
@@ -336,7 +376,9 @@ export async function fetchActiveFeeds(): Promise<Feed[]> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get episodes with feed information for enhanced display
 | 
					// Get episodes with feed information for enhanced display
 | 
				
			||||||
export async function fetchEpisodesWithFeedInfo(): Promise<EpisodeWithFeedInfo[]> {
 | 
					export async function fetchEpisodesWithFeedInfo(): Promise<
 | 
				
			||||||
 | 
					  EpisodeWithFeedInfo[]
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
      SELECT 
 | 
					      SELECT 
 | 
				
			||||||
@@ -386,7 +428,9 @@ export async function fetchEpisodesWithFeedInfo(): Promise<EpisodeWithFeedInfo[]
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get episodes by feed ID
 | 
					// Get episodes by feed ID
 | 
				
			||||||
export async function fetchEpisodesByFeedId(feedId: string): Promise<EpisodeWithFeedInfo[]> {
 | 
					export async function fetchEpisodesByFeedId(
 | 
				
			||||||
 | 
					  feedId: string,
 | 
				
			||||||
 | 
					): Promise<EpisodeWithFeedInfo[]> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
      SELECT 
 | 
					      SELECT 
 | 
				
			||||||
@@ -436,7 +480,9 @@ export async function fetchEpisodesByFeedId(feedId: string): Promise<EpisodeWith
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get single episode with source information
 | 
					// Get single episode with source information
 | 
				
			||||||
export async function fetchEpisodeWithSourceInfo(episodeId: string): Promise<EpisodeWithFeedInfo | null> {
 | 
					export async function fetchEpisodeWithSourceInfo(
 | 
				
			||||||
 | 
					  episodeId: string,
 | 
				
			||||||
 | 
					): Promise<EpisodeWithFeedInfo | null> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
      SELECT 
 | 
					      SELECT 
 | 
				
			||||||
@@ -487,9 +533,7 @@ export async function fetchEpisodeWithSourceInfo(episodeId: string): Promise<Epi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
 | 
					export async function getAllFeedsIncludingInactive(): Promise<Feed[]> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare(
 | 
					    const stmt = db.prepare("SELECT * FROM feeds ORDER BY created_at DESC");
 | 
				
			||||||
      "SELECT * FROM feeds ORDER BY created_at DESC",
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    const rows = stmt.all() as any[];
 | 
					    const rows = stmt.all() as any[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return rows.map((row) => ({
 | 
					    return rows.map((row) => ({
 | 
				
			||||||
@@ -511,7 +555,7 @@ export async function deleteFeed(feedId: string): Promise<boolean> {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Start transaction
 | 
					    // Start transaction
 | 
				
			||||||
    db.exec("BEGIN TRANSACTION");
 | 
					    db.exec("BEGIN TRANSACTION");
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Delete all episodes for articles belonging to this feed
 | 
					    // Delete all episodes for articles belonging to this feed
 | 
				
			||||||
    const deleteEpisodesStmt = db.prepare(`
 | 
					    const deleteEpisodesStmt = db.prepare(`
 | 
				
			||||||
      DELETE FROM episodes 
 | 
					      DELETE FROM episodes 
 | 
				
			||||||
@@ -520,17 +564,19 @@ export async function deleteFeed(feedId: string): Promise<boolean> {
 | 
				
			|||||||
      )
 | 
					      )
 | 
				
			||||||
    `);
 | 
					    `);
 | 
				
			||||||
    deleteEpisodesStmt.run(feedId);
 | 
					    deleteEpisodesStmt.run(feedId);
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Delete all articles for this feed
 | 
					    // 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);
 | 
					    deleteArticlesStmt.run(feedId);
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Delete the feed itself
 | 
					    // Delete the feed itself
 | 
				
			||||||
    const deleteFeedStmt = db.prepare("DELETE FROM feeds WHERE id = ?");
 | 
					    const deleteFeedStmt = db.prepare("DELETE FROM feeds WHERE id = ?");
 | 
				
			||||||
    const result = deleteFeedStmt.run(feedId);
 | 
					    const result = deleteFeedStmt.run(feedId);
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    db.exec("COMMIT");
 | 
					    db.exec("COMMIT");
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    return result.changes > 0;
 | 
					    return result.changes > 0;
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    db.exec("ROLLBACK");
 | 
					    db.exec("ROLLBACK");
 | 
				
			||||||
@@ -539,7 +585,10 @@ export async function deleteFeed(feedId: string): Promise<boolean> {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function toggleFeedActive(feedId: string, active: boolean): Promise<boolean> {
 | 
					export async function toggleFeedActive(
 | 
				
			||||||
 | 
					  feedId: string,
 | 
				
			||||||
 | 
					  active: boolean,
 | 
				
			||||||
 | 
					): Promise<boolean> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare("UPDATE feeds SET active = ? WHERE id = ?");
 | 
					    const stmt = db.prepare("UPDATE feeds SET active = ? WHERE id = ?");
 | 
				
			||||||
    const result = stmt.run(active ? 1 : 0, feedId);
 | 
					    const result = stmt.run(active ? 1 : 0, feedId);
 | 
				
			||||||
@@ -816,7 +865,7 @@ export interface TTSQueueItem {
 | 
				
			|||||||
  retryCount: number;
 | 
					  retryCount: number;
 | 
				
			||||||
  createdAt: string;
 | 
					  createdAt: string;
 | 
				
			||||||
  lastAttemptedAt?: string;
 | 
					  lastAttemptedAt?: string;
 | 
				
			||||||
  status: 'pending' | 'processing' | 'failed';
 | 
					  status: "pending" | "processing" | "failed";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function addToQueue(
 | 
					export async function addToQueue(
 | 
				
			||||||
@@ -840,7 +889,9 @@ export async function addToQueue(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getQueueItems(limit: number = 10): Promise<TTSQueueItem[]> {
 | 
					export async function getQueueItems(
 | 
				
			||||||
 | 
					  limit: number = 10,
 | 
				
			||||||
 | 
					): Promise<TTSQueueItem[]> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
      SELECT * FROM tts_queue
 | 
					      SELECT * FROM tts_queue
 | 
				
			||||||
@@ -867,7 +918,7 @@ export async function getQueueItems(limit: number = 10): Promise<TTSQueueItem[]>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export async function updateQueueItemStatus(
 | 
					export async function updateQueueItemStatus(
 | 
				
			||||||
  queueId: string,
 | 
					  queueId: string,
 | 
				
			||||||
  status: 'pending' | 'processing' | 'failed',
 | 
					  status: "pending" | "processing" | "failed",
 | 
				
			||||||
  lastAttemptedAt?: string,
 | 
					  lastAttemptedAt?: string,
 | 
				
			||||||
): Promise<void> {
 | 
					): Promise<void> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@@ -897,7 +948,7 @@ export interface FeedRequest {
 | 
				
			|||||||
  url: string;
 | 
					  url: string;
 | 
				
			||||||
  requestedBy?: string;
 | 
					  requestedBy?: string;
 | 
				
			||||||
  requestMessage?: string;
 | 
					  requestMessage?: string;
 | 
				
			||||||
  status: 'pending' | 'approved' | 'rejected';
 | 
					  status: "pending" | "approved" | "rejected";
 | 
				
			||||||
  createdAt: string;
 | 
					  createdAt: string;
 | 
				
			||||||
  reviewedAt?: string;
 | 
					  reviewedAt?: string;
 | 
				
			||||||
  reviewedBy?: string;
 | 
					  reviewedBy?: string;
 | 
				
			||||||
@@ -905,7 +956,7 @@ export interface FeedRequest {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function submitFeedRequest(
 | 
					export async function submitFeedRequest(
 | 
				
			||||||
  request: Omit<FeedRequest, "id" | "createdAt" | "status">
 | 
					  request: Omit<FeedRequest, "id" | "createdAt" | "status">,
 | 
				
			||||||
): Promise<string> {
 | 
					): Promise<string> {
 | 
				
			||||||
  const id = crypto.randomUUID();
 | 
					  const id = crypto.randomUUID();
 | 
				
			||||||
  const createdAt = new Date().toISOString();
 | 
					  const createdAt = new Date().toISOString();
 | 
				
			||||||
@@ -914,7 +965,13 @@ export async function submitFeedRequest(
 | 
				
			|||||||
    const stmt = db.prepare(
 | 
					    const stmt = db.prepare(
 | 
				
			||||||
      "INSERT INTO feed_requests (id, url, requested_by, request_message, status, created_at) VALUES (?, ?, ?, ?, 'pending', ?)",
 | 
					      "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}`);
 | 
					    console.log(`Feed request submitted: ${request.url}`);
 | 
				
			||||||
    return id;
 | 
					    return id;
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
@@ -928,7 +985,7 @@ export async function getFeedRequests(status?: string): Promise<FeedRequest[]> {
 | 
				
			|||||||
    const sql = status
 | 
					    const sql = status
 | 
				
			||||||
      ? "SELECT * FROM feed_requests WHERE status = ? ORDER BY created_at DESC"
 | 
					      ? "SELECT * FROM feed_requests WHERE status = ? ORDER BY created_at DESC"
 | 
				
			||||||
      : "SELECT * FROM feed_requests ORDER BY created_at DESC";
 | 
					      : "SELECT * FROM feed_requests ORDER BY created_at DESC";
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    const stmt = db.prepare(sql);
 | 
					    const stmt = db.prepare(sql);
 | 
				
			||||||
    const rows = status ? stmt.all(status) : stmt.all();
 | 
					    const rows = status ? stmt.all(status) : stmt.all();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -951,7 +1008,7 @@ export async function getFeedRequests(status?: string): Promise<FeedRequest[]> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export async function updateFeedRequestStatus(
 | 
					export async function updateFeedRequestStatus(
 | 
				
			||||||
  requestId: string,
 | 
					  requestId: string,
 | 
				
			||||||
  status: 'approved' | 'rejected',
 | 
					  status: "approved" | "rejected",
 | 
				
			||||||
  reviewedBy?: string,
 | 
					  reviewedBy?: string,
 | 
				
			||||||
  adminNotes?: string,
 | 
					  adminNotes?: string,
 | 
				
			||||||
): Promise<boolean> {
 | 
					): Promise<boolean> {
 | 
				
			||||||
@@ -960,7 +1017,13 @@ export async function updateFeedRequestStatus(
 | 
				
			|||||||
    const stmt = db.prepare(
 | 
					    const stmt = db.prepare(
 | 
				
			||||||
      "UPDATE feed_requests SET status = ?, reviewed_at = ?, reviewed_by = ?, admin_notes = ? WHERE id = ?",
 | 
					      "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;
 | 
					    return result.changes > 0;
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error("Error updating feed request status:", error);
 | 
					    console.error("Error updating feed request status:", error);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,10 @@
 | 
				
			|||||||
import { promises as fs } from "fs";
 | 
					import { promises as fs } from "fs";
 | 
				
			||||||
import { dirname } from "path";
 | 
					import { dirname } from "path";
 | 
				
			||||||
import { Episode, fetchAllEpisodes } from "./database.js";
 | 
					import {
 | 
				
			||||||
 | 
					  Episode,
 | 
				
			||||||
 | 
					  fetchAllEpisodes,
 | 
				
			||||||
 | 
					  performDatabaseIntegrityFixes,
 | 
				
			||||||
 | 
					} from "./database.js";
 | 
				
			||||||
import path from "node:path";
 | 
					import path from "node:path";
 | 
				
			||||||
import fsSync from "node:fs";
 | 
					import fsSync from "node:fs";
 | 
				
			||||||
import { config } from "./config.js";
 | 
					import { config } from "./config.js";
 | 
				
			||||||
@@ -17,17 +21,20 @@ function escapeXml(text: string): string {
 | 
				
			|||||||
function createItemXml(episode: Episode): string {
 | 
					function createItemXml(episode: Episode): string {
 | 
				
			||||||
  const fileUrl = `${config.podcast.baseUrl}/podcast_audio/${path.basename(episode.audioPath)}`;
 | 
					  const fileUrl = `${config.podcast.baseUrl}/podcast_audio/${path.basename(episode.audioPath)}`;
 | 
				
			||||||
  const pubDate = new Date(episode.createdAt).toUTCString();
 | 
					  const pubDate = new Date(episode.createdAt).toUTCString();
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  let fileSize = 0;
 | 
					  let fileSize = 0;
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const audioPath = path.join(config.paths.podcastAudioDir, episode.audioPath);
 | 
					    const audioPath = path.join(
 | 
				
			||||||
 | 
					      config.paths.podcastAudioDir,
 | 
				
			||||||
 | 
					      episode.audioPath,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    if (fsSync.existsSync(audioPath)) {
 | 
					    if (fsSync.existsSync(audioPath)) {
 | 
				
			||||||
      fileSize = fsSync.statSync(audioPath).size;
 | 
					      fileSize = fsSync.statSync(audioPath).size;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.warn(`Could not get file size for ${episode.audioPath}:`, error);
 | 
					    console.warn(`Could not get file size for ${episode.audioPath}:`, error);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
  return `
 | 
					  return `
 | 
				
			||||||
    <item>
 | 
					    <item>
 | 
				
			||||||
      <title><![CDATA[${escapeXml(episode.title)}]]></title>
 | 
					      <title><![CDATA[${escapeXml(episode.title)}]]></title>
 | 
				
			||||||
@@ -45,11 +52,14 @@ function createItemXml(episode: Episode): string {
 | 
				
			|||||||
export async function updatePodcastRSS(): Promise<void> {
 | 
					export async function updatePodcastRSS(): Promise<void> {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const episodes: Episode[] = await fetchAllEpisodes();
 | 
					    const episodes: Episode[] = await fetchAllEpisodes();
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    // Filter episodes to only include those with valid audio files
 | 
					    // Filter episodes to only include those with valid audio files
 | 
				
			||||||
    const validEpisodes = episodes.filter(episode => {
 | 
					    const validEpisodes = episodes.filter((episode) => {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const audioPath = path.join(config.paths.podcastAudioDir, episode.audioPath);
 | 
					        const audioPath = path.join(
 | 
				
			||||||
 | 
					          config.paths.podcastAudioDir,
 | 
				
			||||||
 | 
					          episode.audioPath,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        return fsSync.existsSync(audioPath);
 | 
					        return fsSync.existsSync(audioPath);
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.warn(`Audio file not found for episode: ${episode.title}`);
 | 
					        console.warn(`Audio file not found for episode: ${episode.title}`);
 | 
				
			||||||
@@ -57,7 +67,9 @@ export async function updatePodcastRSS(): Promise<void> {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 lastBuildDate = new Date().toUTCString();
 | 
				
			||||||
    const itemsXml = validEpisodes.map(createItemXml).join("\n");
 | 
					    const itemsXml = validEpisodes.map(createItemXml).join("\n");
 | 
				
			||||||
@@ -81,8 +93,10 @@ export async function updatePodcastRSS(): Promise<void> {
 | 
				
			|||||||
    // Ensure directory exists
 | 
					    // Ensure directory exists
 | 
				
			||||||
    await fs.mkdir(dirname(outputPath), { recursive: true });
 | 
					    await fs.mkdir(dirname(outputPath), { recursive: true });
 | 
				
			||||||
    await fs.writeFile(outputPath, rssXml);
 | 
					    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) {
 | 
					  } catch (error) {
 | 
				
			||||||
    console.error("Error updating podcast RSS:", error);
 | 
					    console.error("Error updating podcast RSS:", error);
 | 
				
			||||||
    throw error;
 | 
					    throw error;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user