diff --git a/apps/proxy/config.py b/apps/proxy/config.py index bb53beba..ca246b78 100644 --- a/apps/proxy/config.py +++ b/apps/proxy/config.py @@ -14,18 +14,24 @@ class BaseConfig: @classmethod def get_proxy_settings(cls): - """Get ProxySettings from database with fallback to defaults""" + """Get proxy settings from CoreSettings JSON data with fallback to defaults""" try: - from core.models import ProxySettings - return ProxySettings.objects.first() + from core.models import CoreSettings + return CoreSettings.get_proxy_settings() except Exception: - return None + return { + "buffering_timeout": 15, + "buffering_speed": 1.0, + "redis_chunk_ttl": 60, + "channel_shutdown_delay": 0, + "channel_init_grace_period": 5, + } @classmethod def get_redis_chunk_ttl(cls): """Get Redis chunk TTL from database or default""" settings = cls.get_proxy_settings() - return settings.redis_chunk_ttl if settings else 60 + return settings.get("redis_chunk_ttl", 60) @property def REDIS_CHUNK_TTL(self): @@ -79,25 +85,25 @@ class TSConfig(BaseConfig): def get_channel_shutdown_delay(cls): """Get channel shutdown delay from database or default""" settings = cls.get_proxy_settings() - return settings.channel_shutdown_delay if settings else 0 + return settings.get("channel_shutdown_delay", 0) @classmethod def get_buffering_timeout(cls): """Get buffering timeout from database or default""" settings = cls.get_proxy_settings() - return settings.buffering_timeout if settings else 15 + return settings.get("buffering_timeout", 15) @classmethod def get_buffering_speed(cls): """Get buffering speed threshold from database or default""" settings = cls.get_proxy_settings() - return settings.buffering_speed if settings else 1.0 + return settings.get("buffering_speed", 1.0) @classmethod def get_channel_init_grace_period(cls): """Get channel init grace period from database or default""" settings = cls.get_proxy_settings() - return settings.channel_init_grace_period if settings else 5 + return settings.get("channel_init_grace_period", 5) # Dynamic property access for these settings @property diff --git a/core/api_views.py b/core/api_views.py index 01bd3f5a..04e39f11 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -15,7 +15,7 @@ from .models import ( CoreSettings, STREAM_HASH_KEY, NETWORK_ACCESS, - ProxySettings, + PROXY_SETTINGS_KEY, ) from .serializers import ( UserAgentSerializer, @@ -112,62 +112,85 @@ class CoreSettingsViewSet(viewsets.ModelViewSet): return Response({}, status=status.HTTP_200_OK) -class ProxySettingsViewSet(viewsets.ModelViewSet): +class ProxySettingsViewSet(viewsets.ViewSet): """ - API endpoint for proxy settings. - This is treated as a singleton: only one instance should exist. + API endpoint for proxy settings stored as JSON in CoreSettings. """ serializer_class = ProxySettingsSerializer - def get_queryset(self): - # Always return the singleton settings - return ProxySettings.objects.all() + def _get_or_create_settings(self): + """Get or create the proxy settings CoreSettings entry""" + try: + settings_obj = CoreSettings.objects.get(key=PROXY_SETTINGS_KEY) + settings_data = json.loads(settings_obj.value) + except (CoreSettings.DoesNotExist, json.JSONDecodeError): + # Create default settings + settings_data = { + "buffering_timeout": 15, + "buffering_speed": 1.0, + "redis_chunk_ttl": 60, + "channel_shutdown_delay": 0, + "channel_init_grace_period": 5, + } + settings_obj, created = CoreSettings.objects.get_or_create( + key=PROXY_SETTINGS_KEY, + defaults={ + "name": "Proxy Settings", + "value": json.dumps(settings_data) + } + ) + return settings_obj, settings_data - def get_object(self): - # Always return the singleton settings (create if doesn't exist) - return ProxySettings.get_settings() - - def list(self, request, *args, **kwargs): - # Return the singleton settings as a single object - settings = self.get_object() - serializer = self.get_serializer(settings) + def list(self, request): + """Return proxy settings""" + settings_obj, settings_data = self._get_or_create_settings() + serializer = ProxySettingsSerializer(data=settings_data) + serializer.is_valid() return Response(serializer.data) - def retrieve(self, request, *args, **kwargs): - # Always return the singleton settings regardless of ID - settings = self.get_object() - serializer = self.get_serializer(settings) + def retrieve(self, request, pk=None): + """Return proxy settings regardless of ID""" + settings_obj, settings_data = self._get_or_create_settings() + serializer = ProxySettingsSerializer(data=settings_data) + serializer.is_valid() return Response(serializer.data) - def update(self, request, *args, **kwargs): - # Update the singleton settings - settings = self.get_object() - serializer = self.get_serializer(settings, data=request.data, partial=True) + def update(self, request, pk=None): + """Update proxy settings""" + settings_obj, current_data = self._get_or_create_settings() + + serializer = ProxySettingsSerializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.save() + + # Update the JSON data + settings_obj.value = json.dumps(serializer.validated_data) + settings_obj.save() + return Response(serializer.data) - def partial_update(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) + def partial_update(self, request, pk=None): + """Partially update proxy settings""" + settings_obj, current_data = self._get_or_create_settings() + + # Merge current data with new data + updated_data = {**current_data, **request.data} + + serializer = ProxySettingsSerializer(data=updated_data) + serializer.is_valid(raise_exception=True) + + # Update the JSON data + settings_obj.value = json.dumps(serializer.validated_data) + settings_obj.save() + + return Response(serializer.data) @action(detail=False, methods=['get', 'patch']) def settings(self, request): - """ - Get or update the proxy settings. - """ - settings = self.get_object() - + """Get or update the proxy settings.""" if request.method == 'GET': - # Return current settings - serializer = self.get_serializer(settings) - return Response(serializer.data) - + return self.list(request) elif request.method == 'PATCH': - # Update settings - serializer = self.get_serializer(settings, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data) + return self.partial_update(request) diff --git a/core/migrations/0014_proxysettings.py b/core/migrations/0014_proxysettings.py deleted file mode 100644 index 75a3096f..00000000 --- a/core/migrations/0014_proxysettings.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 5.1.6 on 2025-06-12 15:44 - -from django.db import migrations, models - - -def create_default_proxy_settings(apps, schema_editor): - """Create the default ProxySettings instance""" - ProxySettings = apps.get_model("core", "ProxySettings") - ProxySettings.objects.create( - id=1, # Force singleton ID - buffering_timeout=15, - buffering_speed=1.0, - redis_chunk_ttl=60, - channel_shutdown_delay=0, - channel_init_grace_period=5, - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0013_default_network_access_settings'), - ] - - operations = [ - migrations.CreateModel( - name='ProxySettings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('buffering_timeout', models.IntegerField(default=15, help_text='Seconds to wait for buffering before switching streams')), - ('buffering_speed', models.FloatField(default=1.0, help_text='Speed threshold to consider stream buffering (1.0 = normal speed)')), - ('redis_chunk_ttl', models.IntegerField(default=60, help_text='Time in seconds before Redis chunks expire')), - ('channel_shutdown_delay', models.IntegerField(default=0, help_text='Seconds to wait after last client before shutting down channel')), - ('channel_init_grace_period', models.IntegerField(default=5, help_text='Seconds to wait for first client after channel initialization')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'Proxy Settings', - 'verbose_name_plural': 'Proxy Settings', - }, - ), - migrations.RunPython(create_default_proxy_settings), - ] diff --git a/core/models.py b/core/models.py index 57c76553..557776c2 100644 --- a/core/models.py +++ b/core/models.py @@ -149,6 +149,7 @@ STREAM_HASH_KEY = slugify("M3U Hash Key") PREFERRED_REGION_KEY = slugify("Preferred Region") AUTO_IMPORT_MAPPED_FILES = slugify("Auto-Import Mapped Files") NETWORK_ACCESS = slugify("Network Access") +PROXY_SETTINGS_KEY = slugify("Proxy Settings") class CoreSettings(models.Model): @@ -195,55 +196,19 @@ class CoreSettings(models.Model): except cls.DoesNotExist: return None -class ProxySettings(models.Model): - """Proxy configuration settings""" - - buffering_timeout = models.IntegerField( - default=15, - help_text="Seconds to wait for buffering before switching streams" - ) - - buffering_speed = models.FloatField( - default=1.0, - help_text="Speed threshold to consider stream buffering (1.0 = normal speed)" - ) - - redis_chunk_ttl = models.IntegerField( - default=60, - help_text="Time in seconds before Redis chunks expire" - ) - - channel_shutdown_delay = models.IntegerField( - default=0, - help_text="Seconds to wait after last client before shutting down channel" - ) - - channel_init_grace_period = models.IntegerField( - default=5, - help_text="Seconds to wait for first client after channel initialization" - ) - - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - verbose_name = "Proxy Settings" - verbose_name_plural = "Proxy Settings" - - def __str__(self): - return "Proxy Settings" - @classmethod - def get_settings(cls): - """Get or create the singleton proxy settings instance""" - settings, created = cls.objects.get_or_create( - pk=1, # Force single instance - defaults={ - 'buffering_timeout': 15, - 'buffering_speed': 1.0, - 'redis_chunk_ttl': 60, - 'channel_shutdown_delay': 0, - 'channel_init_grace_period': 20, + def get_proxy_settings(cls): + """Retrieve proxy settings as dict (or return defaults if not found).""" + try: + import json + settings_json = cls.objects.get(key=PROXY_SETTINGS_KEY).value + return json.loads(settings_json) + except (cls.DoesNotExist, json.JSONDecodeError): + # Return defaults if not found or invalid JSON + return { + "buffering_timeout": 15, + "buffering_speed": 1.0, + "redis_chunk_ttl": 60, + "channel_shutdown_delay": 0, + "channel_init_grace_period": 5, } - ) - return settings diff --git a/core/serializers.py b/core/serializers.py index fcc813fe..c6029bc4 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -3,7 +3,7 @@ import json import ipaddress from rest_framework import serializers -from .models import CoreSettings, UserAgent, StreamProfile, ProxySettings, NETWORK_ACCESS +from .models import CoreSettings, UserAgent, StreamProfile, NETWORK_ACCESS class UserAgentSerializer(serializers.ModelSerializer): @@ -66,24 +66,17 @@ class CoreSettingsSerializer(serializers.ModelSerializer): return super().update(instance, validated_data) -class ProxySettingsSerializer(serializers.ModelSerializer): - class Meta: - model = ProxySettings - fields = [ - 'id', - 'buffering_timeout', - 'buffering_speed', - 'redis_chunk_ttl', - 'channel_shutdown_delay', - 'channel_init_grace_period', - 'created_at', - 'updated_at' - ] - read_only_fields = ['id', 'created_at', 'updated_at'] +class ProxySettingsSerializer(serializers.Serializer): + """Serializer for proxy settings stored as JSON in CoreSettings""" + buffering_timeout = serializers.IntegerField(min_value=0, max_value=300) + buffering_speed = serializers.FloatField(min_value=0.1, max_value=10.0) + redis_chunk_ttl = serializers.IntegerField(min_value=10, max_value=3600) + channel_shutdown_delay = serializers.IntegerField(min_value=0, max_value=300) + channel_init_grace_period = serializers.IntegerField(min_value=0, max_value=60) def validate_buffering_timeout(self, value): - if value < 1 or value > 300: - raise serializers.ValidationError("Buffering timeout must be between 1 and 300 seconds") + if value < 0 or value > 300: + raise serializers.ValidationError("Buffering timeout must be between 0 and 300 seconds") return value def validate_buffering_speed(self, value): @@ -102,6 +95,6 @@ class ProxySettingsSerializer(serializers.ModelSerializer): return value def validate_channel_init_grace_period(self, value): - if value < 1 or value > 60: - raise serializers.ValidationError("Channel init grace period must be between 1 and 60 seconds") + if value < 0 or value > 60: + raise serializers.ValidationError("Channel init grace period must be between 0 and 60 seconds") return value diff --git a/dispatcharr/settings.py b/dispatcharr/settings.py index 63b88c7c..acac4c1a 100644 --- a/dispatcharr/settings.py +++ b/dispatcharr/settings.py @@ -254,7 +254,6 @@ PROXY_SETTINGS = { "BUFFER_SIZE": 1000, "RECONNECT_DELAY": 5, "USER_AGENT": "VLC/3.0.20 LibVLC/3.0.20", - "REDIS_CHUNK_TTL": 60, # How long to keep chunks in Redis (seconds) }, }