multiuser/subscriptions/notifications
This commit is contained in:
		
							
								
								
									
										0
									
								
								accounts/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								accounts/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								accounts/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								accounts/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| from django.contrib import admin | ||||
|  | ||||
| # Register your models here. | ||||
							
								
								
									
										6
									
								
								accounts/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								accounts/apps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class AccountsConfig(AppConfig): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     name = 'accounts' | ||||
							
								
								
									
										24
									
								
								accounts/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								accounts/forms.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| from django import forms | ||||
| from django.contrib.auth.forms import UserCreationForm, UserChangeForm | ||||
| from .models import User | ||||
|  | ||||
| class CustomUserCreationForm(UserCreationForm): | ||||
|     email = forms.EmailField(required=True) | ||||
|      | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = ('username', 'email', 'password1', 'password2') | ||||
|  | ||||
| class CustomUserChangeForm(UserChangeForm): | ||||
|     password = None  # Passwort-Änderung über extra Formular | ||||
|      | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = ('email',) | ||||
|         widgets = { | ||||
|             'email': forms.EmailInput(attrs={'class': 'text-input', 'placeholder': 'E-Mail-Adresse'}), | ||||
|         } | ||||
|  | ||||
| class JellyfinLoginForm(forms.Form): | ||||
|     username = forms.CharField(label='Benutzername', widget=forms.TextInput(attrs={'class': 'form-control'})) | ||||
|     password = forms.CharField(label='Passwort', widget=forms.PasswordInput(attrs={'class': 'form-control'})) | ||||
							
								
								
									
										45
									
								
								accounts/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								accounts/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| # Generated by Django 5.2.5 on 2025-08-10 11:59 | ||||
