""" Calendar routes for the Podcastrr application. """ from flask import Blueprint, render_template, request, jsonify, current_app, url_for from app.models.podcast import Podcast, Episode from app.models.settings import Settings from app.models.database import db from datetime import datetime, timedelta import calendar import json calendar_bp = Blueprint('calendar', __name__) @calendar_bp.route('/') def index(): """ Display the calendar view. """ # 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, calendar_first_day="Monday", calendar_show_monitored_only=False ) db.session.add(settings) db.session.commit() # Get view type (month, week, day, agenda) view_type = request.args.get('view', 'month') # Get current date or use the one from the query parameters today = datetime.today() year = int(request.args.get('year', today.year)) month = int(request.args.get('month', today.month)) day = int(request.args.get('day', today.day)) # Create a date object for the selected date selected_date = datetime(year, month, day) # Get the first day of the month first_day = datetime(year, month, 1) # Get the last day of the month if month == 12: last_day = datetime(year + 1, 1, 1) - timedelta(days=1) else: last_day = datetime(year, month + 1, 1) - timedelta(days=1) # Get all days in the month days_in_month = [] current_day = first_day while current_day <= last_day: days_in_month.append(current_day) current_day += timedelta(days=1) # For week view, get the start and end of the week if view_type == 'week': # Determine the first day of the week based on settings first_day_of_week = 0 if settings.calendar_first_day == 'Sunday' else 1 # Calculate the start of the week start_of_week = selected_date - timedelta(days=(selected_date.weekday() - first_day_of_week) % 7) # Calculate the end of the week end_of_week = start_of_week + timedelta(days=6) elif view_type == 'day': # For day view, just use the selected date start_of_week = selected_date end_of_week = selected_date else: # For month view, use the first and last day of the month start_of_week = first_day end_of_week = last_day # Get episodes for the selected view (month, week, or day) query = Episode.query.filter( Episode.published_date >= start_of_week, Episode.published_date <= end_of_week ).order_by(Episode.published_date) # Apply filter for monitored podcasts only if setting is enabled if settings.calendar_show_monitored_only: query = query.join(Podcast).filter(Podcast.auto_download == True) episodes = query.all() # Group episodes by day episodes_by_day = {} # Determine which days to include based on the view type if view_type == 'week': # For week view, include all days of the week days_to_include = [] current_day = start_of_week while current_day <= end_of_week: days_to_include.append(current_day) current_day += timedelta(days=1) elif view_type == 'day': # For day view, just include the selected date days_to_include = [selected_date] else: # For month view, include all days in the month days_to_include = days_in_month # Initialize the episodes_by_day dictionary for day in days_to_include: day_str = day.strftime('%Y-%m-%d') episodes_by_day[day_str] = [] for episode in episodes: day_str = episode.published_date.strftime('%Y-%m-%d') if day_str in episodes_by_day: # Get podcast info podcast = Podcast.query.get(episode.podcast_id) # Determine status color status_class = 'status-unmonitored' if podcast and podcast.auto_download: if episode.downloaded: status_class = 'status-downloaded' elif episode.download_error: status_class = 'status-error' else: status_class = 'status-downloading' # Format air time air_time = episode.published_date.strftime('%I:%M%p').lower() # Calculate end time (using duration if available, otherwise add 30 minutes) if episode.duration: end_time = (episode.published_date + timedelta(seconds=episode.duration)).strftime('%I:%M%p').lower() else: end_time = (episode.published_date + timedelta(minutes=30)).strftime('%I:%M%p').lower() # Format episode info episode_info = { 'id': episode.id, 'podcast_id': episode.podcast_id, 'podcast_title': podcast.title if podcast else 'Unknown', 'title': episode.title, 'season': episode.season, 'episode_number': episode.episode_number, 'air_time': f"{air_time} - {end_time}", 'status_class': status_class, 'url': url_for('podcasts.view', podcast_id=episode.podcast_id) } episodes_by_day[day_str].append(episode_info) return render_template('calendar/index.html', title='Calendar', settings=settings, view_type=view_type, selected_date=selected_date, days_in_month=days_in_month, episodes_by_day=episodes_by_day, start_of_week=start_of_week, end_of_week=end_of_week, days_to_include=days_to_include, today=today, first_day=first_day) @calendar_bp.route('/events') def events(): """ Get events for the calendar as JSON. """ # Get current settings settings = Settings.query.first() # Get date range from query parameters start_date_str = request.args.get('start', '') end_date_str = request.args.get('end', '') try: start_date = datetime.fromisoformat(start_date_str.replace('Z', '+00:00')) end_date = datetime.fromisoformat(end_date_str.replace('Z', '+00:00')) except ValueError: # If dates are invalid, use current month today = datetime.today() start_date = datetime(today.year, today.month, 1) end_date = datetime(today.year, today.month + 1 if today.month < 12 else 1, 1) - timedelta(days=1) # Query episodes within the date range query = Episode.query.filter( Episode.published_date >= start_date, Episode.published_date <= end_date ) # Apply filter for monitored podcasts only if setting is enabled if settings.calendar_show_monitored_only: query = query.join(Podcast).filter(Podcast.auto_download == True) episodes = query.all() # Convert episodes to calendar events events = [] for episode in episodes: # Determine color based on status color = '#999999' # Default gray for unmonitored # Check if podcast is monitored podcast = Podcast.query.get(episode.podcast_id) if podcast and podcast.auto_download: if episode.downloaded: color = '#28a745' # Green for downloaded elif episode.download_error: color = '#dc3545' # Red for error/missing else: color = '#6f42c1' # Purple for downloading/pending events.append({ 'id': episode.id, 'title': episode.title, 'start': episode.published_date.isoformat(), 'url': url_for('podcasts.view', podcast_id=episode.podcast_id), 'color': color, 'description': f"Podcast: {podcast.title if podcast else 'Unknown'}" }) return jsonify(events) @calendar_bp.route('/ical') def ical(): """ Generate iCal feed for podcast episodes. """ # Get current settings settings = Settings.query.first() # Query episodes (limit to recent and upcoming) start_date = datetime.today() - timedelta(days=30) # Past 30 days end_date = datetime.today() + timedelta(days=30) # Next 30 days query = Episode.query.filter( Episode.published_date >= start_date, Episode.published_date <= end_date ) # Apply filter for monitored podcasts only if setting is enabled if settings.calendar_show_monitored_only: query = query.join(Podcast).filter(Podcast.auto_download == True) episodes = query.all() # Generate iCal content ical_content = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Podcastrr//EN", "CALSCALE:GREGORIAN", "METHOD:PUBLISH", f"X-WR-CALNAME:Podcastrr Episodes", f"X-WR-CALDESC:Podcast episodes from Podcastrr" ] for episode in episodes: podcast = Podcast.query.get(episode.podcast_id) pub_date = episode.published_date # Format date for iCal dt_stamp = datetime.now().strftime("%Y%m%dT%H%M%SZ") dt_start = pub_date.strftime("%Y%m%dT%H%M%SZ") ical_content.extend([ "BEGIN:VEVENT", f"UID:{episode.id}@podcastrr", f"DTSTAMP:{dt_stamp}", f"DTSTART:{dt_start}", f"SUMMARY:{episode.title}", f"DESCRIPTION:{podcast.title if podcast else 'Unknown podcast'}: {episode.description[:100] if episode.description else ''}...", "END:VEVENT" ]) ical_content.append("END:VCALENDAR") response = "\r\n".join(ical_content) return response, 200, { 'Content-Type': 'text/calendar; charset=utf-8', 'Content-Disposition': 'attachment; filename=podcastrr.ics' }