Add new batch scheduler service
This commit is contained in:
		@@ -32,10 +32,15 @@ interface FeedItem {
 | 
			
		||||
 * Main batch processing function
 | 
			
		||||
 * Processes all feeds and generates podcasts for new articles
 | 
			
		||||
 */
 | 
			
		||||
export async function batchProcess(): Promise<void> {
 | 
			
		||||
export async function batchProcess(abortSignal?: AbortSignal): Promise<void> {
 | 
			
		||||
  try {
 | 
			
		||||
    console.log("🚀 Starting enhanced batch process...");
 | 
			
		||||
 | 
			
		||||
    // Check for cancellation at start
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Batch process was cancelled before starting');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Load feed URLs from file
 | 
			
		||||
    const feedUrls = await loadFeedUrls();
 | 
			
		||||
    if (feedUrls.length === 0) {
 | 
			
		||||
@@ -47,22 +52,42 @@ export async function batchProcess(): Promise<void> {
 | 
			
		||||
 | 
			
		||||
    // Process each feed URL
 | 
			
		||||
    for (const url of feedUrls) {
 | 
			
		||||
      // Check for cancellation before processing each feed
 | 
			
		||||
      if (abortSignal?.aborted) {
 | 
			
		||||
        throw new Error('Batch process was cancelled during feed processing');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        await processFeedUrl(url);
 | 
			
		||||
        await processFeedUrl(url, abortSignal);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        // Re-throw cancellation errors
 | 
			
		||||
        if (error instanceof Error && (error.message.includes('cancelled') || error.name === 'AbortError')) {
 | 
			
		||||
          throw error;
 | 
			
		||||
        }
 | 
			
		||||
        console.error(`❌ Failed to process feed ${url}:`, error);
 | 
			
		||||
        // Continue with other feeds
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for cancellation before processing articles
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Batch process was cancelled before article processing');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Process unprocessed articles and generate podcasts
 | 
			
		||||
    await processUnprocessedArticles();
 | 
			
		||||
    await processUnprocessedArticles(abortSignal);
 | 
			
		||||
 | 
			
		||||
    console.log(
 | 
			
		||||
      "✅ Enhanced batch process completed:",
 | 
			
		||||
      new Date().toISOString(),
 | 
			
		||||
    );
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    if (error instanceof Error && (error.message.includes('cancelled') || error.name === 'AbortError')) {
 | 
			
		||||
      console.log("🛑 Batch process was cancelled");
 | 
			
		||||
      const abortError = new Error('Batch process was cancelled');
 | 
			
		||||
      abortError.name = 'AbortError';
 | 
			
		||||
      throw abortError;
 | 
			
		||||
    }
 | 
			
		||||
    console.error("💥 Batch process failed:", error);
 | 
			
		||||
    throw error;
 | 
			
		||||
  }
 | 
			
		||||
@@ -90,17 +115,27 @@ async function loadFeedUrls(): Promise<string[]> {
 | 
			
		||||
/**
 | 
			
		||||
 * Process a single feed URL and discover new articles
 | 
			
		||||
 */
 | 
			
		||||
async function processFeedUrl(url: string): Promise<void> {
 | 
			
		||||
async function processFeedUrl(url: string, abortSignal?: AbortSignal): Promise<void> {
 | 
			
		||||
  if (!url || !url.startsWith("http")) {
 | 
			
		||||
    throw new Error(`Invalid feed URL: ${url}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check for cancellation
 | 
			
		||||
  if (abortSignal?.aborted) {
 | 
			
		||||
    throw new Error('Feed processing was cancelled');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  console.log(`🔍 Processing feed: ${url}`);
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    // Parse RSS feed
 | 
			
		||||
    const parser = new Parser<FeedItem>();
 | 
			
		||||
    const feed = await parser.parseURL(url);
 | 
			
		||||
    
 | 
			
		||||
    // Check for cancellation after parsing
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Feed processing was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get or create feed record
 | 
			
		||||
    let feedRecord = await getFeedByUrl(url);
 | 
			
		||||
@@ -189,12 +224,22 @@ async function discoverNewArticles(
 | 
			
		||||
/**
 | 
			
		||||
 * Process unprocessed articles and generate podcasts
 | 
			
		||||
 */
 | 
			
		||||
async function processUnprocessedArticles(): Promise<void> {
 | 
			
		||||
async function processUnprocessedArticles(abortSignal?: AbortSignal): Promise<void> {
 | 
			
		||||
  console.log("🎧 Processing unprocessed articles...");
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    // Check for cancellation
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Article processing was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Process retry queue first
 | 
			
		||||
    await processRetryQueue();
 | 
			
		||||
    await processRetryQueue(abortSignal);
 | 
			
		||||
 | 
			
		||||
    // Check for cancellation after retry queue
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Article processing was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get unprocessed articles (limit to prevent overwhelming)
 | 
			
		||||
    const unprocessedArticles = await getUnprocessedArticles(
 | 
			
		||||
@@ -212,8 +257,13 @@ async function processUnprocessedArticles(): Promise<void> {
 | 
			
		||||
    let successfullyGeneratedCount = 0;
 | 
			
		||||
 | 
			
		||||
    for (const article of unprocessedArticles) {
 | 
			
		||||
      // Check for cancellation before processing each article
 | 
			
		||||
      if (abortSignal?.aborted) {
 | 
			
		||||
        throw new Error('Article processing was cancelled');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        const episodeCreated = await generatePodcastForArticle(article);
 | 
			
		||||
        const episodeCreated = await generatePodcastForArticle(article, abortSignal);
 | 
			
		||||
        
 | 
			
		||||
        // Only mark as processed and update RSS if episode was actually created
 | 
			
		||||
        if (episodeCreated) {
 | 
			
		||||
@@ -233,6 +283,10 @@ async function processUnprocessedArticles(): Promise<void> {
 | 
			
		||||
          console.warn(`⚠️  Episode creation failed for: ${article.title} - not marking as processed`);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error instanceof Error && (error.message.includes('cancelled') || error.name === 'AbortError')) {
 | 
			
		||||
          console.log(`🛑 Article processing cancelled, stopping batch`);
 | 
			
		||||
          throw error; // Re-throw to propagate cancellation
 | 
			
		||||
        }
 | 
			
		||||
        console.error(
 | 
			
		||||
          `❌ Failed to generate podcast for article: ${article.title}`,
 | 
			
		||||
          error,
 | 
			
		||||
@@ -254,7 +308,7 @@ async function processUnprocessedArticles(): Promise<void> {
 | 
			
		||||
/**
 | 
			
		||||
 * Process retry queue for failed TTS generation
 | 
			
		||||
 */
 | 
			
		||||
async function processRetryQueue(): Promise<void> {
 | 
			
		||||
async function processRetryQueue(abortSignal?: AbortSignal): Promise<void> {
 | 
			
		||||
  const { getQueueItems, updateQueueItemStatus, removeFromQueue } = await import("../services/database.js");
 | 
			
		||||
  const { Database } = await import("bun:sqlite");
 | 
			
		||||
  const db = new Database(config.paths.dbPath);
 | 
			
		||||
@@ -271,6 +325,11 @@ async function processRetryQueue(): Promise<void> {
 | 
			
		||||
    console.log(`📋 Found ${queueItems.length} items in retry queue`);
 | 
			
		||||
 | 
			
		||||
    for (const item of queueItems) {
 | 
			
		||||
      // Check for cancellation before processing each retry item
 | 
			
		||||
      if (abortSignal?.aborted) {
 | 
			
		||||
        throw new Error('Retry queue processing was cancelled');
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        console.log(`🔁 Retrying TTS generation for: ${item.itemId} (attempt ${item.retryCount + 1}/3)`);
 | 
			
		||||
        
 | 
			
		||||
@@ -294,6 +353,11 @@ async function processRetryQueue(): Promise<void> {
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        if (error instanceof Error && (error.message.includes('cancelled') || error.name === 'AbortError')) {
 | 
			
		||||
          console.log(`🛑 TTS retry processing cancelled for: ${item.itemId}`);
 | 
			
		||||
          throw error; // Re-throw cancellation errors
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        console.error(`❌ TTS retry failed for: ${item.itemId}`, error);
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
@@ -330,20 +394,35 @@ async function processRetryQueue(): Promise<void> {
 | 
			
		||||
 * Generate podcast for a single article
 | 
			
		||||
 * Returns true if episode was successfully created, false otherwise
 | 
			
		||||
 */
 | 
			
		||||
async function generatePodcastForArticle(article: any): Promise<boolean> {
 | 
			
		||||
async function generatePodcastForArticle(article: any, abortSignal?: AbortSignal): Promise<boolean> {
 | 
			
		||||
  console.log(`🎤 Generating podcast for: ${article.title}`);
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    // Check for cancellation
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Podcast generation was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Get feed information for context
 | 
			
		||||
    const feed = await getFeedById(article.feedId);
 | 
			
		||||
    const feedTitle = feed?.title || "Unknown Feed";
 | 
			
		||||
 | 
			
		||||
    // Check for cancellation before classification
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Podcast generation was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Classify the article/feed
 | 
			
		||||
    const category = await openAI_ClassifyFeed(
 | 
			
		||||
      `${feedTitle}: ${article.title}`,
 | 
			
		||||
    );
 | 
			
		||||
    console.log(`🏷️  Article classified as: ${category}`);
 | 
			
		||||
 | 
			
		||||
    // Check for cancellation before content generation
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Podcast generation was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate podcast content for this single article
 | 
			
		||||
    const podcastContent = await openAI_GeneratePodcastContent(article.title, [
 | 
			
		||||
      {
 | 
			
		||||
@@ -351,6 +430,11 @@ async function generatePodcastForArticle(article: any): Promise<boolean> {
 | 
			
		||||
        link: article.link,
 | 
			
		||||
      },
 | 
			
		||||
    ]);
 | 
			
		||||
    
 | 
			
		||||
    // Check for cancellation before TTS
 | 
			
		||||
    if (abortSignal?.aborted) {
 | 
			
		||||
      throw new Error('Podcast generation was cancelled');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate unique ID for the episode
 | 
			
		||||
    const episodeId = crypto.randomUUID();
 | 
			
		||||
@@ -410,6 +494,10 @@ async function generatePodcastForArticle(article: any): Promise<boolean> {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    if (error instanceof Error && (error.message.includes('cancelled') || error.name === 'AbortError')) {
 | 
			
		||||
      console.log(`🛑 Podcast generation cancelled for: ${article.title}`);
 | 
			
		||||
      throw error; // Re-throw cancellation errors to stop the batch
 | 
			
		||||
    }
 | 
			
		||||
    console.error(
 | 
			
		||||
      `💥 Error generating podcast for article: ${article.title}`,
 | 
			
		||||
      error,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user