diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 96bce55..71e7fc7 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 291d043..f1c904d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -151,7 +151,6 @@ STATIC_ROOT = BASE_DIR / 'staticfiles' STATICFILES_DIRS = [ BASE_DIR / 'static', - BASE_DIR / 'assets', BASE_DIR / 'node_modules', ] @@ -179,4 +178,4 @@ if EMAIL_USE_SSL: # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' \ No newline at end of file diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..b3c7d96 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/api_clients.cpython-311.pyc b/core/__pycache__/api_clients.cpython-311.pyc new file mode 100644 index 0000000..f72f89e Binary files /dev/null and b/core/__pycache__/api_clients.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..cc337ee 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..9bdcb74 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/utils.cpython-311.pyc b/core/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..366ff46 Binary files /dev/null and b/core/__pycache__/utils.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..b436cab 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..80e2ba7 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,7 @@ from django.contrib import admin +from .models import ServiceSetting -# Register your models here. +@admin.register(ServiceSetting) +class ServiceSettingAdmin(admin.ModelAdmin): + list_display = ('name', 'url', 'is_active') + list_editable = ('is_active',) \ No newline at end of file diff --git a/core/api_clients.py b/core/api_clients.py new file mode 100644 index 0000000..67ee1dc --- /dev/null +++ b/core/api_clients.py @@ -0,0 +1,121 @@ +import requests +from .models import ServiceSetting + +class BaseClient: + def __init__(self, name): + try: + self.setting = ServiceSetting.objects.get(name=name, is_active=True) + self.url = self.setting.url.rstrip('/') + self.api_key = self.setting.api_key + except ServiceSetting.DoesNotExist: + self.setting = None + self.url = None + self.api_key = None + + def is_configured(self): + return self.url and self.api_key + +class JellyfinClient(BaseClient): + def __init__(self): + super().__init__('jellyfin') + + def get_info(self): + if not self.is_configured(): return None + try: + headers = {'X-Emby-Token': self.api_key} + resp = requests.get(f"{self.url}/System/Info", headers=headers, timeout=5) + if resp.status_code == 200: + return resp.json() + except: + pass + return None + + def get_recent_items(self, limit=5): + if not self.is_configured(): return [] + try: + headers = {'X-Emby-Token': self.api_key} + # Get latest media + resp = requests.get(f"{self.url}/Users/Public/Items/Latest?Limit={limit}", headers=headers, timeout=5) + if resp.status_code == 200: + return resp.json() + except: + pass + return [] + +class RadarrClient(BaseClient): + def __init__(self): + super().__init__('radarr') + + def get_status(self): + if not self.is_configured(): return None + try: + params = {'apikey': self.api_key} + resp = requests.get(f"{self.url}/api/v3/system/status", params=params, timeout=5) + if resp.status_code == 200: + return resp.json() + except: + pass + return None + + def get_queue(self): + if not self.is_configured(): return [] + try: + params = {'apikey': self.api_key} + resp = requests.get(f"{self.url}/api/v3/queue", params=params, timeout=5) + if resp.status_code == 200: + return resp.json().get('records', []) + except: + pass + return [] + +class SonarrClient(BaseClient): + def __init__(self): + super().__init__('sonarr') + + def get_status(self): + if not self.is_configured(): return None + try: + params = {'apikey': self.api_key} + resp = requests.get(f"{self.url}/api/v3/system/status", params=params, timeout=5) + if resp.status_code == 200: + return resp.json() + except: + pass + return None + + def get_queue(self): + if not self.is_configured(): return [] + try: + params = {'apikey': self.api_key} + resp = requests.get(f"{self.url}/api/v3/queue", params=params, timeout=5) + if resp.status_code == 200: + return resp.json().get('records', []) + except: + pass + return [] + +class JellyseerrClient(BaseClient): + def __init__(self): + super().__init__('jellyseerr') + + def get_status(self): + if not self.is_configured(): return None + try: + headers = {'X-Api-Key': self.api_key} + resp = requests.get(f"{self.url}/api/v1/status", headers=headers, timeout=5) + if resp.status_code == 200: + return resp.json() + except: + pass + return None + + def get_requests(self, count=5): + if not self.is_configured(): return [] + try: + headers = {'X-Api-Key': self.api_key} + resp = requests.get(f"{self.url}/api/v1/request?take={count}&skip=0&filter=all", headers=headers, timeout=5) + if resp.status_code == 200: + return resp.json().get('results', []) + except: + pass + return [] \ No newline at end of file diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..82ecd10 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-02-06 12:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ServiceSetting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(choices=[('jellyfin', 'Jellyfin'), ('radarr', 'Radarr'), ('sonarr', 'Sonarr'), ('jellyseerr', 'Jellyseerr')], max_length=50, unique=True)), + ('url', models.URLField(help_text='Base URL of the service (e.g., http://192.168.1.100:8096)')), + ('api_key', models.CharField(blank=True, help_text='API Key or Token for the service', max_length=255, null=True)), + ('is_active', models.BooleanField(default=True)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..7a97750 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..e5867a0 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,17 @@ from django.db import models -# Create your models here. +class ServiceSetting(models.Model): + SERVICE_CHOICES = [ + ('jellyfin', 'Jellyfin'), + ('radarr', 'Radarr'), + ('sonarr', 'Sonarr'), + ('jellyseerr', 'Jellyseerr'), + ] + + name = models.CharField(max_length=50, choices=SERVICE_CHOICES, unique=True) + url = models.URLField(help_text="Base URL of the service (e.g., http://192.168.1.100:8096)") + api_key = models.CharField(max_length=255, blank=True, null=True, help_text="API Key or Token for the service") + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.get_name_display() \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..c3be64e 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,130 @@ - + - {% block title %}Knowledge Base{% endblock %} + + {% block title %}Matzeflix Dashboard{% endblock %} + + {% if project_description %} - - - {% endif %} - {% if project_image_url %} - - {% endif %} {% load static %} - + + + + + + {% block head %}{% endblock %} - {% block content %}{% endblock %} + + +
+ {% block content %}{% endblock %} +
+ + + + + - + \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..cd3630b 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,193 @@ -{% extends "base.html" %} +{% extends 'base.html' %} -{% block title %}{{ project_name }}{% endblock %} +{% block title %}Dashboard - Matzeflix{% endblock %} {% block head %} - - - + + {% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

Dashboard

+

Welcome to your Matzeflix control center.

-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file +
+ + +
+ + + +
+ {% for service, data in service_status.items %} +
+
+
+
+
+ {% if service == 'jellyfin' %} + + {% elif service == 'radarr' %} + + {% elif service == 'sonarr' %} + + {% elif service == 'jellyseerr' %} + + {% endif %} +
+ {% if data.status == 'Online' %} + ONLINE + {% elif data.status == 'Not Configured' %} + UNCONFIGURED + {% else %} + OFFLINE + {% endif %} +
+
{{ service }}
+
+ {% if service == 'radarr' %} + {{ radarr_queue_count }} in Queue + {% elif service == 'sonarr' %} + {{ sonarr_queue_count }} in Queue + {% else %} + {{ data.status }} + {% endif %} +
+

{{ data.details|default:"No additional info" }}

+
+ {% if data.url %} + + {% endif %} +
+
+ {% endfor %} +
+ + +{% if recent_media %} +
+
+

Recently Added

+ View All +
+
+ {% for item in recent_media %} +
+
+ {% if item.ImageTags.Primary %} + {{ item.Name }} + {% else %} +
+ +
+ {% endif %} +
+

{{ item.Name }}

+

{{ item.ProductionYear|default:"" }}

+
+
+
+ {% endfor %} +
+
+{% endif %} + +
+ +
+
+
+
Pending Requests
+
+
+ {% if pending_requests %} +
    + {% for req in pending_requests %} +
  • +
    +
    +
    {{ req.media.title|default:req.media.name }}
    +

    Requested by {{ req.requestedBy.displayName }}

    +
    + + {% if req.status == 1 %}Pending{% elif req.status == 2 %}Approved{% else %}Processing{% endif %} + +
    +
  • + {% endfor %} +
+ {% else %} +
+ +

No pending requests

+
+ {% endif %} +
+
+
+ + +
+
+
+ +

Ready to Stream?

+

Use the cast button in the header or your mobile app to send media directly to your TV.

+
+ Pro Tip: + For the best experience, ensure your server is connected via Ethernet and your playback device is on a 5GHz Wi-Fi band. +
+
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/settings.html b/core/templates/core/settings.html new file mode 100644 index 0000000..fd21708 --- /dev/null +++ b/core/templates/core/settings.html @@ -0,0 +1,91 @@ +{% extends 'base.html' %} + +{% block title %}Settings - Matzeflix{% endblock %} + +{% block content %} +
+
+

Settings

+

Configure your service connections and API keys.

+
+
+ +
+
+
+
+
+ {% csrf_token %} + + {% for config in service_configs %} +
+
+
+ {% if config.name == 'jellyfin' %} + {% elif config.name == 'radarr' %} + {% elif config.name == 'sonarr' %} + {% elif config.name == 'jellyseerr' %} + {% endif %} +
+
+

{{ config.name }}

+ Configuration for {{ config.name|capfirst }} +
+
+ +
+
+ + +
+ +
+ +
+ + +
+
+ +
+
+ + +
+
+
+
+ {% endfor %} + +
+ + + Return to Dashboard + +
+
+
+
+
+
+ + +{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..fc575b0 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,7 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), -] + path('', views.home, name='home'), + path('settings/', views.settings_view, name='settings'), +] \ No newline at end of file diff --git a/core/utils.py b/core/utils.py new file mode 100644 index 0000000..b9f2397 --- /dev/null +++ b/core/utils.py @@ -0,0 +1,50 @@ +import requests +from .models import ServiceSetting +from .api_clients import JellyfinClient, RadarrClient, SonarrClient, JellyseerrClient + +def check_service_status(service_name): + try: + setting = ServiceSetting.objects.get(name=service_name, is_active=True) + if not setting.url: + return {"status": "Not Configured", "details": None} + + # Detailed check + if service_name == 'jellyfin': + client = JellyfinClient() + info = client.get_info() + if info: + return {"status": "Online", "details": f"Version: {info.get('Version')}"} + elif service_name == 'radarr': + client = RadarrClient() + info = client.get_status() + if info: + return {"status": "Online", "details": f"Version: {info.get('version')}"} + elif service_name == 'sonarr': + client = SonarrClient() + info = client.get_status() + if info: + return {"status": "Online", "details": f"Version: {info.get('version')}"} + elif service_name == 'jellyseerr': + client = JellyseerrClient() + info = client.get_status() + if info: + return {"status": "Online", "details": f"Version: {info.get('version')}"} + + # Fallback to simple ping + response = requests.get(setting.url, timeout=5) + if response.status_code < 400: + return {"status": "Online", "details": "Reachable (No API details)"} + else: + return {"status": "Error", "details": f"Status: {response.status_code}"} + + except ServiceSetting.DoesNotExist: + return {"status": "Not Configured", "details": None} + except Exception as e: + return {"status": "Offline", "details": str(e)} + +def get_all_services_status(): + services = ['jellyfin', 'radarr', 'sonarr', 'jellyseerr'] + status_map = {} + for service in services: + status_map[service] = check_service_status(service) + return status_map \ No newline at end of file diff --git a/core/views.py b/core/views.py index c9aed12..2ff257d 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,73 @@ import os import platform - -from django import get_version as django_version -from django.shortcuts import render +from django.shortcuts import render, redirect from django.utils import timezone - +from .utils import get_all_services_status +from .models import ServiceSetting +from .api_clients import JellyfinClient, RadarrClient, SonarrClient, JellyseerrClient def home(request): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() - + """Render the dashboard with service statuses and detailed info.""" + service_status = get_all_services_status() + + # Fetch additional data for the dashboard + jellyfin = JellyfinClient() + recent_media = jellyfin.get_recent_items(6) + + radarr = RadarrClient() + radarr_queue = radarr.get_queue() + + sonarr = SonarrClient() + sonarr_queue = sonarr.get_queue() + + jellyseerr = JellyseerrClient() + pending_requests = jellyseerr.get_requests(5) + + # Add URLs to the context for links + settings = {s.name: s.url for s in ServiceSetting.objects.all()} + for service in service_status: + service_status[service]['url'] = settings.get(service, '') + context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "project_name": "Matzeflix Dashboard", + "service_status": service_status, + "recent_media": recent_media, + "radarr_queue_count": len(radarr_queue), + "sonarr_queue_count": len(sonarr_queue), + "pending_requests": pending_requests, + "current_time": timezone.now(), + "jellyfin_url": settings.get('jellyfin', ''), } return render(request, "core/index.html", context) + +def settings_view(request): + """View to manage service settings.""" + services = ['jellyfin', 'radarr', 'sonarr', 'jellyseerr'] + + if request.method == "POST": + for service in services: + url = request.POST.get(f"{service}_url") + api_key = request.POST.get(f"{service}_api_key") + is_active = request.POST.get(f"{service}_active") == "on" + + setting, created = ServiceSetting.objects.get_or_create(name=service) + setting.url = url or "" + setting.api_key = api_key or "" + setting.is_active = is_active + setting.save() + return redirect('home') + + settings_query = ServiceSetting.objects.all() + settings_dict = {s.name: s for s in settings_query} + + service_configs = [] + for service in services: + config = settings_dict.get(service) + if not config: + config = {'name': service, 'url': '', 'api_key': '', 'is_active': False} + service_configs.append(config) + + context = { + "service_configs": service_configs, + } + return render(request, "core/settings.html", context) diff --git a/requirements.txt b/requirements.txt index e22994c..081e3cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Django==5.2.7 mysqlclient==2.2.7 python-dotenv==1.1.1 +requests diff --git a/static/js/sw.js b/static/js/sw.js new file mode 100644 index 0000000..44e9410 --- /dev/null +++ b/static/js/sw.js @@ -0,0 +1,14 @@ +self.addEventListener('install', (e) => { + e.waitUntil( + caches.open('matzeflix-v1').then((cache) => cache.addAll([ + '/', + '/settings/', + ])), + ); +}); + +self.addEventListener('fetch', (e) => { + e.respondWith( + caches.match(e.request).then((response) => response || fetch(e.request)), + ); +}); diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..17e47b5 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Matzeflix Dashboard", + "short_name": "Matzeflix", + "description": "Control center for Jellyfin, Radarr, Sonarr and Jellyseerr", + "start_url": "/", + "display": "standalone", + "background_color": "#141414", + "theme_color": "#e50914", + "icons": [ + { + "src": "https://cdn-icons-png.flaticon.com/512/711/711245.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/staticfiles/js/sw.js b/staticfiles/js/sw.js new file mode 100644 index 0000000..44e9410 --- /dev/null +++ b/staticfiles/js/sw.js @@ -0,0 +1,14 @@ +self.addEventListener('install', (e) => { + e.waitUntil( + caches.open('matzeflix-v1').then((cache) => cache.addAll([ + '/', + '/settings/', + ])), + ); +}); + +self.addEventListener('fetch', (e) => { + e.respondWith( + caches.match(e.request).then((response) => response || fetch(e.request)), + ); +}); diff --git a/staticfiles/manifest.json b/staticfiles/manifest.json new file mode 100644 index 0000000..17e47b5 --- /dev/null +++ b/staticfiles/manifest.json @@ -0,0 +1,17 @@ +{ + "name": "Matzeflix Dashboard", + "short_name": "Matzeflix", + "description": "Control center for Jellyfin, Radarr, Sonarr and Jellyseerr", + "start_url": "/", + "display": "standalone", + "background_color": "#141414", + "theme_color": "#e50914", + "icons": [ + { + "src": "https://cdn-icons-png.flaticon.com/512/711/711245.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +}