diff --git a/accounts/forms.py b/accounts/forms.py
index 1e71180..022e3c5 100644
--- a/accounts/forms.py
+++ b/accounts/forms.py
@@ -10,15 +10,15 @@ class CustomUserCreationForm(UserCreationForm):
fields = ('username', 'email', 'password1', 'password2')
class CustomUserChangeForm(UserChangeForm):
- password = None # Passwort-Änderung über extra Formular
+ password = None # Password change via separate form
class Meta:
model = User
fields = ('email',)
widgets = {
- 'email': forms.EmailInput(attrs={'class': 'text-input', 'placeholder': 'E-Mail-Adresse'}),
+ 'email': forms.EmailInput(attrs={'class': 'text-input', 'placeholder': 'Email address'}),
}
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'}))
+ username = forms.CharField(label='Username', widget=forms.TextInput(attrs={'class': 'form-control'}))
+ password = forms.CharField(label='Password', widget=forms.PasswordInput(attrs={'class': 'form-control'}))
diff --git a/accounts/models.py b/accounts/models.py
index f80eb9a..5233221 100644
--- a/accounts/models.py
+++ b/accounts/models.py
@@ -4,9 +4,9 @@ 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.
+ Custom User Model with additional fields and permissions.
+ Regular users can only edit their own data.
+ Admin users can edit everything.
"""
email = models.EmailField(_("email address"), unique=True)
bio = models.TextField(max_length=500, blank=True)
@@ -26,7 +26,7 @@ class User(AbstractUser):
client = JellyfinClient()
return client.is_admin(self.jellyfin_user_id, self.jellyfin_token)
except:
- # Im Fehlerfall den lokalen Status verwenden
+ # On error, fall back to local status
return self.is_admin
@property
diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html
index 996abd9..830af11 100644
--- a/accounts/templates/accounts/login.html
+++ b/accounts/templates/accounts/login.html
@@ -2,14 +2,14 @@
{% block content %}
{% endblock %}
\ No newline at end of file
diff --git a/accounts/utils.py b/accounts/utils.py
index a7c7dab..92ae664 100644
--- a/accounts/utils.py
+++ b/accounts/utils.py
@@ -7,7 +7,7 @@ from django.contrib import messages
class JellyfinClient:
def __init__(self):
- # Basis-Einstellungen aus den Django-Settings
+ # Base settings from Django settings
self.client = settings.JELLYFIN_CLIENT
self.version = settings.JELLYFIN_VERSION
self.device = settings.JELLYFIN_DEVICE
@@ -18,13 +18,13 @@ class JellyfinClient:
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")
+ raise ValueError("No server URL provided")
- # Stelle sicher, dass die URL ein Protokoll hat
+ # Ensure the URL has a protocol
if not self.server_url.startswith(('http://', 'https://')):
self.server_url = f'http://{self.server_url}'
- # Entferne trailing slashes
+ # Remove trailing slashes
self.server_url = self.server_url.rstrip('/')
headers = {
@@ -57,13 +57,13 @@ class JellyfinClient:
'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.")
+ raise ValueError("Unable to connect to the server. Please check the server URL.")
except requests.exceptions.Timeout:
- raise ValueError("Zeitüberschreitung bei der Verbindung zum Server.")
+ raise ValueError("Connection to the server timed out.")
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}")
+ raise ValueError(f"HTTP error: {e.response.status_code}")
except Exception as e:
return None
@@ -71,7 +71,7 @@ class JellyfinClient:
"""Check if user is admin in Jellyfin"""
cache_key = f'jellyfin_admin_{user_id}'
- # Check cache first
+ # Check cache first
cached = cache.get(cache_key)
if cached is not None:
return cached
@@ -106,11 +106,11 @@ 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.')
+ messages.error(request, 'You must be logged in to view this page.')
return redirect('accounts:login')
if not request.user.is_jellyfin_admin:
- messages.error(request, 'Sie benötigen Admin-Rechte, um diese Seite zu sehen.')
+ messages.error(request, 'You need admin rights to view this page.')
return redirect('index')
return view_func(request, *args, **kwargs)
diff --git a/accounts/views.py b/accounts/views.py
index ba5515d..8ab32b1 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -17,7 +17,7 @@ class RegisterView(CreateView):
def form_valid(self, form):
response = super().form_valid(form)
- messages.success(self.request, 'Registrierung erfolgreich! Sie können sich jetzt anmelden.')
+ messages.success(self.request, 'Registration successful! You can now sign in.')
return response
@login_required
@@ -26,12 +26,12 @@ def profile(request):
form = CustomUserChangeForm(request.POST, instance=request.user)
if form.is_valid():
form.save()
- messages.success(request, 'E-Mail gespeichert.')
+ messages.success(request, 'Email saved.')
return redirect('accounts:profile')
else:
form = CustomUserChangeForm(instance=request.user)
- # Lade Abonnements
+ # Load subscriptions
series_subs = request.user.series_subscriptions.all()
movie_subs = request.user.movie_subscriptions.all()
@@ -40,7 +40,7 @@ def profile(request):
from settingspanel.models import AppSettings
from arr_api.services import sonarr_get_series, radarr_lookup_movie_by_title
cfg = AppSettings.current()
- # Serien
+ # Series
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)
@@ -51,7 +51,7 @@ def profile(request):
if not sub.series_genres:
sub.series_genres = details.get('series_genres') or []
sub.save(update_fields=['series_poster', 'series_overview', 'series_genres'])
- # Filme
+ # Movies
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)
@@ -84,7 +84,7 @@ def jellyfin_login(request):
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.')
+ messages.error(request, 'Jellyfin server is not configured. Please complete setup.')
return render(request, 'accounts/login.html', {'form': form})
try:
@@ -93,7 +93,7 @@ def jellyfin_login(request):
auth_result = client.authenticate(username, password)
if not auth_result:
- messages.error(request, 'Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten.')
+ messages.error(request, 'Sign in failed. Please check your credentials.')
return render(request, 'accounts/login.html', {'form': form})
# Existierenden User finden oder neu erstellen
@@ -116,13 +116,13 @@ def jellyfin_login(request):
user.save()
login(request, user)
- messages.success(request, f'Willkommen, {username}!')
+ messages.success(request, f'Welcome, {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)}')
+ messages.error(request, f'Connection error: {str(e)}')
# invalid form or error path
return render(request, 'accounts/login.html', {'form': form})
diff --git a/arr_api/management/commands/check_new_media.py b/arr_api/management/commands/check_new_media.py
index c3eca81..ad9ae90 100644
--- a/arr_api/management/commands/check_new_media.py
+++ b/arr_api/management/commands/check_new_media.py
@@ -3,12 +3,12 @@ from django.utils import timezone
from arr_api.notifications import check_and_notify_users
class Command(BaseCommand):
- help = 'Prüft neue Medien und sendet Benachrichtigungen'
+ help = 'Checks for new media and sends notifications'
def handle(self, *args, **kwargs):
- self.stdout.write(f'[{timezone.now()}] Starte Medien-Check...')
+ self.stdout.write(f'[{timezone.now()}] Starting media check...')
try:
check_and_notify_users()
- self.stdout.write(self.style.SUCCESS(f'[{timezone.now()}] Medien-Check erfolgreich beendet'))
+ self.stdout.write(self.style.SUCCESS(f'[{timezone.now()}] Media check finished successfully'))
except Exception as e:
- self.stdout.write(self.style.ERROR(f'[{timezone.now()}] Fehler beim Medien-Check: {str(e)}'))
+ self.stdout.write(self.style.ERROR(f'[{timezone.now()}] Error during media check: {str(e)}'))
diff --git a/arr_api/models.py b/arr_api/models.py
index 8d731cb..d0a567c 100644
--- a/arr_api/models.py
+++ b/arr_api/models.py
@@ -12,7 +12,7 @@ class SeriesSubscription(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
- unique_together = ['user', 'series_id'] # Ein User kann eine Serie nur einmal abonnieren
+ unique_together = ['user', 'series_id'] # A user can subscribe to a series only once
def __str__(self):
return self.series_title
@@ -29,18 +29,16 @@ class MovieSubscription(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
- unique_together = ['user', 'movie_id'] # Ein User kann einen Film nur einmal abonnieren
+ unique_together = ['user', 'movie_id'] # A user can subscribe to a movie only once
def __str__(self):
return self.title
class SentNotification(models.Model):
- """
- Speichert gesendete Benachrichtigungen um Duplikate zu vermeiden
- """
+ """Store sent notifications to avoid duplicates"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
media_id = models.IntegerField()
- media_type = models.CharField(max_length=10) # 'series' oder 'movie'
+ media_type = models.CharField(max_length=10) # 'series' or 'movie'
media_title = models.CharField(max_length=255)
air_date = models.DateField()
sent_at = models.DateTimeField(auto_now_add=True)
diff --git a/arr_api/notifications.py b/arr_api/notifications.py
index 4c90b08..45ff7ce 100644
--- a/arr_api/notifications.py
+++ b/arr_api/notifications.py
@@ -68,7 +68,7 @@ def send_notification_email(
release_type=None,
):
"""
- Sendet eine Benachrichtigungs-E-Mail an einen User mit erweiterten Details
+ Sends a notification email to a user with extended details
"""
eff = _set_runtime_email_settings()
logger.info(
@@ -94,7 +94,7 @@ def send_notification_email(
context = {
'username': user.username,
'title': media_title,
- 'type': 'Serie' if media_type == 'series' else 'Film',
+ 'type': 'Series' if media_type == 'series' else 'Movie',
'overview': overview,
'poster_url': poster_url,
'episode_title': episode_title,
@@ -105,7 +105,7 @@ def send_notification_email(
'release_type': release_type,
}
- subject = f"Neue {context['type']} verfügbar: {media_title}"
+ subject = f"New {context['type']} available: {media_title}"
message = render_to_string('arr_api/email/new_media_notification.html', context)
send_mail(
@@ -209,8 +209,8 @@ def get_todays_radarr_calendar():
def check_jellyfin_availability(user, media_id, media_type):
"""
- Ersetzt: Wir prüfen Verfügbarkeit über Sonarr/Radarr (hasFile),
- was zuverlässig ist, wenn Jellyfin dieselben Ordner scannt.
+ Replaced: We check availability via Sonarr/Radarr (hasFile),
+ which is reliable if Jellyfin scans the same folders.
"""
# user is unused here; kept for backward compatibility
if media_type == 'series':
diff --git a/arr_api/templates/arr_api/calendar.html b/arr_api/templates/arr_api/calendar.html
new file mode 100644
index 0000000..ae51bba
--- /dev/null
+++ b/arr_api/templates/arr_api/calendar.html
@@ -0,0 +1,301 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block title %}Calendar – Subscribarr{% endblock %}
+
+{% block extra_style %}
+
+
+
+{% endblock %}
+
+{% block content %}
+