Initial commit

This commit is contained in:
Cody Cook 2025-06-14 22:53:38 -07:00
commit e86ab53de5
35 changed files with 2638 additions and 0 deletions

237
app/web/routes/api.py Normal file
View file

@ -0,0 +1,237 @@
"""
API routes for the Podcastrr application.
"""
from flask import Blueprint, jsonify, request, current_app
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_downloader import download_episode
import os
api_bp = Blueprint('api', __name__)
# Podcasts API
@api_bp.route('/podcasts', methods=['GET'])
def get_podcasts():
"""
Get all tracked podcasts.
"""
podcasts = Podcast.query.all()
return jsonify({
'podcasts': [podcast.to_dict() for podcast in podcasts]
})
@api_bp.route('/podcasts/<int:podcast_id>', methods=['GET'])
def get_podcast(podcast_id):
"""
Get a specific podcast.
"""
podcast = Podcast.query.get_or_404(podcast_id)
return jsonify(podcast.to_dict())
@api_bp.route('/podcasts/search', methods=['GET'])
def search_api():
"""
Search for podcasts.
"""
query = request.args.get('q', '')
if not query:
return jsonify({'error': 'Query parameter is required'}), 400
results = search_podcasts(query)
return jsonify({'results': results})
@api_bp.route('/podcasts', methods=['POST'])
def add_podcast():
"""
Add a podcast to track.
"""
data = request.json
if not data or 'podcast_id' not in data:
return jsonify({'error': 'podcast_id is required'}), 400
# Check if podcast already exists
existing = Podcast.query.filter_by(external_id=data['podcast_id']).first()
if existing:
return jsonify({'error': 'Podcast is already being tracked'}), 409
# Get podcast details from service
podcast_data = search_podcasts(podcast_id=data['podcast_id'])
if not podcast_data:
return jsonify({'error': 'Failed to find podcast'}), 404
# Check if feed URL is valid
import logging
logger = logging.getLogger(__name__)
if not podcast_data.get('feed_url'):
logger.error(f"No feed URL found for podcast ID: {data['podcast_id']}")
return jsonify({'error': 'Podcast has no valid RSS feed URL'}), 400
logger.info(f"Adding podcast: {podcast_data['title']} with feed URL: {podcast_data['feed_url']}")
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=data['podcast_id']
)
db.session.add(podcast)
db.session.commit()
# Fetch and add episodes for the podcast
from app.services.podcast_search import get_podcast_episodes
import logging
logger = logging.getLogger(__name__)
logger.info(f"Fetching episodes for podcast: {podcast.title} (ID: {podcast.id})")
logger.info(f"Feed URL: {podcast.feed_url}")
episodes_data = get_podcast_episodes(podcast.feed_url)
logger.info(f"Found {len(episodes_data)} episodes in feed")
episodes_added = 0
for episode_data in episodes_data:
# Check if episode has 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 by GUID
existing = Episode.query.filter_by(guid=episode_data.get('guid')).first()
if existing:
logger.debug(f"Episode already exists: {episode_data.get('title', 'Unknown')}")
continue
# 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.get('guid', '')
)
db.session.add(episode)
episodes_added += 1
logger.debug(f"Added episode: {episode.title}")
except Exception as e:
logger.error(f"Error adding episode: {str(e)}")
db.session.commit()
logger.info(f"Added {episodes_added} new episodes for podcast: {podcast.title}")
# If no episodes were added, try updating the podcast
if episodes_added == 0 and len(episodes_data) == 0:
logger.warning(f"No episodes found for podcast: {podcast.title}. Trying to update...")
try:
from app.services.podcast_updater import update_podcast
stats = update_podcast(podcast.id)
logger.info(f"Podcast update completed: {stats}")
except Exception as e:
logger.error(f"Error updating podcast: {str(e)}")
return jsonify(podcast.to_dict()), 201
@api_bp.route('/podcasts/<int:podcast_id>', methods=['DELETE'])
def delete_podcast(podcast_id):
"""
Delete a podcast from tracking.
"""
podcast = Podcast.query.get_or_404(podcast_id)
# Delete associated episodes
Episode.query.filter_by(podcast_id=podcast_id).delete()
db.session.delete(podcast)
db.session.commit()
return jsonify({'message': f'Podcast "{podcast.title}" has been deleted'})
# Podcast update API
@api_bp.route('/podcasts/<int:podcast_id>/update', methods=['POST'])
def update_podcast_api(podcast_id):
"""
Update a podcast to fetch new episodes.
"""
from app.services.podcast_updater import update_podcast
try:
stats = update_podcast(podcast_id)
return jsonify({
'message': f'Podcast updated successfully. {stats["new_episodes"]} new episodes found.',
'stats': stats
})
except Exception as e:
return jsonify({'error': str(e)}), 500
# Episodes API
@api_bp.route('/podcasts/<int:podcast_id>/episodes', methods=['GET'])
def get_episodes(podcast_id):
"""
Get all episodes for a podcast.
"""
Podcast.query.get_or_404(podcast_id) # Ensure podcast exists
episodes = Episode.query.filter_by(podcast_id=podcast_id).order_by(Episode.published_date.desc()).all()
return jsonify({
'episodes': [episode.to_dict() for episode in episodes]
})
@api_bp.route('/episodes/<int:episode_id>', methods=['GET'])
def get_episode(episode_id):
"""
Get a specific episode.
"""
episode = Episode.query.get_or_404(episode_id)
return jsonify(episode.to_dict())
@api_bp.route('/episodes/<int:episode_id>/download', methods=['POST'])
def download_episode_api(episode_id):
"""
Download an episode.
"""
episode = Episode.query.get_or_404(episode_id)
try:
download_path = download_episode(episode)
return jsonify({
'message': 'Episode downloaded successfully',
'path': download_path
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@api_bp.route('/episodes/<int:episode_id>', methods=['DELETE'])
def delete_episode(episode_id):
"""
Delete a downloaded episode.
"""
episode = Episode.query.get_or_404(episode_id)
if episode.file_path and os.path.exists(episode.file_path):
try:
os.remove(episode.file_path)
episode.file_path = None
episode.downloaded = False
db.session.commit()
return jsonify({'message': 'Episode deleted successfully'})
except Exception as e:
return jsonify({'error': str(e)}), 500
else:
return jsonify({'error': 'Episode file not found'}), 404

38
app/web/routes/main.py Normal file
View file

@ -0,0 +1,38 @@
"""
Main routes for the Podcastrr application.
"""
from flask import Blueprint, render_template, current_app
from app.models.podcast import Podcast
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
"""
Render the home page.
"""
# Get recent podcasts
recent_podcasts = Podcast.query.order_by(Podcast.last_updated.desc()).limit(5).all()
return render_template('index.html',
title='Home',
recent_podcasts=recent_podcasts)
@main_bp.route('/about')
def about():
"""
Render the about page.
"""
return render_template('about.html', title='About')
@main_bp.route('/dashboard')
def dashboard():
"""
Render the dashboard page.
"""
# Get statistics
total_podcasts = Podcast.query.count()
return render_template('dashboard.html',
title='Dashboard',
total_podcasts=total_podcasts)

140
app/web/routes/podcasts.py Normal file
View file

@ -0,0 +1,140 @@
"""
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 app.models.podcast import Podcast, Episode
from app.models.database import db
from app.services.podcast_search import search_podcasts
from app.services.podcast_downloader import download_episode
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:
results = search_podcasts(query)
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.
"""
# Check if podcast already exists
existing = Podcast.query.filter_by(external_id=podcast_id).first()
if existing:
flash('Podcast is already being tracked.', 'info')
else:
# Get podcast details from service
podcast_data = search_podcasts(podcast_id=podcast_id)
if podcast_data:
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()
flash('Podcast added successfully!', 'success')
else:
flash('Failed to add podcast.', 'error')
return redirect(url_for('podcasts.index'))
@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()
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)
# Delete associated episodes
Episode.query.filter_by(podcast_id=podcast_id).delete()
db.session.delete(podcast)
db.session.commit()
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.
"""
episode = Episode.query.get_or_404(episode_id)
try:
download_path = download_episode(episode)
flash(f'Episode downloaded to {download_path}', 'success')
except Exception as e:
flash(f'Download failed: {str(e)}', 'error')
return redirect(url_for('podcasts.view', podcast_id=episode.podcast_id))
@podcasts_bp.route('/update/<int:podcast_id>', methods=['POST'])
def update(podcast_id):
"""
Manually update a podcast to fetch new episodes.
"""
podcast = Podcast.query.get_or_404(podcast_id)
try:
from app.services.podcast_updater import update_podcast
logger.info(f"Manually updating podcast: {podcast.title} (ID: {podcast.id})")
stats = update_podcast(podcast_id)
if stats['new_episodes'] > 0:
flash(f"Found {stats['new_episodes']} new episodes!", 'success')
else:
flash("No new episodes found.", 'info')
logger.info(f"Manual update completed: {stats}")
except Exception as e:
logger.error(f"Error updating podcast: {str(e)}")
flash(f"Error updating podcast: {str(e)}", 'error')
return redirect(url_for('podcasts.view', podcast_id=podcast_id))

View file

@ -0,0 +1,88 @@
"""
Settings routes for the Podcastrr application.
"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
import os
from app.models.settings import Settings
from app.models.database import db
settings_bp = Blueprint('settings', __name__)
@settings_bp.route('/', methods=['GET', 'POST'])
def index():
"""
Display and update application settings.
"""
# Get current settings
settings = Settings.query.first()
# If no settings exist, create default settings
if not settings:
settings = Settings(
download_path=current_app.config['DOWNLOAD_PATH'],
naming_format="{podcast_title}/{episode_title}",
auto_download=False,
max_downloads=5,
delete_after_days=30
)
db.session.add(settings)
db.session.commit()
if request.method == 'POST':
# Update settings
download_path = request.form.get('download_path')
naming_format = request.form.get('naming_format')
auto_download = 'auto_download' in request.form
max_downloads = int(request.form.get('max_downloads', 5))
delete_after_days = int(request.form.get('delete_after_days', 30))
# Validate download path
if not os.path.exists(download_path):
try:
os.makedirs(download_path, exist_ok=True)
except Exception as e:
flash(f'Error creating download directory: {str(e)}', 'error')
return render_template('settings/index.html',
title='Settings',
settings=settings)
# Update settings
settings.download_path = download_path
settings.naming_format = naming_format
settings.auto_download = auto_download
settings.max_downloads = max_downloads
settings.delete_after_days = delete_after_days
db.session.commit()
# Update application config
current_app.config['DOWNLOAD_PATH'] = download_path
flash('Settings updated successfully!', 'success')
return redirect(url_for('settings.index'))
return render_template('settings/index.html',
title='Settings',
settings=settings)
@settings_bp.route('/naming-preview', methods=['POST'])
def naming_preview():
"""
Preview the naming format.
"""
naming_format = request.form.get('naming_format', '')
# Example data for preview
example_data = {
'podcast_title': 'Example Podcast',
'episode_title': 'Episode 1: Introduction',
'published_date': '2023-01-01',
'episode_number': '1'
}
try:
# Format the example data with the naming format
preview = naming_format.format(**example_data)
return {'preview': preview}
except Exception as e:
return {'error': str(e)}