Add podgrab featureset

This commit is contained in:
Cody Cook 2025-06-16 22:55:39 -07:00
commit 233dd5b5c0
33 changed files with 2315 additions and 125 deletions

View file

@ -3,11 +3,13 @@ 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
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
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__)
@ -178,6 +180,27 @@ def update(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):
"""
@ -252,3 +275,165 @@ def update_naming_format(podcast_id):
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'))