Skip to content

🚀 :Email output instead of to Discord #26

@sleighton

Description

@sleighton

Love the idea of this app but I don't use discord so I asked Gemini to modify your calendar-to-discord.py script to output to email instead of discord. Amazingly it created a totally usable script. I was surprised and am a bit worried for devs and programmers if AI is getting this smart.

Anyways, anyone who would prefer output to email instead of discord can implement it on their system with these instructions.

Add the following to the environment section of your docker-compose.yml, example assumes gmail is the mail server

      environment:
         EMAIL_SENDER: youremail@gmail.com
         EMAIL_RECEIVER: youremail@gmail.com
         EMAIL_PASSWORD: youremailapppassword
         SMTP_SERVER: smtp.gmail.com
         SMTP_PORT: 587

Add the following to the volumes section of docker-compose.yml. This causes the container to run this version of the script instead of the discord version in the container

      volumes:
         - ./calendarr-to-email.py:/app/src/main.py

Save the following script as calendarr-to-email.py in the same folder that you have your docker-compose.yml file in. This makes the script available to map using the volumes entry you added above.

import requests
import icalendar
import recurring_ical_events
import datetime
import sys
import os
import json
import re
import smtplib
from email.mime.text import MIMEText

def get_events_for_week(ical_urls, start_week_on_monday=True):
    # Use today's date
    base_date = datetime.date.today()
    
    # Calculate start of week based on preference
    if start_week_on_monday:
        # Start on Monday (0 = Monday in our calculation)
        start_offset = (7 - base_date.weekday()) % 7
    else:
        # Start on Sunday (6 = Sunday in our calculation)
        start_offset = (7 - (base_date.weekday() + 1) % 7) % 7
        if start_offset == 0:
            start_offset = 7
    
    start_of_week_date = base_date + datetime.timedelta(days=start_offset)
    end_of_week_date = start_of_week_date + datetime.timedelta(days=6)

    # Convert those dates to full datetimes
    start_of_week = datetime.datetime.combine(start_of_week_date, datetime.time.min)
    end_of_week = datetime.datetime.combine(end_of_week_date, datetime.time.max)

    all_events = []
    
    for url_info in ical_urls:
        url = url_info["url"]
        source_type = url_info["type"]  # "tv" or "movie"
        
        response = requests.get(url)
        if response.status_code != 200:
            print(f"Failed to fetch iCal from {url}: {response.status_code}")
            continue
        
        calendar = icalendar.Calendar.from_ical(response.content)
        
        events = recurring_ical_events.of(calendar).between(
            start_of_week, 
            end_of_week
        )
        
        # Convert date-only events to datetime
        for event in events:
            dtstart = event.get('DTSTART').dt
            # Strip timezone if present
            if isinstance(dtstart, datetime.datetime) and dtstart.tzinfo is not None:
                dtstart = dtstart.replace(tzinfo=None)
            if isinstance(dtstart, datetime.date) and not isinstance(dtstart, datetime.datetime):
                dtstart = datetime.datetime(dtstart.year, dtstart.month, dtstart.day)
            event['DTSTART'].dt = dtstart
        
        # Add source type to each event
        for event in events:
            event["SOURCE_TYPE"] = source_type
        
        all_events.extend(events)
    
    return all_events, start_of_week, end_of_week

