usermanagement/translation/calendar
This commit is contained in:
@@ -7,20 +7,20 @@ class FirstRunSetupForm(forms.Form):
|
||||
jellyfin_server_url = forms.URLField(
|
||||
label="Jellyfin Server URL",
|
||||
required=True,
|
||||
help_text="Die URL deines Jellyfin-Servers"
|
||||
help_text="URL of your Jellyfin server"
|
||||
)
|
||||
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"
|
||||
help_text="API key from Jellyfin settings"
|
||||
)
|
||||
|
||||
# Sonarr (Optional)
|
||||
sonarr_url = forms.URLField(
|
||||
label="Sonarr URL",
|
||||
required=False,
|
||||
help_text="Die URL deines Sonarr-Servers"
|
||||
help_text="URL of your Sonarr server"
|
||||
)
|
||||
sonarr_api_key = forms.CharField(
|
||||
label="Sonarr API Key",
|
||||
@@ -32,7 +32,7 @@ class FirstRunSetupForm(forms.Form):
|
||||
radarr_url = forms.URLField(
|
||||
label="Radarr URL",
|
||||
required=False,
|
||||
help_text="Die URL deines Radarr-Servers"
|
||||
help_text="URL of your Radarr server"
|
||||
)
|
||||
radarr_api_key = forms.CharField(
|
||||
label="Radarr API Key",
|
||||
@@ -45,13 +45,13 @@ class JellyfinSettingsForm(forms.Form):
|
||||
label="Jellyfin Server URL",
|
||||
required=False,
|
||||
widget=forms.URLInput(attrs=WIDE),
|
||||
help_text="z.B. http://localhost:8096"
|
||||
help_text="e.g. 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"
|
||||
help_text="Admin API key from Jellyfin settings"
|
||||
)
|
||||
|
||||
class ArrSettingsForm(forms.Form):
|
||||
@@ -68,18 +68,18 @@ class MailSettingsForm(forms.Form):
|
||||
mail_host = forms.CharField(label="Mail Host", required=False)
|
||||
mail_port = forms.IntegerField(label="Mail Port", required=False, min_value=1, max_value=65535)
|
||||
mail_secure = forms.ChoiceField(
|
||||
label="Sicherheit", required=False,
|
||||
choices=[("", "Kein TLS/SSL"), ("starttls", "STARTTLS"), ("ssl", "SSL/TLS")]
|
||||
label="Security", required=False,
|
||||
choices=[("", "No TLS/SSL"), ("starttls", "STARTTLS"), ("ssl", "SSL/TLS")]
|
||||
)
|
||||
mail_user = forms.CharField(label="Mail Benutzer", required=False)
|
||||
mail_user = forms.CharField(label="Mail Username", required=False)
|
||||
mail_password = forms.CharField(
|
||||
label="Mail Passwort", required=False,
|
||||
label="Mail Password", required=False,
|
||||
widget=forms.PasswordInput(render_value=True)
|
||||
)
|
||||
mail_from = forms.EmailField(label="Absender (From)", required=False)
|
||||
mail_from = forms.EmailField(label="Sender (From)", required=False)
|
||||
|
||||
class AccountForm(forms.Form):
|
||||
username = forms.CharField(label="Benutzername", required=False)
|
||||
email = forms.EmailField(label="E-Mail", required=False)
|
||||
new_password = forms.CharField(label="Neues Passwort", required=False, widget=forms.PasswordInput)
|
||||
repeat_password = forms.CharField(label="Passwort wiederholen", required=False, widget=forms.PasswordInput)
|
||||
username = forms.CharField(label="Username", required=False)
|
||||
email = forms.EmailField(label="Email", required=False)
|
||||
new_password = forms.CharField(label="New password", required=False, widget=forms.PasswordInput)
|
||||
repeat_password = forms.CharField(label="Repeat password", required=False, widget=forms.PasswordInput)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from django.db import models
|
||||
|
||||
class AppSettings(models.Model):
|
||||
# Singleton-Pattern über feste ID
|
||||
# Singleton pattern via fixed ID
|
||||
singleton_id = models.PositiveSmallIntegerField(default=1, unique=True, editable=False)
|
||||
|
||||
# Jellyfin
|
||||
@@ -20,7 +20,7 @@ class AppSettings(models.Model):
|
||||
mail_secure = models.CharField(
|
||||
max_length=10, blank=True, null=True,
|
||||
choices=(
|
||||
("", "Kein TLS/SSL"),
|
||||
("", "No TLS/SSL"),
|
||||
("starttls", "STARTTLS (Port 587)"),
|
||||
("ssl", "SSL/TLS (Port 465)"),
|
||||
("tls", "TLS (alias STARTTLS)"),
|
||||
@@ -30,7 +30,7 @@ class AppSettings(models.Model):
|
||||
mail_password = models.CharField(max_length=255, blank=True, null=True)
|
||||
mail_from = models.EmailField(blank=True, null=True)
|
||||
|
||||
# „Account“
|
||||
# Account
|
||||
acc_username = models.CharField(max_length=150, blank=True, null=True)
|
||||
acc_email = models.EmailField(blank=True, null=True)
|
||||
|
||||
|
@@ -7,28 +7,28 @@
|
||||
|
||||
{% 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>
|
||||
<h1>Welcome to Subscribarr</h1>
|
||||
<p class="setup-intro">Let's set up your installation. You need at least one Jellyfin server.</p>
|
||||
|
||||
<form method="post" class="setup-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Jellyfin Server (Erforderlich)</h2>
|
||||
<h2>Jellyfin server (required)</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 class="help">e.g., http://192.168.1.100:8096 or 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 class="help">Admin API key from Jellyfin settings</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Sonarr (Optional)</h2>
|
||||
<h2>Sonarr (optional)</h2>
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
{{ form.sonarr_url }}
|
||||
@@ -40,7 +40,7 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<h2>Radarr (Optional)</h2>
|
||||
<h2>Radarr (optional)</h2>
|
||||
<div class="form-group">
|
||||
<label>Server URL</label>
|
||||
{{ form.radarr_url }}
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="setup-submit">Installation abschließen</button>
|
||||
<button type="submit" class="setup-submit">Finish setup</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -1,19 +1,22 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Einstellungen – Subscribarr</title>
|
||||
<title>Settings – Subscribarr</title>
|
||||
<link rel="stylesheet" href="{% static 'css/settings.css' %}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="topbar">
|
||||
<div><a href="/" class="btn">← Zurück</a></div>
|
||||
<div><strong>Einstellungen</strong></div>
|
||||
<div style="display:flex; gap:8px; align-items:center;">
|
||||
<a href="/" class="btn">← Back</a>
|
||||
<a href="{% url 'settingspanel:subscriptions' %}" class="btn">👥 Subscriptions</a>
|
||||
</div>
|
||||
<div><strong>Settings</strong></div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
@@ -29,12 +32,12 @@
|
||||
<div class="row">
|
||||
<label>Jellyfin Server URL</label>
|
||||
{{ jellyfin_form.jellyfin_server_url }}
|
||||
<div class="help">z.B. http://localhost:8096</div>
|
||||
<div class="help">e.g., 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 class="help">Admin API key from your Jellyfin settings</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,8 +49,7 @@
|
||||
<div class="inline">
|
||||
<div class="field">{{ arr_form.sonarr_url }}</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn" type="button" onclick="testConnection('sonarr', this)">Test
|
||||
Sonarr</button>
|
||||
<button class="btn" type="button" onclick="testConnection('sonarr', this)">Test Sonarr</button>
|
||||
<span id="sonarrStatus" class="badge muted">—</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,8 +66,7 @@
|
||||
<div class="inline">
|
||||
<div class="field">{{ arr_form.radarr_url }}</div>
|
||||
<div class="inline-actions">
|
||||
<button class="btn" type="button" onclick="testConnection('radarr', this)">Test
|
||||
Radarr</button>
|
||||
<button class="btn" type="button" onclick="testConnection('radarr', this)">Test Radarr</button>
|
||||
<span id="radarrStatus" class="badge muted">—</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,32 +77,31 @@
|
||||
{{ arr_form.radarr_api_key }}
|
||||
</div>
|
||||
|
||||
<div class="help">Klicke „Test …“, um die Verbindung gegen <code>/api/v3/system/status</code> zu
|
||||
prüfen.</div>
|
||||
<div class="help">Click “Test …” to verify against <code>/api/v3/system/status</code>.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Mailserver</h2>
|
||||
<h2>Mail server</h2>
|
||||
<div class="row"><label>Host</label>{{ mail_form.mail_host }}</div>
|
||||
<div class="row"><label>Port</label>{{ mail_form.mail_port }}</div>
|
||||
<div class="row"><label>Sicherheit</label>{{ mail_form.mail_secure }}</div>
|
||||
<div class="row"><label>Benutzer</label>{{ mail_form.mail_user }}</div>
|
||||
<div class="row"><label>Passwort</label>{{ mail_form.mail_password }}</div>
|
||||
<div class="row"><label>Absender</label>{{ mail_form.mail_from }}</div>
|
||||
<div class="row"><label>Security</label>{{ mail_form.mail_secure }}</div>
|
||||
<div class="row"><label>User</label>{{ mail_form.mail_user }}</div>
|
||||
<div class="row"><label>Password</label>{{ mail_form.mail_password }}</div>
|
||||
<div class="row"><label>From</label>{{ mail_form.mail_from }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Konto</h2>
|
||||
<div class="row"><label>Benutzername</label>{{ account_form.username }}</div>
|
||||
<div class="row"><label>E-Mail</label>{{ account_form.email }}</div>
|
||||
<div class="row"><label>Neues Passwort</label>{{ account_form.new_password }}</div>
|
||||
<div class="row"><label>Passwort wiederholen</label>{{ account_form.repeat_password }}</div>
|
||||
<div class="help">Nur Oberfläche – Umsetzung Passwortänderung später.</div>
|
||||
<h2>Account</h2>
|
||||
<div class="row"><label>Username</label>{{ account_form.username }}</div>
|
||||
<div class="row"><label>Email</label>{{ account_form.email }}</div>
|
||||
<div class="row"><label>New password</label>{{ account_form.new_password }}</div>
|
||||
<div class="row"><label>Repeat password</label>{{ account_form.repeat_password }}</div>
|
||||
<div class="help">Only UI – implementing password change later.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:16px">
|
||||
<button class="btn btn-primary" type="submit">Speichern</button>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -116,25 +116,25 @@
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
alert(kind + " Verbindung erfolgreich!");
|
||||
alert(kind + " connection successful!");
|
||||
} else {
|
||||
alert(kind + " Fehler: " + data.error);
|
||||
alert(kind + " error: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(err => alert(kind + " Fehler: " + err));
|
||||
.catch(err => alert(kind + " error: " + err));
|
||||
}
|
||||
|
||||
function setBadge(kind, state, text, tooltip) {
|
||||
const el = document.getElementById(kind + "Status");
|
||||
if (!el) return;
|
||||
el.classList.remove("ok", "err", "muted");
|
||||
el.title = tooltip || ""; // voller Fehlertext im Tooltip
|
||||
el.title = tooltip || ""; // full error text in tooltip
|
||||
if (state === "ok") {
|
||||
el.classList.add("ok");
|
||||
el.textContent = "Verbunden";
|
||||
el.textContent = "Connected";
|
||||
} else if (state === "err") {
|
||||
el.classList.add("err");
|
||||
el.textContent = "Fehler";
|
||||
el.textContent = "Error";
|
||||
} else {
|
||||
el.classList.add("muted");
|
||||
el.textContent = "—";
|
||||
@@ -147,19 +147,19 @@
|
||||
const url = urlEl ? urlEl.value.trim() : "";
|
||||
const key = keyEl ? keyEl.value.trim() : "";
|
||||
|
||||
setBadge(kind, "muted", "Teste…");
|
||||
setBadge(kind, "muted", "Testing…");
|
||||
if (btnEl) { btnEl.disabled = true; }
|
||||
|
||||
fetch(`/settings/test-connection/?kind=${encodeURIComponent(kind)}&url=${encodeURIComponent(url)}&key=${encodeURIComponent(key)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.ok) {
|
||||
setBadge(kind, "ok", "Verbunden", "");
|
||||
setBadge(kind, "ok", "Connected", "");
|
||||
} else {
|
||||
setBadge(kind, "err", "Fehler", data.error || "Unbekannter Fehler");
|
||||
setBadge(kind, "err", "Error", data.error || "Unknown error");
|
||||
}
|
||||
})
|
||||
.catch(err => setBadge(kind, "err", "Fehler", String(err)))
|
||||
.catch(err => setBadge(kind, "err", "Error", String(err)))
|
||||
.finally(() => { if (btnEl) btnEl.disabled = false; });
|
||||
}
|
||||
|
||||
|
218
settingspanel/templates/settingspanel/subscriptions.html
Normal file
218
settingspanel/templates/settingspanel/subscriptions.html
Normal file
@@ -0,0 +1,218 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Subscriptions – Admin{% endblock %}
|
||||
{% block extra_style %}
|
||||
<style>
|
||||
.wrap { max-width: 1200px; margin: 0 auto; padding: 16px; }
|
||||
.filters { display:flex; gap:8px; margin: 10px 0 16px; }
|
||||
.filters input { padding: 10px 12px; border-radius: 10px; border: 1px solid #2a2a34; background:#111119; color:#e6e6e6; min-width: 240px; }
|
||||
.section { margin-top: 20px; }
|
||||
.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
|
||||
.card { background:#12121a; border:1px solid #1f2030; border-radius:12px; padding:12px; display:flex; gap:12px; }
|
||||
.poster { width: 90px; height: 135px; border-radius:8px; overflow:hidden; background:#222233; flex:0 0 auto; }
|
||||
.poster img { width:100%; height:100%; object-fit: cover; display:block; }
|
||||
.meta { flex:1 1 auto; min-width:0; }
|
||||
.title { font-weight:600; margin-bottom:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.muted { color:#9aa0b4; font-size:.9rem; }
|
||||
.user { font-weight:600; }
|
||||
|
||||
/* user list */
|
||||
.users { display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:10px; }
|
||||
.user-item { background:#10121b; border:1px solid #1f2030; border-radius:10px; padding:10px 12px; display:flex; justify-content:space-between; align-items:center; cursor:pointer; transition: transform .08s ease, border-color .12s ease; }
|
||||
.user-item:hover { transform: translateY(-1px); border-color:#2a2b44; }
|
||||
.badge { background:#171a26; border:1px solid #2a2b44; color:#cfd3ea; border-radius:999px; padding:2px 8px; font-size:.85rem; }
|
||||
|
||||
/* modal fix */
|
||||
.modal-backdrop { position:fixed; inset:0; display:none; align-items:center; justify-content:center; background: rgba(10,12,20,.55); backdrop-filter: blur(4px); z-index: 1000; }
|
||||
.modal { width:min(720px, 100%); max-height:92vh; overflow:auto; background: linear-gradient(180deg, #10121b 0%, #0d0f16 100%); border:1px solid #2a2b44; border-radius: 14px; }
|
||||
.modal-header { display:flex; align-items:center; gap:10px; padding:12px 14px; border-bottom:1px solid #20223a; background: rgba(13,15,22,.85); position:sticky; top:0; }
|
||||
.modal-close { margin-left:auto; background:#1a1f33; color:#c9cbe3; border:1px solid #2a2b44; width:34px; height:34px; border-radius:10px; cursor:pointer; }
|
||||
.section-block { background:#101327; border:1px solid #20223a; border-radius:12px; padding:14px; margin:12px; }
|
||||
.section-title { font-weight:650; margin-bottom:8px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="wrap">
|
||||
<h1>Subscriptions overview</h1>
|
||||
<div class="filters">
|
||||
<input type="text" id="q" placeholder="Search user/series/movies…">
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Users</h2>
|
||||
<p class="muted">Tap a user to view all subscriptions.</p>
|
||||
<div class="users" id="usersList">
|
||||
{% for u in user_stats %}
|
||||
<div class="user-item" data-user="{{ u.username_lower }}">
|
||||
<div>{{ u.username }}</div>
|
||||
<div class="badge">{{ u.total_count }}</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">No subscriptions yet.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Series</h2>
|
||||
<div class="grid" id="seriesGrid">
|
||||
{% for s in series %}
|
||||
<div class="card" data-user="{{ s.user.username|lower }}" data-title="{{ s.series_title|lower }}" data-id="{{ s.series_id }}">
|
||||
<div class="poster">
|
||||
{% if s.series_poster %}
|
||||
<img src="{{ s.series_poster }}" alt="{{ s.series_title }}">
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/90x135?text=No+Poster" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title">{{ s.series_title }}</div>
|
||||
<div class="muted">User: <span class="user">{{ s.user.username }}</span></div>
|
||||
<div class="muted">SeriesId: {{ s.series_id }}</div>
|
||||
<div class="muted">Since: {{ s.created_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">No series subscriptions.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Movies</h2>
|
||||
<div class="grid" id="moviesGrid">
|
||||
{% for m in movies %}
|
||||
<div class="card" data-user="{{ m.user.username|lower }}" data-title="{{ m.title|lower }}" data-id="{{ m.movie_id }}">
|
||||
<div class="poster">
|
||||
{% if m.poster %}
|
||||
<img src="{{ m.poster }}" alt="{{ m.title }}">
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/90x135?text=No+Poster" alt="">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<div class="title">{{ m.title }}</div>
|
||||
<div class="muted">User: <span class="user">{{ m.user.username }}</span></div>
|
||||
<div class="muted">MovieId: {{ m.movie_id }}</div>
|
||||
<div class="muted">Since: {{ m.created_at }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="muted">No movie subscriptions.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function(){
|
||||
const q = document.getElementById('q');
|
||||
function filter(){
|
||||
const v = (q.value||'').toLowerCase();
|
||||
[document.getElementById('seriesGrid'), document.getElementById('moviesGrid')].forEach(grid=>{
|
||||
if(!grid) return; Array.from(grid.children).forEach(card=>{
|
||||
const text = (card.dataset.user + ' ' + card.dataset.title).toLowerCase();
|
||||
card.style.display = text.includes(v) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
const users = document.getElementById('usersList');
|
||||
if(users){ Array.from(users.children).forEach(item=>{
|
||||
const username = item.querySelector('div')?.textContent?.toLowerCase() || '';
|
||||
item.style.display = username.includes(v) ? '' : 'none';
|
||||
}); }
|
||||
}
|
||||
q.addEventListener('input', filter);
|
||||
|
||||
// Group-by-user popup
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-backdrop';
|
||||
modal.style.display = 'none';
|
||||
modal.innerHTML = `
|
||||
<div class="modal" style="max-width:720px">
|
||||
<div class="modal-header">
|
||||
<div style="font-weight:700" id="uTitle"></div>
|
||||
<button class="modal-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="section-block"><div class="section-title">Series</div><div id="uSeries"></div></div>
|
||||
<div class="section-block"><div class="section-title">Movies</div><div id="uMovies"></div></div>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const uTitle = modal.querySelector('#uTitle');
|
||||
const uSeries = modal.querySelector('#uSeries');
|
||||
const uMovies = modal.querySelector('#uMovies');
|
||||
function open(){ modal.style.display='flex'; document.body.style.overflow='hidden'; }
|
||||
function close(){ modal.style.display='none'; document.body.style.overflow=''; }
|
||||
closeBtn.addEventListener('click', close);
|
||||
modal.addEventListener('click', e=>{ if(e.target===modal) close(); });
|
||||
|
||||
function renderList(container, items){
|
||||
container.innerHTML = '';
|
||||
if(!items.length){ container.innerHTML = '<div class="muted">—</div>'; return; }
|
||||
items.forEach(it=>{
|
||||
const row = document.createElement('div');
|
||||
row.style.display = 'flex'; row.style.gap='10px'; row.style.alignItems='center'; row.style.margin='6px 0';
|
||||
const poster = document.createElement('img');
|
||||
poster.src = it.poster || 'https://via.placeholder.com/50x75?text=No+Poster';
|
||||
poster.style.width='50px'; poster.style.height='75px'; poster.style.objectFit='cover'; poster.style.borderRadius='6px';
|
||||
const meta = document.createElement('div');
|
||||
meta.innerHTML = `<div style="font-weight:600">${it.title}</div><div class="muted">ID: ${it.id}</div>`;
|
||||
row.appendChild(poster); row.appendChild(meta);
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function gatherForUser(username){
|
||||
const series = Array.from(document.querySelectorAll('#seriesGrid .card')).filter(c=>c.dataset.user===username).map(c=>{
|
||||
const idFromAttr = c.dataset.id || '';
|
||||
let idFromText = '';
|
||||
if(!idFromAttr){
|
||||
const text = Array.from(c.querySelectorAll('.muted')).map(el=>el.textContent).join(' ');
|
||||
const m = text.match(/SeriesId:\s*(\d+)/i); idFromText = m ? m[1] : '';
|
||||
}
|
||||
return {
|
||||
title: c.querySelector('.title')?.textContent || '',
|
||||
id: idFromAttr || idFromText,
|
||||
poster: c.querySelector('img')?.src || ''
|
||||
};
|
||||
});
|
||||
const movies = Array.from(document.querySelectorAll('#moviesGrid .card')).filter(c=>c.dataset.user===username).map(c=>{
|
||||
const idFromAttr = c.dataset.id || '';
|
||||
let idFromText = '';
|
||||
if(!idFromAttr){
|
||||
const text = Array.from(c.querySelectorAll('.muted')).map(el=>el.textContent).join(' ');
|
||||
const m = text.match(/MovieId:\s*(\d+)/i); idFromText = m ? m[1] : '';
|
||||
}
|
||||
return {
|
||||
title: c.querySelector('.title')?.textContent || '',
|
||||
id: idFromAttr || idFromText,
|
||||
poster: c.querySelector('img')?.src || ''
|
||||
};
|
||||
});
|
||||
return {series, movies};
|
||||
}
|
||||
|
||||
function openForUser(usernameDisplay){
|
||||
const username = usernameDisplay.trim().toLowerCase();
|
||||
const {series, movies} = gatherForUser(username);
|
||||
uTitle.textContent = `Subscriptions for ${usernameDisplay.trim()}`;
|
||||
renderList(uSeries, series);
|
||||
renderList(uMovies, movies);
|
||||
open();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.user').forEach(uEl=>{
|
||||
uEl.style.cursor='pointer';
|
||||
uEl.title='Show all subscriptions for this user';
|
||||
uEl.addEventListener('click', ()=> openForUser(uEl.textContent));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.user-item').forEach(item=>{
|
||||
item.addEventListener('click', ()=>{
|
||||
const name = item.querySelector('div')?.textContent || '';
|
||||
openForUser(name);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
@@ -1,9 +1,10 @@
|
||||
from django.urls import path
|
||||
from .views import SettingsView, test_connection, first_run
|
||||
from .views import SettingsView, test_connection, first_run, subscriptions_overview
|
||||
|
||||
app_name = "settingspanel"
|
||||
urlpatterns = [
|
||||
path("", SettingsView.as_view(), name="index"),
|
||||
path("test-connection/", test_connection, name="test_connection"),
|
||||
path("setup/", first_run, name="setup"),
|
||||
path("subscriptions/", subscriptions_overview, name="subscriptions"),
|
||||
]
|
||||
|
@@ -7,6 +7,8 @@ from .models import AppSettings
|
||||
from django.http import JsonResponse
|
||||
from accounts.utils import jellyfin_admin_required
|
||||
from django.contrib.auth import get_user_model
|
||||
from arr_api.models import SeriesSubscription, MovieSubscription
|
||||
from django.db.models import Count
|
||||
import requests
|
||||
|
||||
def needs_setup():
|
||||
@@ -32,7 +34,7 @@ def first_run(request):
|
||||
settings.radarr_api_key = form.cleaned_data['radarr_api_key']
|
||||
settings.save()
|
||||
|
||||
messages.success(request, 'Setup erfolgreich abgeschlossen!')
|
||||
messages.success(request, 'Setup completed successfully!')
|
||||
return redirect('accounts:login')
|
||||
else:
|
||||
form = FirstRunSetupForm()
|
||||
@@ -53,9 +55,9 @@ def test_connection(request):
|
||||
url = (request.GET.get("url") or "").strip()
|
||||
key = (request.GET.get("key") or "").strip()
|
||||
if kind not in ("sonarr", "radarr"):
|
||||
return JsonResponse({"ok": False, "error": "Ungültiger Typ"}, status=400)
|
||||
return JsonResponse({"ok": False, "error": "Invalid type"}, status=400)
|
||||
if not url or not key:
|
||||
return JsonResponse({"ok": False, "error": "URL und API-Key erforderlich"}, status=400)
|
||||
return JsonResponse({"ok": False, "error": "URL and API key required"}, status=400)
|
||||
|
||||
try:
|
||||
r = requests.get(
|
||||
@@ -105,35 +107,87 @@ class SettingsView(View):
|
||||
arr_form = ArrSettingsForm(request.POST)
|
||||
mail_form = MailSettingsForm(request.POST)
|
||||
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, {
|
||||
"jellyfin_form": jellyfin_form,
|
||||
"arr_form": arr_form,
|
||||
"mail_form": mail_form,
|
||||
"account_form": acc_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
|
||||
cfg.radarr_api_key = arr_form.cleaned_data["radarr_api_key"] or None
|
||||
cfg.jellyfin_server_url = jellyfin_form.cleaned_data.get("jellyfin_server_url") or None
|
||||
cfg.jellyfin_api_key = jellyfin_form.cleaned_data.get("jellyfin_api_key") or None
|
||||
|
||||
cfg.mail_host = mail_form.cleaned_data["mail_host"] or None
|
||||
cfg.mail_port = mail_form.cleaned_data["mail_port"] or None
|
||||
cfg.mail_secure = mail_form.cleaned_data["mail_secure"] or ""
|
||||
cfg.mail_user = mail_form.cleaned_data["mail_user"] or None
|
||||
cfg.mail_password = mail_form.cleaned_data["mail_password"] or None
|
||||
cfg.mail_from = mail_form.cleaned_data["mail_from"] or None
|
||||
# Update Sonarr/Radarr settings
|
||||
cfg.sonarr_url = arr_form.cleaned_data.get("sonarr_url") or None
|
||||
cfg.sonarr_api_key = arr_form.cleaned_data.get("sonarr_api_key") or None
|
||||
cfg.radarr_url = arr_form.cleaned_data.get("radarr_url") or None
|
||||
cfg.radarr_api_key = arr_form.cleaned_data.get("radarr_api_key") or None
|
||||
|
||||
cfg.acc_username = acc_form.cleaned_data["username"] or None
|
||||
cfg.acc_email = acc_form.cleaned_data["email"] or None
|
||||
# Update Mail settings
|
||||
cfg.mail_host = mail_form.cleaned_data.get("mail_host") or None
|
||||
cfg.mail_port = mail_form.cleaned_data.get("mail_port") or None
|
||||
cfg.mail_secure = mail_form.cleaned_data.get("mail_secure") or ""
|
||||
cfg.mail_user = mail_form.cleaned_data.get("mail_user") or None
|
||||
cfg.mail_password = mail_form.cleaned_data.get("mail_password") or None
|
||||
cfg.mail_from = mail_form.cleaned_data.get("mail_from") or None
|
||||
|
||||
# Update account settings
|
||||
cfg.acc_username = acc_form.cleaned_data.get("username") or None
|
||||
cfg.acc_email = acc_form.cleaned_data.get("email") or None
|
||||
|
||||
cfg.save()
|
||||
messages.success(request, "Einstellungen gespeichert (DB).")
|
||||
messages.success(request, "Settings saved (DB).")
|
||||
return redirect("settingspanel:index")
|
||||
|
||||
@jellyfin_admin_required
|
||||
def subscriptions_overview(request):
|
||||
series = SeriesSubscription.objects.select_related('user').order_by('user__username', 'series_title')
|
||||
movies = MovieSubscription.objects.select_related('user').order_by('user__username', 'title')
|
||||
|
||||
# Aggregate counts per user
|
||||
s_counts = SeriesSubscription.objects.values('user_id', 'user__username').annotate(series_count=Count('id'))
|
||||
m_counts = MovieSubscription.objects.values('user_id', 'user__username').annotate(movie_count=Count('id'))
|
||||
|
||||
user_map = {}
|
||||
for row in s_counts:
|
||||
key = row['user_id']
|
||||
user_map.setdefault(key, {
|
||||
'user_id': key,
|
||||
'username': row['user__username'],
|
||||
'series_count': 0,
|
||||
'movie_count': 0,
|
||||
})
|
||||
user_map[key]['series_count'] = row['series_count']
|
||||
for row in m_counts:
|
||||
key = row['user_id']
|
||||
user_map.setdefault(key, {
|
||||
'user_id': key,
|
||||
'username': row['user__username'],
|
||||
'series_count': 0,
|
||||
'movie_count': 0,
|
||||
})
|
||||
user_map[key]['movie_count'] = row['movie_count']
|
||||
|
||||
user_stats = []
|
||||
for key, val in user_map.items():
|
||||
total = (val.get('series_count') or 0) + (val.get('movie_count') or 0)
|
||||
user_stats.append({
|
||||
'user_id': val['user_id'],
|
||||
'username': val['username'],
|
||||
'username_lower': (val['username'] or '').lower(),
|
||||
'series_count': val.get('series_count') or 0,
|
||||
'movie_count': val.get('movie_count') or 0,
|
||||
'total_count': total,
|
||||
})
|
||||
user_stats.sort(key=lambda x: (-x['total_count'], x['username'].lower()))
|
||||
|
||||
return render(request, 'settingspanel/subscriptions.html', {
|
||||
'series': series,
|
||||
'movies': movies,
|
||||
'user_stats': user_stats,
|
||||
})
|
||||
|
Reference in New Issue
Block a user