439 lines
15 KiB
Python
439 lines
15 KiB
Python
"""
|
|
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/<string:podcast_id>', 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('/<int:podcast_id>')
|
|
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/<int:podcast_id>', 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/<int:episode_id>')
|
|
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/<int:podcast_id>', 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/<int:podcast_id>', 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/<int:podcast_id>', 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/<int:episode_id>', 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/<int:podcast_id>', 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/<int:podcast_id>', 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/<string: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'))
|