|  | ||||
| import django.contrib.auth.models | ||||
| import django.contrib.auth.validators | ||||
| import django.utils.timezone | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     initial = True | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('auth', '0012_alter_user_first_name_max_length'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='User', | ||||
|             fields=[ | ||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||
|                 ('password', models.CharField(max_length=128, verbose_name='password')), | ||||
|                 ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), | ||||
|                 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), | ||||
|                 ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), | ||||
|                 ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), | ||||
|                 ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), | ||||
|                 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), | ||||
|                 ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), | ||||
|                 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), | ||||
|                 ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), | ||||
|                 ('bio', models.TextField(blank=True, max_length=500)), | ||||
|                 ('is_admin', models.BooleanField(default=False)), | ||||
|                 ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), | ||||
|                 ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'user', | ||||
|                 'verbose_name_plural': 'users', | ||||
|             }, | ||||
|             managers=[ | ||||
|                 ('objects', django.contrib.auth.models.UserManager()), | ||||
|             ], | ||||
|         ), | ||||
|     ] | ||||
| @@ -0,0 +1,28 @@ | ||||
| # Generated by Django 5.2.5 on 2025-08-10 12:38 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('accounts', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='jellyfin_server', | ||||
|             field=models.CharField(blank=True, max_length=200, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='jellyfin_token', | ||||
|             field=models.CharField(blank=True, max_length=500, null=True), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name='user', | ||||
|             name='jellyfin_user_id', | ||||
|             field=models.CharField(blank=True, max_length=100, null=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										0
									
								
								accounts/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								accounts/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										39
									
								
								accounts/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								accounts/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| from django.db import models | ||||
| from django.contrib.auth.models import AbstractUser | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| class User(AbstractUser): | ||||
|     """ | ||||
|     Custom User Model mit zusätzlichen Feldern und Berechtigungen. | ||||
|     Normale User können nur ihre eigenen Daten bearbeiten. | ||||
|     Admin-User können alles. | ||||
|     """ | ||||
|     email = models.EmailField(_("email address"), unique=True) | ||||
|     bio = models.TextField(max_length=500, blank=True) | ||||
|     is_admin = models.BooleanField(default=False) | ||||
|      | ||||
|     # Jellyfin fields | ||||
|     jellyfin_user_id = models.CharField(max_length=100, blank=True, null=True) | ||||
|     jellyfin_token = models.CharField(max_length=500, blank=True, null=True) | ||||
|     jellyfin_server = models.CharField(max_length=200, blank=True, null=True) | ||||
|      | ||||
|     def check_jellyfin_admin(self): | ||||
|         """Check if user is Jellyfin admin on the server""" | ||||
|         from accounts.utils import JellyfinClient | ||||
|         if not self.jellyfin_user_id or not self.jellyfin_token: | ||||
|             return False | ||||
|         try: | ||||
|             client = JellyfinClient() | ||||
|             return client.is_admin(self.jellyfin_user_id, self.jellyfin_token) | ||||
|         except: | ||||
|             # Im Fehlerfall den lokalen Status verwenden | ||||
|             return self.is_admin | ||||
|              | ||||
|     @property  | ||||
|     def is_jellyfin_admin(self): | ||||
|         """Check if user is admin either locally or on Jellyfin server""" | ||||
|         return self.is_admin or self.check_jellyfin_admin() | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("user") | ||||
|         verbose_name_plural = _("users") | ||||
							
								
								
									
										15
									
								
								accounts/templates/accounts/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								accounts/templates/accounts/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="auth-container"> | ||||
|     <h2>Anmelden</h2> | ||||
|     <form method="post" class="auth-form"> | ||||
|         {% csrf_token %} | ||||
|         {{ form.as_p }} | ||||
|         <button type="submit" class="btn-primary">Anmelden</button> | ||||
|     </form> | ||||
|     <div class="auth-links"> | ||||
|         <p>Noch kein Konto? <a href="{% url 'accounts:register' %}">Jetzt registrieren</a></p> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										12
									
								
								accounts/templates/accounts/password_change.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								accounts/templates/accounts/password_change.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="auth-container"> | ||||
|     <h2>Passwort ändern</h2> | ||||
|     <form method="post" class="auth-form"> | ||||
|         {% csrf_token %} | ||||
|         {{ form.as_p }} | ||||
|         <button type="submit" class="btn-primary">Passwort ändern</button> | ||||
|     </form> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										9
									
								
								accounts/templates/accounts/password_change_done.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								accounts/templates/accounts/password_change_done.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="auth-container"> | ||||
|     <h2>Passwort geändert</h2> | ||||
|     <p>Ihr Passwort wurde erfolgreich geändert.</p> | ||||
|     <p><a href="{% url 'accounts:profile' %}">Zurück zum Profil</a></p> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										97
									
								
								accounts/templates/accounts/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								accounts/templates/accounts/profile.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| {% extends "base.html" %} | ||||
| {% load static %} | ||||
|  | ||||
| {% block extra_style %} | ||||
| <link rel="stylesheet" href="{% static 'css/profile.css' %}"> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="profile-container"> | ||||
|     <h2>Hallo, {{ user.username }}</h2> | ||||
|  | ||||
|     {% if messages %} | ||||
|     <div class="messages"> | ||||
|         {% for message in messages %} | ||||
|         <div class="message {{ message.tags }}">{{ message }}</div> | ||||
|         {% endfor %} | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     <div class="profile-section"> | ||||
|         <h3>E-Mail-Adresse</h3> | ||||
|         <form method="post" class="profile-form compact-form"> | ||||
|             {% csrf_token %} | ||||
|             <div class="form-row"> | ||||
|                 <label for="id_email">E-Mail</label> | ||||
|                 {{ form.email }} | ||||
|             </div> | ||||
|             <button type="submit" class="btn-primary">Speichern</button> | ||||
|         </form> | ||||
|  | ||||
|         {% if user.jellyfin_server %} | ||||
|         <div class="jellyfin-info"> | ||||
|             <h4>Jellyfin-Verbindung</h4> | ||||
|             <p> | ||||
|                 Server: {{ user.jellyfin_server }}<br> | ||||
|                 Status: {% if user.jellyfin_token %}Verbunden{% else %}Nicht verbunden{% endif %}<br> | ||||
|                 {% if user.is_jellyfin_admin %} | ||||
|                 <span class="badge badge-admin">Jellyfin Administrator</span> | ||||
|                 {% endif %} | ||||
|             </p> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|     </div> | ||||
|  | ||||
|     <div class="profile-section"> | ||||
|         <h3>Meine Abonnements</h3> | ||||
|  | ||||
|         <h4>Serien</h4> | ||||
|         {% if series_subs %} | ||||
|         <div class="subscription-list"> | ||||
|             {% for sub in series_subs %} | ||||
|             <div class="subscription-item"> | ||||
|                 {% if sub.series_poster %} | ||||
|                 <img src="{{ sub.series_poster }}" alt="{{ sub.series_title }}" class="subscription-poster"> | ||||
|                 {% else %} | ||||
|                 <img src="https://via.placeholder.com/80x120?text=Kein+Poster" alt="" class="subscription-poster"> | ||||
|                 {% endif %} | ||||
|                 <div class="subscription-info"> | ||||
|                     <div class="subscription-title">{{ sub.series_title }}</div> | ||||
|                     <div class="subscription-date">Abonniert am {{ sub.created_at|date:"d.m.Y" }}</div> | ||||
|                     {% if sub.series_overview %} | ||||
|                     <div class="subscription-overview">{{ sub.series_overview|truncatechars:100 }}</div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         {% else %} | ||||
|         <p class="muted">Keine Serien abonniert.</p> | ||||
|         {% endif %} | ||||
|  | ||||
|         <h4>Filme</h4> | ||||
|         {% if movie_subs %} | ||||
|         <div class="subscription-list"> | ||||
|             {% for sub in movie_subs %} | ||||
|             <div class="subscription-item"> | ||||
|                 {% if sub.poster %} | ||||
|                 <img src="{{ sub.poster }}" alt="{{ sub.title }}" class="subscription-poster"> | ||||
|                 {% else %} | ||||
|                 <img src="https://via.placeholder.com/80x120?text=Kein+Poster" alt="" class="subscription-poster"> | ||||
|                 {% endif %} | ||||
|                 <div class="subscription-info"> | ||||
|                     <div class="subscription-title">{{ sub.title }}</div> | ||||
|                     <div class="subscription-date">Abonniert am {{ sub.created_at|date:"d.m.Y" }}</div> | ||||
|                     {% if sub.overview %} | ||||
|                     <div class="subscription-overview">{{ sub.overview|truncatechars:100 }}</div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
|             </div> | ||||
|             {% endfor %} | ||||
|         </div> | ||||
|         {% else %} | ||||
|         <p class="muted">Keine Filme abonniert.</p> | ||||
|         {% endif %} | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										15
									
								
								accounts/templates/accounts/register.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								accounts/templates/accounts/register.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% block content %} | ||||
| <div class="auth-container"> | ||||
|     <h2>Registrieren</h2> | ||||
|     <form method="post" class="auth-form"> | ||||
|         {% csrf_token %} | ||||
|         {{ form.as_p }} | ||||
|         <button type="submit" class="btn-primary">Registrieren</button> | ||||
|     </form> | ||||
|     <div class="auth-links"> | ||||
|         <p>Bereits ein Konto? <a href="{% url 'accounts:login' %}">Jetzt anmelden</a></p> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
							
								
								
									
										3
									
								
								accounts/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								accounts/tests.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| from django.test import TestCase | ||||
|  | ||||
| # Create your tests here. | ||||
							
								
								
									
										19
									
								
								accounts/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								accounts/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| from django.urls import path | ||||
| from django.contrib.auth import views as auth_views | ||||
| from . import views | ||||
|  | ||||
| app_name = 'accounts' | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('login/', views.jellyfin_login, name='login'), | ||||
|     path('logout/', auth_views.LogoutView.as_view(), name='logout'), | ||||
|     path('register/', views.RegisterView.as_view(), name='register'), | ||||
|     path('profile/', views.profile, name='profile'), | ||||
|     path('password_change/', auth_views.PasswordChangeView.as_view( | ||||
|         template_name='accounts/password_change.html', | ||||
|         success_url='done/' | ||||
|     ), name='password_change'), | ||||
|     path('password_change/done/', auth_views.PasswordChangeDoneView.as_view( | ||||
|         template_name='accounts/password_change_done.html' | ||||
|     ), name='password_change_done'), | ||||
| ] | ||||
							
								
								
									
										117
									
								
								accounts/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								accounts/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | ||||
