287 lines
10 KiB
Python
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'
|
|
}
|