""" Podcast routes for the Podcastrr application. """ import logging logger = logging.getLogger(__name__) from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, Response, send_file from app.models.podcast import Podcast, Episode from app.models.database import db from app.services.podcast_search import search_podcasts, get_podcast_episodes from app.services.podcast_downloader import download_episode from app.services.opml_handler import generate_opml, import_podcasts_from_opml import io podcasts_bp = Blueprint('podcasts', __name__) @podcasts_bp.route('/') def index(): """ Display all tracked podcasts. """ podcasts = Podcast.query.all() return render_template('podcasts/index.html', title='Podcasts', podcasts=podcasts) @podcasts_bp.route('/search', methods=['GET', 'POST']) def search(): """ Search for podcasts. """ results = [] if request.method == 'POST': query = request.form.get('query', '') if query: logger.info(f"Searching for podcasts with query: {query}") results = search_podcasts(query) logger.info(f"Found {len(results)} results") return render_template('podcasts/search.html', title='Search Podcasts', results=results) @podcasts_bp.route('/add/', methods=['POST']) def add(podcast_id): """ Add a podcast to track. """ logger.info(f"Adding podcast with ID: {podcast_id}") # Check if podcast already exists existing = Podcast.query.filter_by(external_id=podcast_id).first() if existing: flash('Podcast is already being tracked.', 'info') return redirect(url_for('podcasts.index')) # Get podcast details from service podcast_data = search_podcasts(podcast_id=podcast_id) if not podcast_data: flash('Failed to get podcast details.', 'error') return redirect(url_for('podcasts.search')) logger.info(f"Got podcast data: {podcast_data['title']}") logger.info(f"Feed URL: {podcast_data.get('feed_url', 'No feed URL')}") # Create podcast record podcast = Podcast( title=podcast_data['title'], author=podcast_data['author'], description=podcast_data['description'], image_url=podcast_data['image_url'], feed_url=podcast_data['feed_url'], external_id=podcast_id ) db.session.add(podcast) db.session.commit() logger.info(f"Podcast saved with ID: {podcast.id}") # Fetch episodes immediately after adding if podcast_data.get('feed_url'): try: from app.services.podcast_updater import update_podcast logger.info(f"Fetching episodes for newly added podcast: {podcast.title}") stats = update_podcast(podcast.id) logger.info(f"Update stats: {stats}") if stats and stats.get('new_episodes', 0) > 0: flash(f'Podcast added successfully! Found {stats["new_episodes"]} episodes.', 'success') else: flash('Podcast added successfully! No episodes found yet. The feed might be empty or inaccessible.', 'info') logger.warning(f"No episodes found for podcast: {podcast.title}") except Exception as e: logger.error(f"Error fetching episodes for new podcast: {str(e)}", exc_info=True) flash(f'Podcast added successfully, but failed to fetch episodes: {str(e)}', 'error') else: flash('Podcast added successfully, but no RSS feed URL available.', 'info') logger.warning(f"No feed URL available for podcast: {podcast.title}") return redirect(url_for('podcasts.view', podcast_id=podcast.id)) @podcasts_bp.route('/') def view(podcast_id): """ View a specific podcast and its episodes. """ podcast = Podcast.query.get_or_404(podcast_id) episodes = Episode.query.filter_by(podcast_id=podcast_id).order_by(Episode.published_date.desc()).all() logger.info(f"Viewing podcast: {podcast.title} with {len(episodes)} episodes") return render_template('podcasts/view.html', title=podcast.title, podcast=podcast, episodes=episodes) @podcasts_bp.route('/delete/', methods=['POST']) def delete(podcast_id): """ Delete a podcast from tracking. """ podcast = Podcast.query.get_or_404(podcast_id) podcast_title = podcast.title # Delete associated episodes Episode.query.filter_by(podcast_id=podcast_id).delete() db.session.delete(podcast) db.session.commit() logger.info(f"Deleted podcast: {podcast_title}") flash(f'Podcast "{podcast_title}" has been deleted.', 'success') return redirect(url_for('podcasts.index')) @podcasts_bp.route('/download/') def download(episode_id): """ Download an episode in the background. """ from app.services.task_manager import task_manager episode = Episode.query.get_or_404(episode_id) episode_title = episode.title podcast_id = episode.podcast_id # Create a background task for the download task_id = task_manager.create_task( 'download', f"Downloading episode: {episode_title}", download_episode, episode_id ) flash(f'Download started in the background. Check the status in the tasks panel.', 'info') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/update/', methods=['POST']) def update(podcast_id): """ Manually update a podcast to fetch new episodes in the background. """ from app.services.task_manager import task_manager from app.services.podcast_updater import update_podcast podcast = Podcast.query.get_or_404(podcast_id) logger.info(f"Starting background update for podcast: {podcast.title} (ID: {podcast.id})") # Create a background task for the update task_id = task_manager.create_task( 'update', f"Updating podcast: {podcast.title}", update_podcast, podcast_id ) flash(f'Update started in the background. Check the status in the tasks panel.', 'info') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/download_all/', methods=['POST']) def download_all(podcast_id): """ Download all episodes of a podcast in the background. """ from app.services.task_manager import task_manager from app.services.podcast_downloader import download_all_episodes podcast = Podcast.query.get_or_404(podcast_id) # Create a background task for downloading all episodes task_id = task_manager.create_task( 'download_all', f"Downloading all episodes for podcast: {podcast.title}", download_all_episodes, podcast_id ) flash(f'Download of all episodes started in the background. Check the status in the tasks panel.', 'info') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/verify/', methods=['POST']) def verify(podcast_id): """ Verify that downloaded episodes still exist on disk. """ from app.services.task_manager import task_manager from app.services.podcast_downloader import verify_downloaded_episodes podcast = Podcast.query.get_or_404(podcast_id) # Create a background task for verification task_id = task_manager.create_task( 'verify', f"Verifying episodes for podcast: {podcast.title}", verify_downloaded_episodes, podcast_id ) flash(f'Verification started in the background. Check the status in the tasks panel.', 'info') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/rename/', methods=['POST']) def rename_episode(episode_id): """ Rename a downloaded episode file. """ from app.services.task_manager import task_manager from app.services.podcast_downloader import rename_episode as rename_episode_func episode = Episode.query.get_or_404(episode_id) episode_title = episode.title podcast_id = episode.podcast_id # Check if episode is downloaded if not episode.downloaded or not episode.file_path: flash('Episode is not downloaded.', 'error') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) # Create a background task for renaming task_id = task_manager.create_task( 'rename', f"Renaming episode: {episode_title}", rename_episode_func, episode_id ) flash(f'Renaming started in the background. Check the status in the tasks panel.', 'info') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/update_naming_format/', methods=['POST']) def update_naming_format(podcast_id): """ Update the naming format for a podcast. """ podcast = Podcast.query.get_or_404(podcast_id) # Get the naming format from the form naming_format = request.form.get('naming_format') # If custom format is selected, use the custom format if naming_format == 'custom': naming_format = request.form.get('custom_format') # Update the podcast's naming format podcast.naming_format = naming_format db.session.commit() # Flash a message to the user if naming_format: flash(f'Naming format updated for {podcast.title}.', 'success') else: flash(f'Naming format reset to global settings for {podcast.title}.', 'success') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/update_tags/', methods=['POST']) def update_tags(podcast_id): """ Update the tags for a podcast. """ podcast = Podcast.query.get_or_404(podcast_id) # Get the tags from the form tags = request.form.get('tags', '') # Split the tags by comma and strip whitespace tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] # Update the podcast's tags podcast.tags = ','.join(tag_list) if tag_list else None db.session.commit() flash(f'Tags updated for {podcast.title}.', 'success') return redirect(url_for('podcasts.view', podcast_id=podcast_id)) @podcasts_bp.route('/tag/') def filter_by_tag(tag): """ Filter podcasts by tag. """ # Find all podcasts with the given tag # We need to use LIKE with wildcards because tags are stored as a comma-separated string podcasts = Podcast.query.filter( (Podcast.tags == tag) | # Exact match (Podcast.tags.like(f'{tag},%')) | # Tag at the beginning (Podcast.tags.like(f'%,{tag},%')) | # Tag in the middle (Podcast.tags.like(f'%,{tag}')) # Tag at the end ).all() return render_template('podcasts/index.html', title=f'Podcasts tagged with "{tag}"', podcasts=podcasts, current_tag=tag) @podcasts_bp.route('/import_opml', methods=['GET', 'POST']) def import_opml(): """ Import podcasts from an OPML file. """ if request.method == 'POST': # Check if a file was uploaded if 'opml_file' not in request.files: flash('No file selected.', 'error') return redirect(url_for('podcasts.index')) opml_file = request.files['opml_file'] # Check if the file has a name if opml_file.filename == '': flash('No file selected.', 'error') return redirect(url_for('podcasts.index')) # Check if the file is an OPML file if not opml_file.filename.lower().endswith('.opml') and not opml_file.filename.lower().endswith('.xml'): flash('Invalid file format. Please upload an OPML file.', 'error') return redirect(url_for('podcasts.index')) # Read the file content opml_content = opml_file.read().decode('utf-8') # Import podcasts from the OPML file from app.services.task_manager import task_manager # Create a background task for importing task_id = task_manager.create_task( 'import_opml', f"Importing podcasts from OPML file: {opml_file.filename}", import_podcasts_from_opml, opml_content ) flash(f'OPML import started in the background. Check the status in the tasks panel.', 'info') return redirect(url_for('podcasts.index')) return render_template('podcasts/import_opml.html', title='Import OPML') @podcasts_bp.route('/export_opml') def export_opml(): """ Export podcasts to an OPML file. """ # Get all podcasts podcasts = Podcast.query.all() # Generate OPML content opml_content = generate_opml(podcasts) # Create a file-like object from the OPML content opml_file = io.BytesIO(opml_content.encode('utf-8')) # Return the file as a download return send_file( opml_file, mimetype='application/xml', as_attachment=True, download_name='podcastrr_subscriptions.opml' ) @podcasts_bp.route('/add_by_url', methods=['POST']) def add_by_url(): """ Add a podcast by its RSS feed URL. """ feed_url = request.form.get('feed_url', '').strip() if not feed_url: flash('Please enter a valid RSS feed URL.', 'error') return redirect(url_for('podcasts.search')) # Check if podcast already exists existing = Podcast.query.filter_by(feed_url=feed_url).first() if existing: flash('Podcast is already being tracked.', 'info') return redirect(url_for('podcasts.view', podcast_id=existing.id)) try: # Try to get podcast episodes to validate the feed episodes = get_podcast_episodes(feed_url) if not episodes: flash('No episodes found in the feed. Please check the URL and try again.', 'error') return redirect(url_for('podcasts.search')) # Get the first episode to extract podcast info first_episode = episodes[0] # Create podcast record with basic info podcast = Podcast( title=first_episode.get('podcast_title', 'Unknown Podcast'), feed_url=feed_url ) db.session.add(podcast) db.session.commit() # Fetch episodes immediately after adding from app.services.podcast_updater import update_podcast # Create a background task for updating from app.services.task_manager import task_manager task_id = task_manager.create_task( 'update', f"Fetching episodes for newly added podcast: {podcast.title}", update_podcast, podcast.id ) flash(f'Podcast added successfully! Fetching episodes in the background.', 'success') return redirect(url_for('podcasts.view', podcast_id=podcast.id)) except Exception as e: logger.error(f"Error adding podcast by URL: {str(e)}") flash(f'Error adding podcast: {str(e)}', 'error') return redirect(url_for('podcasts.search'))