multiuser/subscriptions/notifications
This commit is contained in:
		| @@ -2,6 +2,58 @@ from django import forms | ||||
|  | ||||
| WIDE = {"class": "input-wide"} | ||||
|  | ||||
| class FirstRunSetupForm(forms.Form): | ||||
|     # Jellyfin (Required) | ||||
|     jellyfin_server_url = forms.URLField( | ||||
|         label="Jellyfin Server URL", | ||||
|         required=True, | ||||
|         help_text="Die URL deines Jellyfin-Servers" | ||||
|     ) | ||||
|     jellyfin_api_key = forms.CharField( | ||||
|         label="Jellyfin API Key", | ||||
|         required=True, | ||||
|         widget=forms.PasswordInput(render_value=True), | ||||
|         help_text="Der API-Key aus den Jellyfin-Einstellungen" | ||||
|     ) | ||||
|      | ||||
|     # Sonarr (Optional) | ||||
|     sonarr_url = forms.URLField( | ||||
|         label="Sonarr URL", | ||||
|         required=False, | ||||
|         help_text="Die URL deines Sonarr-Servers" | ||||
|     ) | ||||
|     sonarr_api_key = forms.CharField( | ||||
|         label="Sonarr API Key", | ||||
|         required=False, | ||||
|         widget=forms.PasswordInput(render_value=True) | ||||
|     ) | ||||
|      | ||||
|     # Radarr (Optional) | ||||
|     radarr_url = forms.URLField( | ||||
|         label="Radarr URL", | ||||
|         required=False, | ||||
|         help_text="Die URL deines Radarr-Servers" | ||||
|     ) | ||||
|     radarr_api_key = forms.CharField( | ||||
|         label="Radarr API Key", | ||||
|         required=False, | ||||
|         widget=forms.PasswordInput(render_value=True) | ||||
|     ) | ||||
|  | ||||
| class JellyfinSettingsForm(forms.Form): | ||||
|     jellyfin_server_url = forms.URLField( | ||||
|         label="Jellyfin Server URL", | ||||
|         required=False, | ||||
|         widget=forms.URLInput(attrs=WIDE), | ||||
|         help_text="z.B. http://localhost:8096" | ||||
|     ) | ||||
|     jellyfin_api_key = forms.CharField( | ||||
|         label="Jellyfin API Key", | ||||
|         required=False, | ||||
|         widget=forms.PasswordInput(render_value=True, attrs=WIDE), | ||||
|         help_text="Admin API Key aus den Jellyfin Einstellungen" | ||||
|     ) | ||||
|  | ||||
| class ArrSettingsForm(forms.Form): | ||||
|     sonarr_url     = forms.URLField(label="Sonarr URL", required=False, | ||||
|                                     widget=forms.URLInput(attrs=WIDE)) | ||||
|   | ||||
							
								
								
									
										23
									
								
								settingspanel/middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								settingspanel/middleware.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| from django.shortcuts import redirect | ||||
