From 839fafdb33e16a810bed28d145cca8ff332e727d Mon Sep 17 00:00:00 2001 From: jschaufuss Date: Wed, 13 Aug 2025 21:13:25 +0200 Subject: [PATCH] fix duplicated email --- arr_api/notifications.py | 118 +++++++++++------- .../0004_alter_appsettings_mail_secure.py | 18 +++ subscribarr/settings.py | 3 +- 3 files changed, 94 insertions(+), 45 deletions(-) create mode 100644 settingspanel/migrations/0004_alter_appsettings_mail_secure.py diff --git a/arr_api/notifications.py b/arr_api/notifications.py index 45ff7ce..9fa7a47 100644 --- a/arr_api/notifications.py +++ b/arr_api/notifications.py @@ -2,6 +2,7 @@ from django.core.mail import send_mail from django.conf import settings from django.template.loader import render_to_string from django.utils import timezone +from django.db import transaction from settingspanel.models import AppSettings # from accounts.utils import JellyfinClient # not needed for availability; use Sonarr/Radarr instead import requests @@ -254,22 +255,32 @@ def check_and_notify_users(): if season is None or number is None: continue - # duplicate guard (per series per day per user) - if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): - already_notified = SentNotification.objects.filter( - media_id=sub.series_id, - media_type='series', - air_date=today, - user=sub.user - ).exists() - if already_notified: - continue - # check availability via Sonarr hasFile if sonarr_episode_has_file(sub.series_id, season, number): if not sub.user.email: continue - send_notification_email( + # After confirming availability, reserve once per user/series/day + if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): + try: + with transaction.atomic(): + obj, created = SentNotification.objects.get_or_create( + user=sub.user, + media_id=sub.series_id, + media_type='series', + air_date=today, + defaults={ + 'media_title': sub.series_title, + } + ) + if not created: + # already reserved/sent + continue + except Exception: + # if DB error (race), skip to avoid duplicates + continue + + try: + send_notification_email( user=sub.user, media_title=sub.series_title, media_type='series', @@ -279,16 +290,21 @@ def check_and_notify_users(): season=season, episode=number, air_date=ep.get('airDateUtc'), - ) - # mark as sent unless duplicates are allowed - if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): - SentNotification.objects.create( - user=sub.user, - media_id=sub.series_id, - media_type='series', - media_title=sub.series_title, - air_date=today ) + except Exception: + # roll back reservation so we can retry next run + if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): + try: + SentNotification.objects.filter( + user=sub.user, + media_id=sub.series_id, + media_type='series', + air_date=today, + ).delete() + except Exception: + pass + continue + # no-op: already reserved via get_or_create above # Film-Abos for sub in MovieSubscription.objects.select_related('user').all(): @@ -296,16 +312,6 @@ def check_and_notify_users(): if not it: continue - if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): - already_notified = SentNotification.objects.filter( - media_id=sub.movie_id, - media_type='movie', - air_date=today, - user=sub.user - ).exists() - if already_notified: - continue - if radarr_movie_has_file(sub.movie_id): if not sub.user.email: continue @@ -323,23 +329,47 @@ def check_and_notify_users(): except Exception: pass - send_notification_email( - user=sub.user, - media_title=sub.title, - media_type='movie', - overview=sub.overview, - poster_url=it.get('posterUrl'), - year=it.get('year'), - release_type=rel, - ) + # After confirming availability, reserve once per user/movie/day if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): - SentNotification.objects.create( + try: + with transaction.atomic(): + obj, created = SentNotification.objects.get_or_create( + user=sub.user, + media_id=sub.movie_id, + media_type='movie', + air_date=today, + defaults={ + 'media_title': sub.title, + } + ) + if not created: + continue + except Exception: + continue + + try: + send_notification_email( user=sub.user, - media_id=sub.movie_id, - media_type='movie', media_title=sub.title, - air_date=today + media_type='movie', + overview=sub.overview, + poster_url=it.get('posterUrl'), + year=it.get('year'), + release_type=rel, ) + except Exception: + if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): + try: + SentNotification.objects.filter( + user=sub.user, + media_id=sub.movie_id, + media_type='movie', + air_date=today, + ).delete() + except Exception: + pass + continue + # no-op: already reserved via get_or_create above def has_new_episode_today(series_id): diff --git a/settingspanel/migrations/0004_alter_appsettings_mail_secure.py b/settingspanel/migrations/0004_alter_appsettings_mail_secure.py new file mode 100644 index 0000000..e8c3a5e --- /dev/null +++ b/settingspanel/migrations/0004_alter_appsettings_mail_secure.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.5 on 2025-08-13 19:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('settingspanel', '0003_alter_appsettings_mail_secure'), + ] + + operations = [ + migrations.AlterField( + model_name='appsettings', + name='mail_secure', + field=models.CharField(blank=True, choices=[('', 'No TLS/SSL'), ('starttls', 'STARTTLS (Port 587)'), ('ssl', 'SSL/TLS (Port 465)'), ('tls', 'TLS (alias STARTTLS)')], max_length=10, null=True), + ), + ] diff --git a/subscribarr/settings.py b/subscribarr/settings.py index 5761348..b962675 100644 --- a/subscribarr/settings.py +++ b/subscribarr/settings.py @@ -183,4 +183,5 @@ DEFAULT_FROM_EMAIL = None # Will be set from AppSettings # Notifications / Debug # If True, duplicate suppression is disabled and emails can be resent on every run. -NOTIFICATIONS_ALLOW_DUPLICATES = os.getenv('NOTIFICATIONS_ALLOW_DUPLICATES', 'True').lower() == 'true' +# Default is False to avoid accidental duplicate mails in local/dev runs. +NOTIFICATIONS_ALLOW_DUPLICATES = os.getenv('NOTIFICATIONS_ALLOW_DUPLICATES', 'False').lower() == 'true'