multiuser/subscriptions/notifications

This commit is contained in:
2025-08-10 17:48:15 +02:00
parent d4b811dbad
commit fb0c7da252
49 changed files with 3676 additions and 1034 deletions

View File

@@ -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))

View 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

View File

@@ -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),
),
]

View File

@@ -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('/')

View 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 %}

View File

@@ -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>

View File

@@ -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"),
]

View File

@@ -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