387 lines
15 KiB
Python
387 lines
15 KiB
Python
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 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: {
|
|
"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})
|
|
|
|
|
|
|
|
|
|
|
|
@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)) |