podcastrr/app/web/routes/calendar.py
2025-06-17 16:00:46 -07:00

287 lines
10 KiB
Python

"""
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'
}