| from django.urls import reverse | ||||
| from django.conf import settings | ||||
| from .views import needs_setup | ||||
|  | ||||
| class SetupMiddleware: | ||||
|     def __init__(self, get_response): | ||||
|         self.get_response = get_response | ||||
|  | ||||
|     def __call__(self, request): | ||||
|         if needs_setup(): | ||||
|             # URLs, die auch ohne Setup erlaubt sind | ||||
|             allowed_urls = [ | ||||
|                 reverse('settingspanel:setup'), | ||||
|                 '/static/',  # Für CSS/JS | ||||
|             ] | ||||
|              | ||||
|             # Prüfe, ob die aktuelle URL erlaubt ist | ||||
|             if not any(request.path.startswith(url) for url in allowed_urls): | ||||
|                 return redirect('settingspanel:setup') | ||||
|          | ||||
|         response = self.get_response(request) | ||||
|         return response | ||||
| @@ -0,0 +1,23 @@ | ||||
| # Generated by Django 5.2.5 on 2025-08-10 13:15 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('settingspanel', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='appsettings', | ||||
|             name='jellyfin_api_key', | ||||
|             field=models.CharField(blank=True, max_length=255, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='appsettings', | ||||
|             name='jellyfin_server_url', | ||||
|             field=models.URLField(blank=True, null=True), | ||||
|         ), | ||||
|     ] | ||||
| @@ -4,6 +4,10 @@ class AppSettings(models.Model): | ||||
|     # Singleton-Pattern über feste ID | ||||
|     singleton_id = models.PositiveSmallIntegerField(default=1, unique=True, editable=False) | ||||
|  | ||||
|     # Jellyfin | ||||
|     jellyfin_server_url = models.URLField(blank=True, null=True) | ||||
|     jellyfin_api_key = models.CharField(max_length=255, blank=True, null=True) | ||||
|  | ||||
|     # Arr | ||||
|     sonarr_url = models.URLField(blank=True, null=True) | ||||
|     sonarr_api_key = models.CharField(max_length=255, blank=True, null=True) | ||||
| @@ -15,7 +19,12 @@ class AppSettings(models.Model): | ||||
|     mail_port = models.PositiveIntegerField(blank=True, null=True) | ||||
|     mail_secure = models.CharField( | ||||
|         max_length=10, blank=True, null=True, | ||||
|         choices=(("", "Kein TLS/SSL"), ("starttls", "STARTTLS"), ("ssl", "SSL/TLS")) | ||||
|         choices=( | ||||
|             ("", "Kein TLS/SSL"), | ||||
|             ("starttls", "STARTTLS (Port 587)"), | ||||
|             ("ssl", "SSL/TLS (Port 465)"), | ||||
|             ("tls", "TLS (alias STARTTLS)"), | ||||
|         ) | ||||
|     ) | ||||
|     mail_user = models.CharField(max_length=255, blank=True, null=True) | ||||
|     mail_password = models.CharField(max_length=255, blank=True, null=True) | ||||
| @@ -32,5 +41,15 @@ class AppSettings(models.Model): | ||||
|  | ||||
|     @classmethod | ||||
|     def current(cls): | ||||
|         """Get the current settings instance or create a new one""" | ||||
|         obj, _ = cls.objects.get_or_create(singleton_id=1) | ||||
|         return obj | ||||
|          | ||||
|     def get_jellyfin_url(self): | ||||
|         """Get the Jellyfin server URL with proper formatting""" | ||||
|         if not self.jellyfin_server_url: | ||||
|             return None | ||||
|         url = self.jellyfin_server_url | ||||
|         if not url.startswith(('http://', 'https://')): | ||||
|             url = f'http://{url}' | ||||
|         return url.rstrip('/') | ||||
|   | ||||
							
								
								
									
										57
									
								
								settingspanel/templates/settingspanel/first_run.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								settingspanel/templates/settingspanel/first_run.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| {% extends "base.html" %} | ||||