def create_show_report(events, start_date, end_date):
    if not events:
        return ""
    
    # Count TV episodes and movies
    tv_count = sum(1 for e in events if e.get("SOURCE_TYPE") == "tv")
    movie_count = sum(1 for e in events if e.get("SOURCE_TYPE") == "movie")
    
    # Sort events by date
    sorted_events = sorted(events, key=lambda e: e.get('DTSTART').dt)
    
    # Group events by day
    days = {}
    for event in sorted_events:
        start = event.get('DTSTART').dt
        source_type = event.get("SOURCE_TYPE")
        
        day_key = start.strftime('%A, %b %d')
        
        if day_key not in days:
            days[day_key] = {"tv": [], "movie": []}
        
        summary = event.get('SUMMARY', 'Untitled Event')
        
        # Check if this is a season premiere
        is_premiere = False
        if source_type == "tv" and re.search(r'[-\s](?:s\d+e01|(?:\d+x01))\b', summary.lower()):
            is_premiere = True
        
        # Process TV show titles
        if source_type == "tv":
            parts = re.split(r'\s+-\s+', summary, 1)
            if len(parts) == 2:
                show_name = parts[0]
                episode_info = parts[1]
                sub_parts = re.split(r'\s+-\s+', episode_info, 1)
                if len(sub_parts) == 2:
                    episode_num, episode_title = sub_parts
                else:
                    episode_num = episode_info
                    episode_title = ""
                
                line = f"{start.strftime('%I:%M %p')}: {show_name} - {episode_num} - {episode_title}"
            else:
                line = f"{start.strftime('%I:%M %p')}: {summary}"
            
            if is_premiere:
                line += " 🎉"
            
            days[day_key]["tv"].append(line)
        else:  # movie
            days[day_key]["movie"].append(f"🎬 {summary}")
    
    # Generate the full report text
    report_parts = []
    
    # Header
    header_text = f"TV Guide ({start_date.strftime('%b %d')} - {end_date.strftime('%b %d')})"
    report_parts.append(f"Subject: {header_text}")
    report_parts.append("\n" + "="*len(header_text) + "\n")
    report_parts.append(f"{tv_count} new episodes and {movie_count} movies this week.\n\n")

    # Body
    day_order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    
    # Sort days dictionary by day of the week
    sorted_days = sorted(days.items(), key=lambda item: day_order.index(item[0].split(',')[0]))
    
    for day, content in sorted_days:
        report_parts.append(f"**{day}**")
        if content["tv"]:
            report_parts.append("\n".join(content["tv"]))
        if content["movie"]:
            report_parts.append("\n\n**MOVIES**")
            report_parts.append("\n".join(content["movie"]))
        report_parts.append("\n\n")
        
    return "\n".join(report_parts)

def send_report_to_email(sender, receiver, password, smtp_server, smtp_port, report_content):
    try:
        msg = MIMEText(report_content)
        
        # Extract subject from the first line of the report
        subject = report_content.split('\n')[0].replace("Subject: ", "")
        
        msg['Subject'] = subject
        msg['From'] = sender
        msg['To'] = receiver

        with smtplib.SMTP(smtp_server, smtp_port) as server:
            server.starttls()
            server.login(sender, password)
            server.send_message(msg)
        
        print(f"Report successfully sent to {receiver}")
        return True
    except Exception as e:
        print(f"Failed to send email: {e}")
        return False

def main():
    print("Starting Calendar to Email script")
    
    # Get environment variables
    email_sender = os.environ.get("EMAIL_SENDER")
    email_receiver = os.environ.get("EMAIL_RECEIVER")
    email_password = os.environ.get("EMAIL_PASSWORD")
    smtp_server = os.environ.get("SMTP_SERVER")
    smtp_port = os.environ.get("SMTP_PORT", 587)  # Default to common TLS port
    
    calendar_urls_json = os.environ.get("CALENDAR_URLS")
    start_week_on_monday = os.environ.get("START_WEEK_ON_MONDAY", "true").lower() == "true"
    
    # Validate required email variables
    if not all([email_sender, email_receiver, email_password, smtp_server]):
        print("Error: EMAIL_SENDER, EMAIL_RECEIVER, EMAIL_PASSWORD, and SMTP_SERVER environment variables must be set.")
        sys.exit(1)

    if not calendar_urls_json:
        print("Error: CALENDAR_URLS environment variable not set")
        sys.exit(1)
    
    try:
        calendar_urls = json.loads(calendar_urls_json)
        if not isinstance(calendar_urls, list):
            raise ValueError("CALENDAR_URLS must be a JSON array")
    except json.JSONDecodeError:
        print("Error: CALENDAR_URLS is not valid JSON")
        sys.exit(1)

    try:
        # Get events
        print(f"Fetching events from {len(calendar_urls)} calendars")
        events, start_date, end_date = get_events_for_week(calendar_urls, start_week_on_monday)
        events_count = len(events)
        print(f"Found {events_count} events")
        
        # Create report string
        print("Creating email report")
        report_content = create_show_report(events, start_date, end_date)
        
        if not report_content:
            print("No events found for the week. Skipping email.")
            sys.exit(0)

        # Send report via email
        print(f"Sending email to {email_receiver}")
        success = send_report_to_email(
            sender=email_sender,
            receiver=email_receiver,
            password=email_password,
            smtp_server=smtp_server,
            smtp_port=int(smtp_port),
            report_content=report_content
        )
        
        if success:
            print("Script completed successfully")
        else:
            print("Script failed to send email")
            sys.exit(1)

    except Exception as e:
        print(f"Error in main function: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Start the container. You should receive a startup email. If not, check the container logs for errors (like bad app password for email).

Those who want output other than discord or email might consider using this approach and asking Gemini to create a version. Have fun and thanks to @jordanlambrecht for creating this useful app!

Metadata

Metadata

Labels

Projects

Status

Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions