Add podgrab featureset
This commit is contained in:
parent
095bf52a2f
commit
233dd5b5c0
33 changed files with 2315 additions and 125 deletions
|
@ -33,6 +33,37 @@ def dashboard():
|
|||
# Get statistics
|
||||
total_podcasts = Podcast.query.count()
|
||||
|
||||
# Get episode statistics
|
||||
from app.models.podcast import Episode
|
||||
total_episodes = Episode.query.count()
|
||||
downloaded_episodes = Episode.query.filter_by(downloaded=True).count()
|
||||
not_downloaded_episodes = total_episodes - downloaded_episodes
|
||||
|
||||
# Calculate total storage used (in bytes)
|
||||
from sqlalchemy import func
|
||||
total_storage_bytes = Episode.query.filter_by(downloaded=True).with_entities(
|
||||
func.sum(Episode.file_size)).scalar() or 0
|
||||
|
||||
# Format storage size in appropriate units
|
||||
def format_size(size_bytes):
|
||||
# Convert bytes to appropriate unit
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.2f} KB"
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||||
elif size_bytes < 1024 * 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024 * 1024):.2f} TB"
|
||||
|
||||
formatted_storage = format_size(total_storage_bytes)
|
||||
|
||||
return render_template('dashboard.html',
|
||||
title='Dashboard',
|
||||
total_podcasts=total_podcasts)
|
||||
total_podcasts=total_podcasts,
|
||||
total_episodes=total_episodes,
|
||||
downloaded_episodes=downloaded_episodes,
|
||||
not_downloaded_episodes=not_downloaded_episodes,
|
||||
formatted_storage=formatted_storage)
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -2,14 +2,36 @@
|
|||
Task-related routes for the Podcastrr application.
|
||||
"""
|
||||
import logging
|
||||
from flask import Blueprint, jsonify, request, current_app
|
||||
from app.services.task_manager import task_manager
|
||||
from flask import Blueprint, jsonify, request, current_app, render_template
|
||||
from app.services.task_manager import task_manager, TaskStatus
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tasks_bp = Blueprint('tasks', __name__)
|
||||
|
||||
@tasks_bp.route('/tasks', methods=['GET'])
|
||||
def view_tasks():
|
||||
"""
|
||||
Render the tasks page showing task history and in-progress tasks.
|
||||
"""
|
||||
tasks = task_manager.get_all_tasks()
|
||||
|
||||
# Separate tasks by status
|
||||
running_tasks = [task for task in tasks if task.status == TaskStatus.RUNNING or task.status == TaskStatus.PENDING]
|
||||
completed_tasks = [task for task in tasks if task.status == TaskStatus.COMPLETED]
|
||||
failed_tasks = [task for task in tasks if task.status == TaskStatus.FAILED]
|
||||
|
||||
# Sort tasks by created_at (newest first)
|
||||
running_tasks.sort(key=lambda x: x.created_at, reverse=True)
|
||||
completed_tasks.sort(key=lambda x: x.completed_at or x.created_at, reverse=True)
|
||||
failed_tasks.sort(key=lambda x: x.completed_at or x.created_at, reverse=True)
|
||||
|
||||
return render_template('tasks/index.html',
|
||||
running_tasks=running_tasks,
|
||||
completed_tasks=completed_tasks,
|
||||
failed_tasks=failed_tasks)
|
||||
|
||||
@tasks_bp.route('/api/tasks', methods=['GET'])
|
||||
def get_tasks():
|
||||
"""
|
||||
|
@ -17,10 +39,10 @@ def get_tasks():
|
|||
"""
|
||||
status = request.args.get('status')
|
||||
tasks = task_manager.get_all_tasks()
|
||||
|
||||
|
||||
if status:
|
||||
tasks = [task for task in tasks if task.status.value == status]
|
||||
|
||||
|
||||
return jsonify({
|
||||
'tasks': [task.to_dict() for task in tasks]
|
||||
})
|
||||
|
@ -31,10 +53,10 @@ def get_task(task_id):
|
|||
Get a specific task by ID.
|
||||
"""
|
||||
task = task_manager.get_task(task_id)
|
||||
|
||||
|
||||
if not task:
|
||||
return jsonify({'error': 'Task not found'}), 404
|
||||
|
||||
|
||||
return jsonify(task.to_dict())
|
||||
|
||||
@tasks_bp.route('/api/tasks/clean', methods=['POST'])
|
||||
|
@ -44,8 +66,8 @@ def clean_tasks():
|
|||
"""
|
||||
max_age = request.json.get('max_age_seconds', 3600) if request.json else 3600
|
||||
count = task_manager.clean_old_tasks(max_age)
|
||||
|
||||
|
||||
return jsonify({
|
||||
'message': f'Cleaned up {count} old tasks',
|
||||
'count': count
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue