fix E-Mail notifications
This commit is contained in:
		
							
								
								
									
										44
									
								
								arr_api/management/commands/send_test_email.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								arr_api/management/commands/send_test_email.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.contrib.auth import get_user_model | ||||
| from django.utils import timezone | ||||
|  | ||||
| from arr_api.notifications import send_notification_email | ||||
|  | ||||
|  | ||||
| class Command(BaseCommand): | ||||
|     help = "Send a test notification email to verify SMTP configuration" | ||||
|  | ||||
|     def add_arguments(self, parser): | ||||
|         parser.add_argument('--to', required=True, help='Recipient email address') | ||||
|         parser.add_argument('--username', default='testuser', help='Username to associate with the email') | ||||
|         parser.add_argument('--type', default='movie', choices=['movie', 'series'], help='Media type for the template') | ||||
|         parser.add_argument('--title', default='Subscribarr Test', help='Title to show in the email') | ||||
|  | ||||
|     def handle(self, *args, **opts): | ||||
|         User = get_user_model() | ||||
|         email = opts['to'] | ||||
|         username = opts['username'] | ||||
|         media_type = opts['type'] | ||||
|         title = opts['title'] | ||||
|  | ||||
|         user, _ = User.objects.get_or_create(username=username, defaults={'email': email}) | ||||
|         if user.email != email: | ||||
|             user.email = email | ||||
|             user.save(update_fields=['email']) | ||||
|  | ||||
|         # Use current time as air_date for nicer formatting | ||||
|         send_notification_email( | ||||
|             user=user, | ||||
|             media_title=title, | ||||
|             media_type=media_type, | ||||
|             overview='This is a test email from Subscribarr to verify your mail settings.', | ||||
|             poster_url=None, | ||||
|             episode_title='Pilot' if media_type == 'series' else None, | ||||
|             season=1 if media_type == 'series' else None, | ||||
|             episode=1 if media_type == 'series' else None, | ||||
|             air_date=timezone.now(), | ||||
|             year=timezone.now().year if media_type == 'movie' else None, | ||||
|             release_type='Test' | ||||
|         ) | ||||
|  | ||||
|         self.stdout.write(self.style.SUCCESS(f"Test email queued/sent to {email}")) | ||||
| @@ -7,6 +7,7 @@ from settingspanel.models import AppSettings | ||||
| import requests | ||||
| from dateutil.parser import isoparse | ||||
| import logging | ||||
| from django.db import transaction | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -177,6 +178,8 @@ def _dispatch_user_notification(user, subject: str, body_text: str, html_message | ||||
|             return True | ||||
|         # fallback to email | ||||
|     try: | ||||
|         # Ensure email backend is configured from AppSettings at runtime | ||||
|         _set_runtime_email_settings() | ||||
|         send_mail( | ||||
|             subject=subject, | ||||
|             message=body_text, | ||||
| @@ -326,21 +329,10 @@ 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 | ||||
|             # duplicate guard will be handled atomically before dispatch | ||||
|  | ||||
|             # check availability via Sonarr hasFile | ||||
|             if sonarr_episode_has_file(sub.series_id, season, number): | ||||
|                 if not sub.user.email: | ||||
|                     continue | ||||
|                 # Build subject/body | ||||
|                 subj = f"New episode available: {sub.series_title} S{season:02d}E{number:02d}" | ||||
|                 body = f"{sub.series_title} S{season:02d}E{number:02d} is now available." | ||||
| @@ -361,36 +353,48 @@ def check_and_notify_users(): | ||||
|                     html = render_to_string('arr_api/email/new_media_notification.html', ctx) | ||||
|                 except Exception: | ||||
|                     pass | ||||
|                 ok = _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|                 # mark as sent unless duplicates are allowed | ||||
|                 if ok and not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                     SentNotification.objects.create( | ||||
|                 # Reserve duplicate token atomically, then dispatch; rollback on failure | ||||
|                 if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                     try: | ||||
|                         with transaction.atomic(): | ||||
|                             token, created = SentNotification.objects.get_or_create( | ||||
|                                 user=sub.user, | ||||
|                                 media_id=sub.series_id, | ||||
|                                 media_type='series', | ||||
|                         media_title=sub.series_title, | ||||
|                         air_date=today | ||||
|                                 air_date=today, | ||||
|                                 defaults={'media_title': sub.series_title} | ||||
|                             ) | ||||
|                         if not created: | ||||
|                             continue | ||||
|                     except Exception: | ||||
|                         # race or DB error -> skip to avoid duplicates | ||||
|                         continue | ||||
|                 ok = _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|                 if not ok and not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                     # allow retry on next run | ||||
|                     try: | ||||
|                         SentNotification.objects.filter( | ||||
|                             user=sub.user, | ||||
|                             media_id=sub.series_id, | ||||
|                             media_type='series', | ||||
|                             air_date=today | ||||
|                         ).delete() | ||||
|                     except Exception: | ||||
|                         pass | ||||
|  | ||||
|     # Film-Abos | ||||
|     for sub in MovieSubscription.objects.select_related('user').all(): | ||||
|         it = movie_idx.get(sub.movie_id) | ||||
|         # Fallback: if movie_id missing, try match by title | ||||
|         if not it and getattr(sub, 'title', None): | ||||
|             for _mid, _it in movie_idx.items(): | ||||
|                 if (_it.get('title') or '').strip().lower() == (sub.title or '').strip().lower(): | ||||
|                     it = _it | ||||
|                     break | ||||
|         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 | ||||
|             # detect which release matched today | ||||
|             rel = None | ||||
|             try: | ||||
| @@ -423,15 +427,32 @@ def check_and_notify_users(): | ||||
|                 html = render_to_string('arr_api/email/new_media_notification.html', ctx) | ||||
|             except Exception: | ||||
|                 pass | ||||
|             ok = _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|             if ok and not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                 SentNotification.objects.create( | ||||
|             # Reserve duplicate token atomically, then dispatch; rollback on failure | ||||
|             if not getattr(settings, 'NOTIFICATIONS_ALLOW_DUPLICATES', False): | ||||
|                 try: | ||||
|                     with transaction.atomic(): | ||||
|                         token, created = SentNotification.objects.get_or_create( | ||||
|                             user=sub.user, | ||||
|                             media_id=sub.movie_id, | ||||
|                             media_type='movie', | ||||
|                     media_title=sub.title, | ||||
|                     air_date=today | ||||
|                             air_date=today, | ||||
|                             defaults={'media_title': sub.title} | ||||
|                         ) | ||||
|                     if not created: | ||||
|                         continue | ||||
|                 except Exception: | ||||
|                     continue | ||||
|             ok = _dispatch_user_notification(sub.user, subject=subj, body_text=body, html_message=html) | ||||
|             if not ok and 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 | ||||
|  | ||||
|  | ||||
| def has_new_episode_today(series_id): | ||||
|   | ||||
| @@ -81,9 +81,13 @@ class ArrIndexView(View): | ||||
|             eps = [e for e in eps if q in (e.get("seriesTitle") or "").lower()] | ||||
|             movies = [m for m in movies if q in (m.get("title") or "").lower()] | ||||
|  | ||||
|         # Abonnierte Serien und Filme laden | ||||
|         subscribed_series_ids = set(SeriesSubscription.objects.values_list('series_id', flat=True)) | ||||
|         subscribed_movie_ids = set(MovieSubscription.objects.values_list('movie_id', flat=True)) | ||||
|         # Abonnierte Serien und Filme pro aktuellem Nutzer | ||||
|         if request.user.is_authenticated: | ||||
|             subscribed_series_ids = set(SeriesSubscription.objects.filter(user=request.user).values_list('series_id', flat=True)) | ||||
|             subscribed_movie_ids = set(MovieSubscription.objects.filter(user=request.user).values_list('movie_id', flat=True)) | ||||
|         else: | ||||
|             subscribed_series_ids = set() | ||||
|             subscribed_movie_ids = set() | ||||
|  | ||||
|         # Gruppierung nach Serie | ||||
|         groups = defaultdict(lambda: { | ||||
| @@ -197,70 +201,7 @@ class CalendarEventsApi(APIView): | ||||
|         return Response({"events": events}) | ||||
|  | ||||
|  | ||||
| class SubscribeSeriesView(View): | ||||
|     @method_decorator(require_POST) | ||||
|     def post(self, request, series_id): | ||||
|         series_data = { | ||||
|             'series_id': series_id, | ||||
|             'series_title': request.POST.get('series_title'), | ||||
|             'series_poster': request.POST.get('series_poster'), | ||||
|             'series_overview': request.POST.get('series_overview'), | ||||
|             'series_genres': request.POST.getlist('series_genres[]', []) | ||||
|         } | ||||
|   | ||||
|         subscription, created = SeriesSubscription.objects.get_or_create( | ||||
|             series_id=series_id, | ||||
|             defaults=series_data | ||||
|         ) | ||||
|          | ||||
|         if created: | ||||
|             messages.success(request, f'Subscribed to series "{series_data["series_title"]}"!') | ||||
|         else: | ||||
|             messages.info(request, f'Series "{series_data["series_title"]}" was already subscribed.') | ||||
|              | ||||
|         return redirect('arr_api:index') | ||||
|  | ||||
| class UnsubscribeSeriesView(View): | ||||
|     @method_decorator(require_POST) | ||||
|     def post(self, request, series_id): | ||||
|         subscription = get_object_or_404(SeriesSubscription, series_id=series_id) | ||||
|         series_title = subscription.series_title | ||||
|         subscription.delete() | ||||
|         messages.success(request, f'Subscription for "{series_title}" has been removed.') | ||||
|         return redirect('arr_api:index') | ||||
|  | ||||
| class SubscribeMovieView(View): | ||||
|     @method_decorator(require_POST) | ||||
|     def post(self, request, movie_id): | ||||
|         movie_data = { | ||||
|             'movie_id': movie_id, | ||||
|             'title': request.POST.get('title'), | ||||
|             'poster': request.POST.get('poster'), | ||||
|             'overview': request.POST.get('overview'), | ||||
|             'genres': request.POST.getlist('genres[]', []), | ||||
|             'release_date': request.POST.get('release_date') | ||||
|         } | ||||
|          | ||||
|         subscription, created = MovieSubscription.objects.get_or_create( | ||||
|             movie_id=movie_id, | ||||
|             defaults=movie_data | ||||
|         ) | ||||
|          | ||||
|         if created: | ||||
|             messages.success(request, f'Subscribed to movie "{movie_data["title"]}"!') | ||||
|         else: | ||||
|             messages.info(request, f'Movie "{movie_data["title"]}" was already subscribed.') | ||||
|              | ||||
|         return redirect('arr_api:index') | ||||
|  | ||||
| class UnsubscribeMovieView(View): | ||||
|     @method_decorator(require_POST) | ||||
|     def post(self, request, movie_id): | ||||
|         subscription = get_object_or_404(MovieSubscription, movie_id=movie_id) | ||||
|         movie_title = subscription.title | ||||
|         subscription.delete() | ||||
|         messages.success(request, f'Subscription for "{movie_title}" has been removed.') | ||||
|         return redirect('arr_api:index') | ||||
|  | ||||
|  | ||||
| @require_POST | ||||
|   | ||||
		Reference in New Issue
	
	Block a user