| {% load static %} | ||||
|  | ||||
| {% block extra_style %} | ||||
| <link rel="stylesheet" href="{% static 'css/setup.css' %}"> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="setup-container"> | ||||
|     <h1>Willkommen bei Subscribarr</h1> | ||||
|     <p class="setup-intro">Lass uns deine Installation einrichten. Du brauchst mindestens einen Jellyfin-Server.</p> | ||||
|  | ||||
|     <form method="post" class="setup-form"> | ||||
|         {% csrf_token %} | ||||
|  | ||||
|         <div class="setup-section"> | ||||
|             <h2>Jellyfin Server (Erforderlich)</h2> | ||||
|             <div class="form-group"> | ||||
|                 <label>Server URL</label> | ||||
|                 {{ form.jellyfin_server_url }} | ||||
|                 <div class="help">z.B. http://192.168.1.100:8096 oder http://jellyfin.local:8096</div> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label>API Key</label> | ||||
|                 {{ form.jellyfin_api_key }} | ||||
|                 <div class="help">Admin API Key aus den Jellyfin-Einstellungen</div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="setup-section"> | ||||
|             <h2>Sonarr (Optional)</h2> | ||||
|             <div class="form-group"> | ||||
|                 <label>Server URL</label> | ||||
|                 {{ form.sonarr_url }} | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label>API Key</label> | ||||
|                 {{ form.sonarr_api_key }} | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="setup-section"> | ||||
|             <h2>Radarr (Optional)</h2> | ||||
|             <div class="form-group"> | ||||
|                 <label>Server URL</label> | ||||
|                 {{ form.radarr_url }} | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label>API Key</label> | ||||
|                 {{ form.radarr_api_key }} | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <button type="submit" class="setup-submit">Installation abschließen</button> | ||||
|     </form> | ||||
| </div> | ||||
| {% endblock %} | ||||
| @@ -6,201 +6,7 @@ | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <title>Einstellungen – Subscribarr</title> | ||||
|     <style> | ||||
|         :root { | ||||
|             --bg: #0b0b10; | ||||
|             --panel: #12121a; | ||||
|             --panel-b: #1f2030; | ||||
|             --accent: #3b82f6; | ||||
|             --muted: #9aa0b4; | ||||
|             --text: #e6e6e6; | ||||
|         } | ||||
|  | ||||
|         * { | ||||
|             box-sizing: border-box | ||||
|         } | ||||
|  | ||||
|         body { | ||||
|             background: var(--bg); | ||||
|             color: var(--text); | ||||
|             font-family: system-ui, Segoe UI, Roboto, Arial, sans-serif; | ||||
|             margin: 0 | ||||
|         } | ||||
|  | ||||
|         .wrap { | ||||
|             max-width: 1000px; | ||||
|             margin: 0 auto; | ||||
|             padding: 16px | ||||
|         } | ||||
|  | ||||
|         a { | ||||
|             color: #cfd3ea; | ||||
|             text-decoration: none | ||||
|         } | ||||
|  | ||||
|         .topbar { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: space-between; | ||||
|             margin-bottom: 16px | ||||
|         } | ||||
|  | ||||
|         .btn { | ||||
|             padding: 10px 14px; | ||||
|             border-radius: 10px; | ||||
|             border: 1px solid #2a2a34; | ||||
|             background: #111119; | ||||
|             color: #fff; | ||||
|             cursor: pointer | ||||
|         } | ||||
|  | ||||
|         .btn-primary { | ||||
|             background: var(--accent); | ||||
|             border-color: transparent | ||||
|         } | ||||
|  | ||||
|         .grid { | ||||
|             display: grid; | ||||
|             gap: 16px | ||||
|         } | ||||
|  | ||||
|         @media(min-width:900px) { | ||||
|             .grid { | ||||
|                 grid-template-columns: 1fr 1fr | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         .card { | ||||
|             background: var(--panel); | ||||
|             border: 1px solid var(--panel-b); | ||||
|             border-radius: 12px; | ||||
|             padding: 14px | ||||
|         } | ||||
|  | ||||
|         .card h2 { | ||||
|             margin: 0 0 10px; | ||||
|             font-size: 1.05rem | ||||
|         } | ||||
|  | ||||
|         .row { | ||||
|             display: grid; | ||||
|             grid-template-columns: 160px minmax(0, 1fr); | ||||
|             /* <= statt 160px 1fr */ | ||||
|             gap: 10px; | ||||
|             align-items: center; | ||||
|             margin-bottom: 10px; | ||||
|         } | ||||
|  | ||||
|         .row label { | ||||
|             color: #c9cbe3 | ||||
|         } | ||||
|  | ||||
|         .row input, | ||||
|         .row select { | ||||
|             width: 100%; | ||||
|             padding: 10px 12px; | ||||
|             border-radius: 10px; | ||||
|             border: 1px solid #2a2a34; | ||||
|             background: #111119; | ||||
|             color: var(--text) | ||||
|         } | ||||
|  | ||||
|         .help { | ||||
|             color: var(--muted); | ||||
|             font-size: .9rem | ||||
|         } | ||||
|  | ||||
|         .msgs { | ||||
|             margin-bottom: 10px | ||||
|         } | ||||
|  | ||||
|         .msg { | ||||
|             background: #0f1425; | ||||
|             border: 1px solid #283058; | ||||
|             border-radius: 10px; | ||||
|             padding: 10px; | ||||
|             margin-bottom: 8px | ||||
|         } | ||||
|  | ||||
|         .input-wide { | ||||
|             width: 100% !important; | ||||
|             max-width: 100%; | ||||
|             min-width: 0; | ||||
|             display: block; | ||||
|         } | ||||
|  | ||||
|         .row input, | ||||
|         .row select, | ||||
|         .row textarea { | ||||
|             width: 100%; | ||||
|             max-width: 100%; | ||||
|             min-width: 0; | ||||
|             display: block; | ||||
|         } | ||||
|  | ||||
|         /* falls du passwort/URL Felder extra stylen willst, gleicher Fix */ | ||||
|         .inline>input, | ||||
|         .inline>.django-url, | ||||
|         /* falls Widget eine Klasse rendert */ | ||||
|         .inline>.django-password { | ||||
|             min-width: 0; | ||||
|             width: 100%; | ||||
|         } | ||||
|  | ||||
|         .inline-actions { | ||||
|             display: inline-flex; | ||||
|             align-items: center; | ||||
|             gap: 8px; | ||||
|             min-width: 220px; | ||||
|             justify-content: flex-end; | ||||
|         } | ||||
|  | ||||
|         .btn { | ||||
|             padding: 10px 14px; | ||||
|             border-radius: 10px; | ||||
|             border: 1px solid #2a2a34; | ||||
|             background: #111119; | ||||
|             color: #fff; | ||||
|             cursor: pointer; | ||||
|         } | ||||
|  | ||||
|         .btn:disabled { | ||||
|             opacity: .6; | ||||
|             cursor: default; | ||||
|         } | ||||
|  | ||||
|         .badge { | ||||
|             padding: 6px 10px; | ||||
|             border-radius: 999px; | ||||
|             font-size: .85rem; | ||||
|             border: 1px solid #2a2a34; | ||||
|             background: #111119; | ||||
|             color: #cfd3ea; | ||||
|             white-space: nowrap; | ||||
|             /* eine Zeile */ | ||||
|             max-width: 140px; | ||||
|             /* begrenzt die Breite */ | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|             /* falls doch Text drin ist */ | ||||
|         } | ||||
|  | ||||
|         .badge.ok { | ||||
|             border-color: #1f6f3a; | ||||
|             background: #10331f; | ||||
|             color: #a7e3bd; | ||||
|         } | ||||
|  | ||||
|         .badge.err { | ||||
|             border-color: #6f1f2a; | ||||
|             background: #341016; | ||||
|             color: #f1a3b0; | ||||
|         } | ||||
|  | ||||
|         .badge.muted { | ||||
|             opacity: .8; | ||||
|         } | ||||
|     </style> | ||||
|     <link rel="stylesheet" href="{% static 'css/settings.css' %}"> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
| @@ -218,6 +24,20 @@ | ||||
|         <form method="post"> | ||||
|             {% csrf_token %} | ||||
|             <div class="grid"> | ||||
|                 <div class="card"> | ||||
|                     <h2>Jellyfin</h2> | ||||
|                     <div class="row"> | ||||
|                         <label>Jellyfin Server URL</label> | ||||
|                         {{ jellyfin_form.jellyfin_server_url }} | ||||
|                         <div class="help">z.B. http://localhost:8096</div> | ||||
|                     </div> | ||||
|                     <div class="row"> | ||||
|                         <label>Jellyfin API Key</label> | ||||
|                         {{ jellyfin_form.jellyfin_api_key }} | ||||
|                         <div class="help">Admin API Key aus den Jellyfin Einstellungen</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="card"> | ||||
|                     <h2>Sonarr & Radarr</h2> | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| from django.urls import path | ||||
| from .views import SettingsView, test_connection | ||||
| from .views import SettingsView, test_connection, first_run | ||||
|  | ||||
| app_name = "settingspanel" | ||||
| urlpatterns = [ | ||||
|     path("", SettingsView.as_view(), name="index"), | ||||
|     path("test-connection/", test_connection, name="test_connection"), | ||||
|     path("setup/", first_run, name="setup"), | ||||
| ] | ||||
|   | ||||
| @@ -1,11 +1,53 @@ | ||||
| from django.views import View | ||||
| from django.shortcuts import render, redirect | ||||
| from django.views import View | ||||
| from django.contrib import messages | ||||
| from .forms import ArrSettingsForm, MailSettingsForm, AccountForm | ||||
| from django.utils.decorators import method_decorator | ||||
| from .forms import ArrSettingsForm, MailSettingsForm, AccountForm, FirstRunSetupForm, JellyfinSettingsForm | ||||
| from .models import AppSettings | ||||
| from django.http import JsonResponse | ||||
| from accounts.utils import jellyfin_admin_required | ||||
| from django.contrib.auth import get_user_model | ||||
| import requests | ||||
|  | ||||
| def needs_setup(): | ||||
|     """Check if the app needs first-run setup""" | ||||
|     settings = AppSettings.current() | ||||
|     return not bool(settings.jellyfin_server_url) | ||||
|  | ||||
| def first_run(request): | ||||
|     """Handle first-run setup""" | ||||
|     if not needs_setup(): | ||||
|         return redirect('arr_api:index') | ||||
|          | ||||
|     if request.method == 'POST': | ||||
|         form = FirstRunSetupForm(request.POST) | ||||
|         if form.is_valid(): | ||||
|             # Save settings | ||||
|             settings = AppSettings.current() | ||||
|             settings.jellyfin_server_url = form.cleaned_data['jellyfin_server_url'] | ||||
|             settings.jellyfin_api_key = form.cleaned_data['jellyfin_api_key'] | ||||
|             settings.sonarr_url = form.cleaned_data['sonarr_url'] | ||||
|             settings.sonarr_api_key = form.cleaned_data['sonarr_api_key'] | ||||
|             settings.radarr_url = form.cleaned_data['radarr_url'] | ||||
|             settings.radarr_api_key = form.cleaned_data['radarr_api_key'] | ||||
|             settings.save() | ||||
|              | ||||
|             messages.success(request, 'Setup erfolgreich abgeschlossen!') | ||||
|             return redirect('accounts:login') | ||||
|     else: | ||||
|         form = FirstRunSetupForm() | ||||
|      | ||||
|     return render(request, 'settingspanel/first_run.html', {'form': form}) | ||||
| from django.shortcuts import render, redirect | ||||
| from django.contrib import messages | ||||
| from django.utils.decorators import method_decorator | ||||
| from .forms import ArrSettingsForm, MailSettingsForm, AccountForm, JellyfinSettingsForm | ||||
| from .models import AppSettings | ||||
| from django.http import JsonResponse | ||||
| from accounts.utils import jellyfin_admin_required | ||||
| import requests | ||||
|  | ||||
| @jellyfin_admin_required | ||||
| def test_connection(request): | ||||
|     kind = request.GET.get("kind")  # "sonarr" | "radarr" | ||||
|     url = (request.GET.get("url") or "").strip() | ||||
| @@ -27,12 +69,17 @@ def test_connection(request): | ||||
|     except requests.RequestException as e: | ||||
|         return JsonResponse({"ok": False, "error": str(e)}) | ||||
|  | ||||
| @method_decorator(jellyfin_admin_required, name='dispatch') | ||||
| class SettingsView(View): | ||||
|     template_name = "settingspanel/settings.html" | ||||
|  | ||||
|     def get(self, request): | ||||
|         cfg = AppSettings.current() | ||||
|         return render(request, self.template_name, { | ||||
|             "jellyfin_form": JellyfinSettingsForm(initial={ | ||||
|                 "jellyfin_server_url": cfg.jellyfin_server_url or "", | ||||
|                 "jellyfin_api_key": cfg.jellyfin_api_key or "", | ||||
|             }), | ||||
|             "arr_form": ArrSettingsForm(initial={ | ||||
|                 "sonarr_url": cfg.sonarr_url or "", | ||||
|                 "sonarr_api_key": cfg.sonarr_api_key or "", | ||||
| @@ -54,15 +101,24 @@ class SettingsView(View): | ||||
|         }) | ||||
|  | ||||
|     def post(self, request): | ||||
|         arr_form  = ArrSettingsForm(request.POST) | ||||
|         jellyfin_form = JellyfinSettingsForm(request.POST) | ||||
|         arr_form = ArrSettingsForm(request.POST) | ||||
|         mail_form = MailSettingsForm(request.POST) | ||||
|         acc_form  = AccountForm(request.POST) | ||||
|         if not (arr_form.is_valid() and mail_form.is_valid() and acc_form.is_valid()): | ||||
|         acc_form = AccountForm(request.POST) | ||||
|          | ||||
|         if not (jellyfin_form.is_valid() and arr_form.is_valid() and mail_form.is_valid() and acc_form.is_valid()): | ||||
|             return render(request, self.template_name, { | ||||
|                 "arr_form": arr_form, "mail_form": mail_form, "account_form": acc_form | ||||
|                 "jellyfin_form": jellyfin_form, | ||||
|                 "arr_form": arr_form, | ||||
|                 "mail_form": mail_form, | ||||
|                 "account_form": acc_form | ||||
|             }) | ||||
|  | ||||
|         cfg = AppSettings.current() | ||||
|          | ||||
|         # Update Jellyfin settings | ||||
|         cfg.jellyfin_server_url = jellyfin_form.cleaned_data["jellyfin_server_url"] or None | ||||
|         cfg.jellyfin_api_key = jellyfin_form.cleaned_data["jellyfin_api_key"] or None | ||||
|         cfg.sonarr_url     = arr_form.cleaned_data["sonarr_url"] or None | ||||
|         cfg.sonarr_api_key = arr_form.cleaned_data["sonarr_api_key"] or None | ||||
|         cfg.radarr_url     = arr_form.cleaned_data["radarr_url"] or None | ||||
|   | ||||
		Reference in New Issue
	
	Block a user