from collections import defaultdict from django.shortcuts import render, redirect, get_object_or_404 from django.views import View from django.contrib import messages from django.views.decorators.http import require_POST from django.utils.decorators import method_decorator from django.contrib.auth.decorators import login_required from django.http import JsonResponse from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from settingspanel.models import AppSettings from .services import sonarr_calendar, radarr_calendar, ArrServiceError from .models import SeriesSubscription, MovieSubscription from django.utils import timezone def _get_int(request, key, default): try: v = int(request.GET.get(key, default)) return max(1, min(365, v)) except (TypeError, ValueError): return default def _arr_conf_from_db(): cfg = AppSettings.current() return { "sonarr_url": cfg.sonarr_url, "sonarr_key": cfg.sonarr_api_key, "radarr_url": cfg.radarr_url, "radarr_key": cfg.radarr_api_key, } #class SonarrAiringView(APIView): # def get(self, request): # days = _get_int(request, "days", 30) # conf = _arr_conf_from_db() # try: # data = sonarr_calendar(days=days, base_url=conf["sonarr_url"], api_key=conf["sonarr_key"]) # return Response({"count": len(data), "results": data}) # except ArrServiceError as e: # return Response({"error": str(e)}, status=status.HTTP_502_BAD_GATEWAY) #class RadarrUpcomingMoviesView(APIView): # def get(self, request): # days = _get_int(request, "days", 60) # conf = _arr_conf_from_db() # try: # data = radarr_calendar(days=days, base_url=conf["radarr_url"], api_key=conf["radarr_key"]) # return Response({"count": len(data), "results": data}) # except ArrServiceError as e: # return Response({"error": str(e)}, status=status.HTTP_502_BAD_GATEWAY) class ArrIndexView(View): def get(self, request): q = (request.GET.get("q") or "").lower().strip() kind = (request.GET.get("kind") or "all").lower() days = _get_int(request, "days", 30) conf = _arr_conf_from_db() eps, movies = [], [] # Sonarr robust laden try: eps = sonarr_calendar(days=days, base_url=conf["sonarr_url"], api_key=conf["sonarr_key"]) except ArrServiceError as e: messages.error(request, f"Sonarr is not reachable: {e}") # Radarr robust laden try: movies = radarr_calendar(days=days, base_url=conf["radarr_url"], api_key=conf["radarr_key"]) except ArrServiceError as e: messages.error(request, f"Radarr is not reachable: {e}") # Suche if q: 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)) # Gruppierung nach Serie groups = defaultdict(lambda: { "seriesId": None, "seriesTitle": None, "seriesPoster": None, "seriesOverview": "", "seriesGenres": [], "episodes": [], "is_subscribed": False, }) for e in eps: sid = e["seriesId"] g = groups[sid] g["seriesId"] = sid g["seriesTitle"] = e["seriesTitle"] g["seriesPoster"] = g["seriesPoster"] or e.get("seriesPoster") if not g["seriesOverview"] and e.get("seriesOverview"): g["seriesOverview"] = e["seriesOverview"] if not g["seriesGenres"] and e.get("seriesGenres"): g["seriesGenres"] = e["seriesGenres"] g["episodes"].append({ "episodeId": e["episodeId"], "seasonNumber": e["seasonNumber"], "episodeNumber": e["episodeNumber"], "title": e["title"], "airDateUtc": e["airDateUtc"], }) series_grouped = [] for g in groups.values(): g["episodes"].sort(key=lambda x: (x["airDateUtc"] or "")) g["is_subscribed"] = g["seriesId"] in subscribed_series_ids series_grouped.append(g) # Markiere abonnierte Filme for movie in movies: movie["is_subscribed"] = movie.get("movieId") in subscribed_movie_ids return render(request, "arr_api/index.html", { "query": q, "kind": kind, "days": days, "show_series": kind in ("all", "series"), "show_movies": kind in ("all", "movies"), "series_grouped": series_grouped, "movies": movies, }) class CalendarView(View): def get(self, request): days = _get_int(request, "days", 60) return render(request, "arr_api/calendar.html", {"days": days}) @method_decorator(login_required, name='dispatch') class CalendarEventsApi(APIView): def get(self, request): days = _get_int(request, "days", 60) conf = _arr_conf_from_db() try: eps = sonarr_calendar(days=days, base_url=conf["sonarr_url"], api_key=conf["sonarr_key"]) except ArrServiceError: eps = [] try: movies = radarr_calendar(days=days, base_url=conf["radarr_url"], api_key=conf["radarr_key"]) except ArrServiceError: movies = [] series_sub = set(SeriesSubscription.objects.filter(user=request.user).values_list('series_id', flat=True)) movie_sub_titles = set(MovieSubscription.objects.filter(user=request.user).values_list('title', flat=True)) events = [] for e in eps: when = e.get("airDateUtc") if not when: continue events.append({ "id": f"s:{e.get('seriesId')}:{e.get('episodeId')}", "title": f"{e.get('seriesTitle','')} — S{e.get('seasonNumber')}E{e.get('episodeNumber')}", "start": when, "allDay": False, "extendedProps": { "kind": "series", "seriesId": e.get('seriesId'), "seriesTitle": e.get('seriesTitle'), "seasonNumber": e.get('seasonNumber'), "episodeNumber": e.get('episodeNumber'), "episodeTitle": e.get('title'), "overview": e.get('seriesOverview') or "", "poster": e.get('seriesPoster') or "", "subscribed": int(e.get('seriesId') or 0) in series_sub, } }) for m in movies: when = m.get('digitalRelease') or m.get('physicalRelease') or m.get('inCinemas') if not when: continue events.append({ "id": f"m:{m.get('movieId') or m.get('title')}", "title": m.get('title') or "(movie)", "start": when, "allDay": True, "extendedProps": { "kind": "movie", "movieId": m.get('movieId'), "title": m.get('title'), "overview": m.get('overview') or "", "poster": m.get('posterUrl') or "", "subscribed": (m.get('title') or '') in movie_sub_titles, } }) 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 @login_required def subscribe_series(request, series_id): """Subscribe to a series""" try: # Existiert bereits? if SeriesSubscription.objects.filter(user=request.user, series_id=series_id).exists(): return JsonResponse({'success': True, 'already_subscribed': True}) # Hole Serien-Details vom Sonarr conf = _arr_conf_from_db() # TODO: Sonarr API Call für Series Details # Erstelle Subscription sub = SeriesSubscription.objects.create( user=request.user, series_id=series_id, series_title=request.POST.get('title', ''), series_poster=request.POST.get('poster', ''), series_overview=request.POST.get('overview', ''), series_genres=request.POST.getlist('genres[]', []) ) return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'error': str(e)}, status=400) @require_POST @login_required def unsubscribe_series(request, series_id): """Unsubscribe from a series""" try: SeriesSubscription.objects.filter(user=request.user, series_id=series_id).delete() return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'error': str(e)}, status=400) @login_required def is_subscribed_series(request, series_id): """Check if a series is subscribed""" is_subbed = SeriesSubscription.objects.filter(user=request.user, series_id=series_id).exists() return JsonResponse({'subscribed': is_subbed}) @require_POST @login_required def subscribe_movie(request, movie_id): """Subscribe to a movie""" try: # Existiert bereits? if MovieSubscription.objects.filter(user=request.user, movie_id=movie_id).exists(): return JsonResponse({'success': True, 'already_subscribed': True}) # Hole Film-Details vom Radarr conf = _arr_conf_from_db() # TODO: Radarr API Call für Movie Details # Erstelle Subscription sub = MovieSubscription.objects.create( user=request.user, 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') ) return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'error': str(e)}, status=400) @require_POST @login_required def unsubscribe_movie(request, movie_id): """Unsubscribe from a movie""" try: MovieSubscription.objects.filter(user=request.user, movie_id=movie_id).delete() return JsonResponse({'success': True}) except Exception as e: return JsonResponse({'error': str(e)}, status=400) @login_required def is_subscribed_movie(request, movie_id): """Check if a movie is subscribed""" is_subbed = MovieSubscription.objects.filter(user=request.user, movie_id=movie_id).exists() return JsonResponse({'subscribed': is_subbed}) @login_required def get_subscriptions(request): """Get all subscriptions for the user""" series = SeriesSubscription.objects.filter(user=request.user).values_list('series_id', flat=True) movies = MovieSubscription.objects.filter(user=request.user).values_list('movie_id', flat=True) return JsonResponse({ 'series': list(series), 'movies': list(movies) }) @method_decorator(login_required, name='dispatch') class SeriesSubscribeView(APIView): def post(self, request, series_id): from .services import sonarr_get_series cfg = AppSettings.current() details = None try: details = sonarr_get_series(series_id, base_url=cfg.sonarr_url, api_key=cfg.sonarr_api_key) except Exception: details = None defaults = { 'series_title': request.data.get('title', '') if request.data else '', 'series_poster': request.data.get('poster', '') if request.data else '', 'series_overview': request.data.get('overview', '') if request.data else '', 'series_genres': request.data.get('genres', []) if request.data else [], } if details: defaults.update({ 'series_title': details.get('series_title') or defaults['series_title'], 'series_poster': details.get('series_poster') or defaults['series_poster'], 'series_overview': details.get('series_overview') or defaults['series_overview'], 'series_genres': details.get('series_genres') or defaults['series_genres'], }) sub, created = SeriesSubscription.objects.get_or_create( user=request.user, series_id=series_id, defaults=defaults ) return Response({'status': 'subscribed'}, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK) @method_decorator(login_required, name='dispatch') class SeriesUnsubscribeView(APIView): def post(self, request, series_id): SeriesSubscription.objects.filter(user=request.user, series_id=series_id).delete() return Response({'status': 'unsubscribed'}, status=status.HTTP_200_OK) @method_decorator(login_required, name='dispatch') class MovieSubscribeView(APIView): def post(self, request, title): from .services import radarr_lookup_movie_by_title cfg = AppSettings.current() details = None try: details = radarr_lookup_movie_by_title(title, base_url=cfg.radarr_url, api_key=cfg.radarr_api_key) except Exception: details = None defaults = { 'movie_id': (request.data.get('movie_id', 0) if request.data else 0) or 0, 'poster': request.data.get('poster', '') if request.data else '', 'overview': request.data.get('overview', '') if request.data else '', 'genres': request.data.get('genres', []) if request.data else [], } if details: defaults.update({ 'movie_id': details.get('movie_id') or defaults['movie_id'], 'poster': details.get('poster') or defaults['poster'], 'overview': details.get('overview') or defaults['overview'], 'genres': details.get('genres') or defaults['genres'], }) sub, created = MovieSubscription.objects.get_or_create( user=request.user, title=title, defaults=defaults ) return Response({'status': 'subscribed'}, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK) @method_decorator(login_required, name='dispatch') class MovieUnsubscribeView(APIView): def post(self, request, title): MovieSubscription.objects.filter(user=request.user, title=title).delete() return Response({'status': 'unsubscribed'}, status=status.HTTP_200_OK) @method_decorator(login_required, name='dispatch') class ListSeriesSubscriptionsView(APIView): def get(self, request): subs = SeriesSubscription.objects.filter(user=request.user).values_list('series_id', flat=True) return Response(list(subs)) @method_decorator(login_required, name='dispatch') class ListMovieSubscriptionsView(APIView): def get(self, request): subs = MovieSubscription.objects.filter(user=request.user).values_list('title', flat=True) return Response(list(subs))