""" Podcast updater service for Podcastrr. """ import logging from datetime import datetime, timedelta from flask import current_app from app.models.database import db from app.models.podcast import Podcast, Episode from app.models.settings import Settings from app.services.podcast_search import get_podcast_episodes from app.services.podcast_downloader import download_episode # Set up logging logger = logging.getLogger(__name__) def update_all_podcasts(): """ Update all podcasts in the database. Returns: dict: Statistics about the update process. """ podcasts = Podcast.query.all() stats = { 'podcasts_updated': 0, 'new_episodes': 0, 'episodes_downloaded': 0, 'errors': 0 } for podcast in podcasts: try: result = update_podcast(podcast.id) stats['podcasts_updated'] += 1 stats['new_episodes'] += result['new_episodes'] stats['episodes_downloaded'] += result['episodes_downloaded'] except Exception as e: logger.error(f"Error updating podcast {podcast.title}: {str(e)}") stats['errors'] += 1 return stats def update_podcast(podcast_id, progress_callback=None): """ Update a specific podcast. Args: podcast_id (int): ID of the podcast to update. progress_callback (callable, optional): Callback function for progress updates. Returns: dict: Statistics about the update process. """ podcast = Podcast.query.get_or_404(podcast_id) stats = { 'new_episodes': 0, 'episodes_downloaded': 0, 'feed_status': 'success' } try: logger.info(f"Updating podcast: {podcast.title} (ID: {podcast.id})") logger.info(f"Feed URL: {podcast.feed_url}") if progress_callback: progress_callback(10, f"Fetching episodes for {podcast.title}") # Get episodes from feed episodes = get_podcast_episodes(podcast.feed_url) # Update podcast last_checked timestamp podcast.last_checked = datetime.utcnow() if progress_callback: progress_callback(30, f"Found {len(episodes)} episodes") if not episodes: logger.warning(f"No episodes found for podcast: {podcast.title}") stats['feed_status'] = 'no_episodes' # Check if we need to refresh the feed URL from iTunes if podcast.external_id: try: from app.services.podcast_search import search_podcasts logger.info(f"Trying to refresh feed URL from iTunes for podcast ID: {podcast.external_id}") podcast_data = search_podcasts(podcast_id=podcast.external_id) if podcast_data and podcast_data.get('feed_url') and podcast_data['feed_url'] != podcast.feed_url: logger.info(f"Updated feed URL from {podcast.feed_url} to {podcast_data['feed_url']}") podcast.feed_url = podcast_data['feed_url'] db.session.commit() # Try again with the new feed URL episodes = get_podcast_episodes(podcast.feed_url) logger.info(f"Found {len(episodes)} episodes with updated feed URL") except Exception as e: logger.error(f"Error refreshing feed URL: {str(e)}") # Process each episode total_episodes = len(episodes) for i, episode_data in enumerate(episodes): if progress_callback and total_episodes > 0: progress = 30 + int((i / total_episodes) * 60) # Scale from 30% to 90% progress_callback(progress, f"Processing episode {i+1}/{total_episodes}") # Skip episodes without required fields if not episode_data.get('guid'): logger.warning(f"Skipping episode without GUID: {episode_data.get('title', 'Unknown')}") continue if not episode_data.get('audio_url'): logger.warning(f"Skipping episode without audio URL: {episode_data.get('title', 'Unknown')}") continue # Check if episode already exists existing = Episode.query.filter_by(guid=episode_data['guid']).first() if not existing: # Create new episode try: episode = Episode( podcast_id=podcast.id, title=episode_data.get('title', ''), description=episode_data.get('description', ''), audio_url=episode_data.get('audio_url', ''), image_url=episode_data.get('image_url', podcast.image_url), # Use podcast image if episode has none published_date=episode_data.get('published_date'), duration=episode_data.get('duration'), file_size=episode_data.get('file_size'), episode_number=episode_data.get('episode_number'), guid=episode_data['guid'], downloaded=False ) db.session.add(episode) stats['new_episodes'] += 1 logger.info(f"Added new episode: {episode.title}") # Auto-download if enabled if podcast.auto_download and episode.audio_url: try: # Need to commit first to ensure episode has an ID db.session.commit() download_episode(episode.id) stats['episodes_downloaded'] += 1 logger.info(f"Auto-downloaded episode: {episode.title}") except Exception as e: logger.error(f"Error auto-downloading episode {episode.title}: {str(e)}") except Exception as e: logger.error(f"Error adding episode: {str(e)}") # Update podcast last_updated timestamp if new episodes were found if stats['new_episodes'] > 0: podcast.last_updated = datetime.utcnow() db.session.commit() logger.info(f"Podcast update completed: {stats}") if progress_callback: progress_callback(100, f"Update complete. Found {stats['new_episodes']} new episodes.") return stats except Exception as e: db.session.rollback() logger.error(f"Error updating podcast {podcast.title}: {str(e)}") stats['feed_status'] = 'error' stats['error'] = str(e) if progress_callback: progress_callback(100, f"Error: {str(e)}") raise def schedule_updates(): """ Schedule podcast updates based on settings. This function is meant to be called by a scheduler (e.g., APScheduler). """ logger.info("Starting scheduled podcast updates") try: stats = update_all_podcasts() logger.info(f"Scheduled update completed: {stats}") except Exception as e: logger.error(f"Error during scheduled update: {str(e)}") def clean_old_downloads(): """ Clean up old downloaded episodes. This function is meant to be called by a scheduler (e.g., APScheduler). """ from app.services.podcast_downloader import delete_old_episodes logger.info("Starting cleanup of old downloads") try: count = delete_old_episodes() logger.info(f"Deleted {count} old episodes") except Exception as e: logger.error(f"Error during cleanup: {str(e)}")