| import requests | ||||
| from django.conf import settings | ||||
| from django.core.cache import cache | ||||
| from functools import wraps | ||||
| from django.shortcuts import redirect | ||||
| from django.contrib import messages | ||||
|  | ||||
| class JellyfinClient: | ||||
|     def __init__(self): | ||||
|         # Basis-Einstellungen aus den Django-Settings | ||||
|         self.client = settings.JELLYFIN_CLIENT | ||||
|         self.version = settings.JELLYFIN_VERSION | ||||
|         self.device = settings.JELLYFIN_DEVICE | ||||
|         self.device_id = settings.JELLYFIN_DEVICE_ID | ||||
|         self.server_url = None  # Wird später gesetzt | ||||
|         self.api_key = None     # Optional, wird aus den AppSettings geholt wenn nötig | ||||
|  | ||||
|     def authenticate(self, username, password): | ||||
|         """Authenticate with Jellyfin and return user info if successful""" | ||||
|         if not self.server_url: | ||||
|             raise ValueError("Keine Server-URL angegeben") | ||||
|  | ||||
|         # Stelle sicher, dass die URL ein Protokoll hat | ||||
|         if not self.server_url.startswith(('http://', 'https://')): | ||||
|             self.server_url = f'http://{self.server_url}' | ||||
|          | ||||
|         # Entferne trailing slashes | ||||
|         self.server_url = self.server_url.rstrip('/') | ||||
|  | ||||
|         headers = { | ||||
|             'X-Emby-Authorization': ( | ||||
|                 f'MediaBrowser Client="{self.client}", ' | ||||
|                 f'Device="{self.device}", ' | ||||
|                 f'DeviceId="{self.device_id}", ' | ||||
|                 f'Version="{self.version}"' | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         auth_data = { | ||||
|             'Username': username, | ||||
|             'Pw': password | ||||
|         } | ||||
|  | ||||
|         try: | ||||
|             response = requests.post( | ||||
|                 f'{self.server_url}/Users/AuthenticateByName', | ||||
|                 json=auth_data, | ||||
|                 headers=headers, | ||||
|                 timeout=10 | ||||
|             ) | ||||
|             response.raise_for_status() | ||||
|             data = response.json() | ||||
|              | ||||
|             return { | ||||
|                 'user_id': data['User']['Id'], | ||||
|                 'access_token': data['AccessToken'], | ||||
|                 'is_admin': data['User'].get('Policy', {}).get('IsAdministrator', False) | ||||
|             } | ||||
|         except requests.exceptions.ConnectionError: | ||||
|             raise ValueError("Verbindung zum Server nicht möglich. Bitte überprüfen Sie die Server-URL.") | ||||
|         except requests.exceptions.Timeout: | ||||
|             raise ValueError("Zeitüberschreitung bei der Verbindung zum Server.") | ||||
|         except requests.exceptions.HTTPError as e: | ||||
|             if e.response.status_code == 401: | ||||
|                 return None  # Authentifizierung fehlgeschlagen | ||||
|             raise ValueError(f"HTTP-Fehler: {e.response.status_code}") | ||||
|         except Exception as e: | ||||
|             return None | ||||
|  | ||||
|     def is_admin(self, user_id, token): | ||||
|         """Check if user is admin in Jellyfin""" | ||||
|         cache_key = f'jellyfin_admin_{user_id}' | ||||
|          | ||||
|         # Check cache first | ||||
|         cached = cache.get(cache_key) | ||||
|         if cached is not None: | ||||
|             return cached | ||||
|  | ||||
|         headers = { | ||||
|             'X-Emby-Authorization': ( | ||||
|                 f'MediaBrowser Client="{self.client}", ' | ||||
|                 f'Device="{self.device}", ' | ||||
|                 f'DeviceId="{self.device_id}", ' | ||||
|                 f'Version="{self.version}", ' | ||||
|                 f'Token="{token}"' | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         try: | ||||
|             response = requests.get( | ||||
|                 f'{self.server_url}/Users/{user_id}', | ||||
|                 headers=headers | ||||
|             ) | ||||
|             response.raise_for_status() | ||||
|             data = response.json() | ||||
|             is_admin = data.get('Policy', {}).get('IsAdministrator', False) | ||||
|              | ||||
|             # Cache result for 5 minutes | ||||
|             cache.set(cache_key, is_admin, 300) | ||||
|              | ||||
|             return is_admin | ||||
|         except: | ||||
|             return False | ||||
|  | ||||
| def jellyfin_admin_required(view_func): | ||||
|     @wraps(view_func) | ||||
|     def _wrapped_view(request, *args, **kwargs): | ||||
|         if not request.user.is_authenticated: | ||||
|             messages.error(request, 'Sie müssen angemeldet sein, um diese Seite zu sehen.') | ||||
|             return redirect('accounts:login') | ||||
|          | ||||
|         if not request.user.is_jellyfin_admin: | ||||
|             messages.error(request, 'Sie benötigen Admin-Rechte, um diese Seite zu sehen.') | ||||
|             return redirect('index') | ||||
|              | ||||
|         return view_func(request, *args, **kwargs) | ||||
|     return _wrapped_view | ||||
							
								
								
									
										132
									
								
								accounts/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								accounts/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| from django.shortcuts import render, redirect | ||||
| from django.contrib.auth.decorators import login_required | ||||
| from django.contrib import messages | ||||
| from django.urls import reverse_lazy | ||||
| from django.views.generic.edit import CreateView | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth import login | ||||
| from django.conf import settings | ||||
| from .forms import CustomUserCreationForm, CustomUserChangeForm, JellyfinLoginForm | ||||
| from .models import User | ||||
| from .utils import JellyfinClient | ||||
|  | ||||
| class RegisterView(CreateView): | ||||
|     form_class = CustomUserCreationForm | ||||
|     template_name = 'accounts/register.html' | ||||
|     success_url = reverse_lazy('accounts:login') | ||||
|      | ||||
|     def form_valid(self, form): | ||||
|         response = super().form_valid(form) | ||||
|         messages.success(self.request, 'Registrierung erfolgreich! Sie können sich jetzt anmelden.') | ||||
|         return response | ||||
|  | ||||
| @login_required | ||||
| def profile(request): | ||||
|     if request.method == 'POST': | ||||
|         form = CustomUserChangeForm(request.POST, instance=request.user) | ||||
|         if form.is_valid(): | ||||
|             form.save() | ||||
|             messages.success(request, 'E-Mail gespeichert.') | ||||
|             return redirect('accounts:profile') | ||||
|     else: | ||||
|         form = CustomUserChangeForm(instance=request.user) | ||||
|  | ||||
|     # Lade Abonnements | ||||
|     series_subs = request.user.series_subscriptions.all() | ||||
|     movie_subs = request.user.movie_subscriptions.all() | ||||
|  | ||||
|     # Best-effort Backfill fehlender Poster, damit die Profilseite Bilder zeigt | ||||
|     try: | ||||
|         from settingspanel.models import AppSettings | ||||
|         from arr_api.services import sonarr_get_series, radarr_lookup_movie_by_title | ||||
|         cfg = AppSettings.current() | ||||
|         # Serien | ||||
|         for sub in series_subs: | ||||
|             if not sub.series_poster and sub.series_id: | ||||
|                 details = sonarr_get_series(sub.series_id, base_url=cfg.sonarr_url, api_key=cfg.sonarr_api_key) | ||||
|                 if details and details.get('series_poster'): | ||||
|                     sub.series_poster = details['series_poster'] | ||||
|                     if not sub.series_overview: | ||||
|                         sub.series_overview = details.get('series_overview') or '' | ||||
|                     if not sub.series_genres: | ||||
|                         sub.series_genres = details.get('series_genres') or [] | ||||
|                     sub.save(update_fields=['series_poster', 'series_overview', 'series_genres']) | ||||
|         # Filme | ||||
|         for sub in movie_subs: | ||||
|             if not sub.poster: | ||||
|                 details = radarr_lookup_movie_by_title(sub.title, base_url=cfg.radarr_url, api_key=cfg.radarr_api_key) | ||||
|                 if details and details.get('poster'): | ||||
|                     sub.poster = details['poster'] | ||||
|                     if not sub.overview: | ||||
|                         sub.overview = details.get('overview') or '' | ||||
|                     if not sub.genres: | ||||
|                         sub.genres = details.get('genres') or [] | ||||
|                     sub.save(update_fields=['poster', 'overview', 'genres']) | ||||
|     except Exception: | ||||
|         # still show page even if lookups fail | ||||
|         pass | ||||
|      | ||||
|     return render(request, 'accounts/profile.html', { | ||||
|         'form': form, | ||||
|         'series_subs': series_subs, | ||||
|         'movie_subs': movie_subs, | ||||
|     }) | ||||
|  | ||||
| def jellyfin_login(request): | ||||
|     if request.method == 'POST': | ||||
|         form = JellyfinLoginForm(request.POST) | ||||
|         if form.is_valid(): | ||||
|             username = form.cleaned_data['username'] | ||||
|             password = form.cleaned_data['password'] | ||||
|              | ||||
|             # Jellyfin-URL aus AppSettings | ||||
|             from settingspanel.models import AppSettings | ||||
|             app_settings = AppSettings.current() | ||||
|             server_url = app_settings.get_jellyfin_url() | ||||
|             if not server_url: | ||||
|                 messages.error(request, 'Jellyfin Server ist nicht konfiguriert. Bitte Setup abschließen.') | ||||
|                 return render(request, 'accounts/login.html', {'form': form}) | ||||
|  | ||||
|             try: | ||||
|                 client = JellyfinClient() | ||||
|                 client.server_url = server_url | ||||
|                 auth_result = client.authenticate(username, password) | ||||
|                  | ||||
|                 if not auth_result: | ||||
|                     messages.error(request, 'Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten.') | ||||
|                     return render(request, 'accounts/login.html', {'form': form}) | ||||
|  | ||||
|                 # Existierenden User finden oder neu erstellen | ||||
|                 try: | ||||
|                     user = User.objects.get(username=username) | ||||
|                 except User.DoesNotExist: | ||||
|                     user = User.objects.create_user( | ||||
|                         username=username, | ||||
|                         email=f"{username}@jellyfin.local" | ||||
|                     ) | ||||
|  | ||||
|                 # Jellyfin Daten aktualisieren | ||||
|                 user.jellyfin_user_id = auth_result['user_id'] | ||||
|                 user.jellyfin_token = auth_result['access_token'] | ||||
|                 user.jellyfin_server = server_url | ||||
|                 user.save() | ||||
|  | ||||
|                 if auth_result['is_admin']: | ||||
|                     user.is_admin = True | ||||
|                     user.save() | ||||
|  | ||||
|                 login(request, user) | ||||
|                 messages.success(request, f'Willkommen, {username}!') | ||||
|                 return redirect('arr_api:index') | ||||
|                      | ||||
|             except ValueError as e: | ||||
|                 messages.error(request, str(e)) | ||||
|             except Exception as e: | ||||
|                 messages.error(request, f'Verbindungsfehler: {str(e)}') | ||||
|         # invalid form or error path | ||||
|         return render(request, 'accounts/login.html', {'form': form}) | ||||
|  | ||||
|     else: | ||||
|         form = JellyfinLoginForm() | ||||
|  | ||||
|     return render(request, 'accounts/login.html', {'form': form}) | ||||
		Reference in New Issue
	
	Block a user