From 4c4f2b437e6c590fc8a44972740f5a38c8552596 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:50:09 -0800 Subject: [PATCH] Add chase notifications and weekly digest emails --- backend/import_elgoose.py | 13 ++ backend/scripts/cron_weekly_digest.sh | 6 + backend/services/chase_notifications.py | 87 ++++++++++ backend/services/weekly_digest.py | 220 ++++++++++++++++++++++++ docker-compose.yml | 24 +++ 5 files changed, 350 insertions(+) create mode 100644 backend/scripts/cron_weekly_digest.sh create mode 100644 backend/services/chase_notifications.py create mode 100644 backend/services/weekly_digest.py diff --git a/backend/import_elgoose.py b/backend/import_elgoose.py index d9ec44d..dedaa74 100644 --- a/backend/import_elgoose.py +++ b/backend/import_elgoose.py @@ -265,6 +265,7 @@ def import_setlists(session, show_map, song_map): page = 1 total_processed = 0 performance_count = 0 + new_performance_ids = [] # Track new performances for chase notifications params = {} while True: @@ -321,12 +322,24 @@ def import_setlists(session, show_map, song_map): notes=perf_data.get('notes') ) session.add(perf) + session.flush() # Get the ID + new_performance_ids.append(perf.id) count_in_page += 1 performance_count += 1 session.commit() + + # Send chase notifications for new performances + if new_performance_ids: + try: + from services.chase_notifications import check_new_performances_for_chases + check_new_performances_for_chases(session, new_performance_ids) + except Exception as e: + print(f" Warning: Chase notifications failed: {e}") + print(f" Validated/Added {count_in_page} items.") page += 1 + new_performance_ids = [] # Reset for next page print(f"ā Imported {performance_count} new performances") diff --git a/backend/scripts/cron_weekly_digest.sh b/backend/scripts/cron_weekly_digest.sh new file mode 100644 index 0000000..7b892b6 --- /dev/null +++ b/backend/scripts/cron_weekly_digest.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Cron job runner for Elmeg +# Add to crontab: 0 9 * * 0 /app/scripts/cron_weekly_digest.sh + +cd /app +python services/weekly_digest.py diff --git a/backend/services/chase_notifications.py b/backend/services/chase_notifications.py new file mode 100644 index 0000000..f512fc8 --- /dev/null +++ b/backend/services/chase_notifications.py @@ -0,0 +1,87 @@ +""" +Chase Notification Service +Sends email notifications to users when songs they're chasing are played. +""" +from sqlmodel import Session, select +from models import ChaseSong, User, Song, Show, Performance, Venue +from helpers import create_notification + + +def notify_chase_users(session: Session, song_id: int, show_id: int, performance_id: int): + """ + Send notifications to all users chasing this song. + Called after a new performance is imported. + """ + from services.email_service import send_chase_notification_email + + # Get song, show, and venue info for the notification + song = session.get(Song, song_id) + show = session.get(Show, show_id) + + if not song or not show: + return 0 + + venue = session.get(Venue, show.venue_id) if show.venue_id else None + + # Find all users chasing this song who haven't caught it yet + chase_songs = session.exec( + select(ChaseSong) + .where(ChaseSong.song_id == song_id) + .where(ChaseSong.caught_at.is_(None)) # Not yet caught + ).all() + + notified_count = 0 + + for chase in chase_songs: + user = session.get(User, chase.user_id) + if not user: + continue + + # Create in-app notification + create_notification( + session, + user_id=user.id, + title="Chase Alert!", + message=f"{song.title} was played at {venue.name if venue else 'a show'}!", + type="chase", + link=f"/performances/{performance_id}" # Link to performance page + ) + + # Send email if user has chase alerts enabled + if user.preferences and user.preferences.email_on_chase: + show_date = show.date.strftime("%Y-%m-%d") if show.date else "Unknown date" + venue_name = venue.name if venue else "Unknown venue" + + # Get performance slug for link + perf = session.get(Performance, performance_id) + perf_link = f"/performances/{perf.slug}" if perf and perf.slug else f"/shows/{show.slug}" + + send_chase_notification_email( + to_email=user.email, + song_title=song.title, + show_date=show_date, + venue_name=venue_name, + link=perf_link + ) + notified_count += 1 + + return notified_count + + +def check_new_performances_for_chases(session: Session, performance_ids: list): + """ + Check a list of newly imported performances and notify chase users. + Call this after batch importing shows. + """ + total_notified = 0 + + for perf_id in performance_ids: + perf = session.get(Performance, perf_id) + if perf: + count = notify_chase_users(session, perf.song_id, perf.show_id, perf.id) + total_notified += count + + if total_notified > 0: + print(f"š§ Sent {total_notified} chase notification emails") + + return total_notified diff --git a/backend/services/weekly_digest.py b/backend/services/weekly_digest.py new file mode 100644 index 0000000..06e464b --- /dev/null +++ b/backend/services/weekly_digest.py @@ -0,0 +1,220 @@ +""" +Weekly Digest Email Service +Sends weekly digest emails to users who have email_digest enabled. +Run via cron job: 0 9 * * 0 (Sunday at 9am) +""" +import os +import sys + +# Add backend to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from datetime import datetime, timedelta +from sqlmodel import Session, select, func +from database import engine +from models import User, Show, Performance, Review, Rating, ChaseSong, Attendance + + +def get_weekly_stats(session: Session, user_id: int, start_date: datetime, end_date: datetime): + """Get user's activity stats for the week""" + + # Shows attended this week + shows_attended = session.exec( + select(func.count(Attendance.id)) + .where(Attendance.user_id == user_id) + .where(Attendance.created_at >= start_date) + .where(Attendance.created_at < end_date) + ).one() or 0 + + # Reviews written this week + reviews_written = session.exec( + select(func.count(Review.id)) + .where(Review.user_id == user_id) + .where(Review.created_at >= start_date) + .where(Review.created_at < end_date) + ).one() or 0 + + # Ratings given this week + ratings_given = session.exec( + select(func.count(Rating.id)) + .where(Rating.user_id == user_id) + .where(Rating.created_at >= start_date) + .where(Rating.created_at < end_date) + ).one() or 0 + + # Check for caught chase songs + chase_caught = session.exec( + select(func.count(ChaseSong.id)) + .where(ChaseSong.user_id == user_id) + .where(ChaseSong.caught_at >= start_date) + .where(ChaseSong.caught_at < end_date) + ).one() or 0 + + return { + "shows_attended": shows_attended, + "reviews_written": reviews_written, + "ratings_given": ratings_given, + "chase_caught": chase_caught + } + + +def get_community_highlights(session: Session, start_date: datetime, end_date: datetime): + """Get community highlights for the week""" + + # New shows added + new_shows = session.exec( + select(func.count(Show.id)) + .where(Show.date >= start_date.date()) + .where(Show.date < end_date.date()) + ).one() or 0 + + # Total reviews this week + total_reviews = session.exec( + select(func.count(Review.id)) + .where(Review.created_at >= start_date) + .where(Review.created_at < end_date) + ).one() or 0 + + # Most reviewed performance this week + top_perf = session.exec( + select(Performance.id, func.count(Review.id).label("count")) + .join(Review, Review.performance_id == Performance.id) + .where(Review.created_at >= start_date) + .where(Review.created_at < end_date) + .group_by(Performance.id) + .order_by(func.count(Review.id).desc()) + .limit(1) + ).first() + + return { + "new_shows": new_shows, + "total_reviews": total_reviews, + "top_performance_id": top_perf[0] if top_perf else None + } + + +def send_digest_email(user, stats: dict, highlights: dict, frontend_url: str): + """Send the weekly digest email to a user""" + from services.email_service import email_service + + subject = "Your Weekly Elmeg Digest" + + # Build activity section + activity_items = [] + if stats["shows_attended"] > 0: + activity_items.append(f"šµ Attended {stats['shows_attended']} show{'s' if stats['shows_attended'] > 1 else ''}") + if stats["reviews_written"] > 0: + activity_items.append(f"āļø Wrote {stats['reviews_written']} review{'s' if stats['reviews_written'] > 1 else ''}") + if stats["ratings_given"] > 0: + activity_items.append(f"ā Gave {stats['ratings_given']} rating{'s' if stats['ratings_given'] > 1 else ''}") + if stats["chase_caught"] > 0: + activity_items.append(f"šÆ Caught {stats['chase_caught']} chase song{'s' if stats['chase_caught'] > 1 else ''}!") + + activity_html = "" + if activity_items: + activity_html = "
No activity this week. Get out there and catch some shows!
" + + html_content = f""" + + +You're receiving this because you enabled weekly digests. Manage preferences
+