From 7812a410b3dd78e09769a7e4fc366cd26942791c Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 10 Jun 2025 21:17:30 -0500 Subject: [PATCH 01/19] Allow users to change proxy settings. --- apps/proxy/ts_proxy/config_helper.py | 46 ++++++++++++++++++++- core/api_views.py | 62 +++++++++++++++++++++++++++- core/models.py | 38 +++++++++++++++++ core/serializers.py | 42 ++++++++++++++++++- 4 files changed, 183 insertions(+), 5 deletions(-) diff --git a/apps/proxy/ts_proxy/config_helper.py b/apps/proxy/ts_proxy/config_helper.py index 4057a2d5..28474b37 100644 --- a/apps/proxy/ts_proxy/config_helper.py +++ b/apps/proxy/ts_proxy/config_helper.py @@ -34,6 +34,13 @@ class ConfigHelper: @staticmethod def channel_shutdown_delay(): """Get channel shutdown delay in seconds""" + try: + from core.models import ProxySettings + settings = ProxySettings.objects.first() + if settings: + return settings.channel_shutdown_delay + except: + pass return ConfigHelper.get('CHANNEL_SHUTDOWN_DELAY', 0) @staticmethod @@ -54,6 +61,13 @@ class ConfigHelper: @staticmethod def redis_chunk_ttl(): """Get Redis chunk TTL in seconds""" + try: + from core.models import ProxySettings + settings = ProxySettings.objects.first() + if settings: + return settings.redis_chunk_ttl + except: + pass return ConfigHelper.get('REDIS_CHUNK_TTL', 60) @staticmethod @@ -85,11 +99,39 @@ class ConfigHelper: def failover_grace_period(): """Get extra time (in seconds) to allow for stream switching before disconnecting clients""" return ConfigHelper.get('FAILOVER_GRACE_PERIOD', 20) # Default to 20 seconds + @staticmethod def buffering_timeout(): """Get buffering timeout in seconds""" + try: + from core.models import ProxySettings + settings = ProxySettings.objects.first() + if settings: + return settings.buffering_timeout + except: + pass return ConfigHelper.get('BUFFERING_TIMEOUT', 15) # Default to 15 seconds + @staticmethod def buffering_speed(): - """Get buffering speed in bytes per second""" - return ConfigHelper.get('BUFFERING_SPEED',1) # Default to 1x + """Get buffering speed threshold""" + try: + from core.models import ProxySettings + settings = ProxySettings.objects.first() + if settings: + return settings.buffering_speed + except: + pass + return ConfigHelper.get('BUFFERING_SPEED', 1) # Default to 1x + + @staticmethod + def channel_init_grace_period(): + """Get channel initialization grace period in seconds""" + try: + from core.models import ProxySettings + settings = ProxySettings.objects.first() + if settings: + return settings.channel_init_grace_period + except: + pass + return ConfigHelper.get('CHANNEL_INIT_GRACE_PERIOD', 5) # Default to 5 seconds diff --git a/core/api_views.py b/core/api_views.py index 77473b5d..84eb4918 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -1,10 +1,11 @@ # core/api_views.py from rest_framework import viewsets, status +from rest_framework.decorators import action from rest_framework.response import Response from django.shortcuts import get_object_or_404 -from .models import UserAgent, StreamProfile, CoreSettings, STREAM_HASH_KEY -from .serializers import UserAgentSerializer, StreamProfileSerializer, CoreSettingsSerializer +from .models import UserAgent, StreamProfile, CoreSettings, ProxySettings, STREAM_HASH_KEY +from .serializers import UserAgentSerializer, StreamProfileSerializer, CoreSettingsSerializer, ProxySettingsSerializer from rest_framework.permissions import IsAuthenticated from rest_framework.decorators import api_view, permission_classes from drf_yasg.utils import swagger_auto_schema @@ -44,6 +45,63 @@ class CoreSettingsViewSet(viewsets.ModelViewSet): return response +class ProxySettingsViewSet(viewsets.ModelViewSet): + """ + API endpoint for proxy settings. + This is treated as a singleton: only one instance should exist. + """ + serializer_class = ProxySettingsSerializer + + def get_queryset(self): + # Always return the singleton settings + return ProxySettings.objects.all() + + 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) + 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) + 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) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + def partial_update(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + @action(detail=False, methods=['get', 'patch']) + def settings(self, request): + """ + Get or update the proxy settings. + """ + settings = self.get_object() + + if request.method == 'GET': + # Return current settings + serializer = self.get_serializer(settings) + return Response(serializer.data) + + 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) + @swagger_auto_schema( method='get', operation_description="Endpoint for environment details", diff --git a/core/models.py b/core/models.py index fe7e9eb5..3af21628 100644 --- a/core/models.py +++ b/core/models.py @@ -183,3 +183,41 @@ class CoreSettings(models.Model): return cls.objects.get(key=AUTO_IMPORT_MAPPED_FILES).value 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" diff --git a/core/serializers.py b/core/serializers.py index c80ad630..4648a74a 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,7 +1,7 @@ # core/serializers.py from rest_framework import serializers -from .models import UserAgent, StreamProfile, CoreSettings +from .models import CoreSettings, UserAgent, StreamProfile, ProxySettings class UserAgentSerializer(serializers.ModelSerializer): class Meta: @@ -17,3 +17,43 @@ class CoreSettingsSerializer(serializers.ModelSerializer): class Meta: model = CoreSettings fields = '__all__' + +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'] + + def validate_buffering_timeout(self, value): + if value < 1 or value > 300: + raise serializers.ValidationError("Buffering timeout must be between 1 and 300 seconds") + return value + + def validate_buffering_speed(self, value): + if value < 0.1 or value > 10.0: + raise serializers.ValidationError("Buffering speed must be between 0.1 and 10.0") + return value + + def validate_redis_chunk_ttl(self, value): + if value < 10 or value > 3600: + raise serializers.ValidationError("Redis chunk TTL must be between 10 and 3600 seconds") + return value + + def validate_channel_shutdown_delay(self, value): + if value < 0 or value > 300: + raise serializers.ValidationError("Channel shutdown delay must be between 0 and 300 seconds") + 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") + return value From 2add2c1dd21c8faf0ea30c36e0f814a65777a3be Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Tue, 10 Jun 2025 21:23:04 -0500 Subject: [PATCH 02/19] Add new settings to database. --- core/migrations/0013_proxysettings.py | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 core/migrations/0013_proxysettings.py diff --git a/core/migrations/0013_proxysettings.py b/core/migrations/0013_proxysettings.py new file mode 100644 index 00000000..1a69c4ef --- /dev/null +++ b/core/migrations/0013_proxysettings.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.6 on 2025-06-11 02:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0012_default_active_m3u_accounts'), + ] + + 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', + }, + ), + ] From a99a6431b2a258254a76883c07ad9cacbad59285 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 12 Jun 2025 10:45:57 -0500 Subject: [PATCH 03/19] More merge fixes. --- core/api_views.py | 13 +++---------- ...{0013_proxysettings.py => 0014_proxysettings.py} | 4 ++-- 2 files changed, 5 insertions(+), 12 deletions(-) rename core/migrations/{0013_proxysettings.py => 0014_proxysettings.py} (92%) diff --git a/core/api_views.py b/core/api_views.py index db9725ee..01bd3f5a 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -4,13 +4,11 @@ import json import ipaddress import logging from rest_framework import viewsets, status -from rest_framework.decorators import action from rest_framework.response import Response from django.shortcuts import get_object_or_404 -<<<<<<< HEAD from rest_framework.permissions import IsAuthenticated -from rest_framework.decorators import api_view, permission_classes -======= +from rest_framework.decorators import api_view, permission_classes, action +from drf_yasg.utils import swagger_auto_schema from .models import ( UserAgent, StreamProfile, @@ -25,9 +23,7 @@ from .serializers import ( CoreSettingsSerializer, ProxySettingsSerializer, ) -from rest_framework.decorators import api_view, permission_classes, action ->>>>>>> 59e4a28b311d00d073f238e01e735d68a821c3f3 -from drf_yasg.utils import swagger_auto_schema + import socket import requests import os @@ -116,7 +112,6 @@ class CoreSettingsViewSet(viewsets.ModelViewSet): return Response({}, status=status.HTTP_200_OK) -<<<<<<< HEAD class ProxySettingsViewSet(viewsets.ModelViewSet): """ API endpoint for proxy settings. @@ -173,10 +168,8 @@ class ProxySettingsViewSet(viewsets.ModelViewSet): serializer.is_valid(raise_exception=True) serializer.save() return Response(serializer.data) -======= ->>>>>>> 59e4a28b311d00d073f238e01e735d68a821c3f3 @swagger_auto_schema( method="get", diff --git a/core/migrations/0013_proxysettings.py b/core/migrations/0014_proxysettings.py similarity index 92% rename from core/migrations/0013_proxysettings.py rename to core/migrations/0014_proxysettings.py index 1a69c4ef..1f50adea 100644 --- a/core/migrations/0013_proxysettings.py +++ b/core/migrations/0014_proxysettings.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.6 on 2025-06-11 02:22 +# Generated by Django 5.1.6 on 2025-06-12 15:44 from django.db import migrations, models @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('core', '0012_default_active_m3u_accounts'), + ('core', '0013_default_network_access_settings'), ] operations = [ From b4ae6911c95cb4c2217cb80696f2eeb213d9e418 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 12 Jun 2025 15:42:26 -0500 Subject: [PATCH 04/19] Pull settings from database --- apps/proxy/config.py | 70 ++++++++++++++++++++++++---- apps/proxy/ts_proxy/config_helper.py | 45 ++---------------- core/models.py | 15 ++++++ 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/apps/proxy/config.py b/apps/proxy/config.py index b369a92f..bb53beba 100644 --- a/apps/proxy/config.py +++ b/apps/proxy/config.py @@ -9,11 +9,28 @@ class BaseConfig: CONNECTION_TIMEOUT = 10 # seconds to wait for initial connection MAX_STREAM_SWITCHES = 10 # Maximum number of stream switch attempts before giving up BUFFER_CHUNK_SIZE = 188 * 1361 # ~256KB - # Redis settings - REDIS_CHUNK_TTL = 60 # Number in seconds - Chunks expire after 1 minute BUFFERING_TIMEOUT = 15 # Seconds to wait for buffering before switching streams BUFFER_SPEED = 1 # What speed to condsider the stream buffering, 1x is normal speed, 2x is double speed, etc. + @classmethod + def get_proxy_settings(cls): + """Get ProxySettings from database with fallback to defaults""" + try: + from core.models import ProxySettings + return ProxySettings.objects.first() + except Exception: + return None + + @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 + + @property + def REDIS_CHUNK_TTL(self): + return self.get_redis_chunk_ttl() + class HLSConfig(BaseConfig): MIN_SEGMENTS = 12 MAX_SEGMENTS = 16 @@ -42,21 +59,14 @@ class TSConfig(BaseConfig): # Resource management CLEANUP_INTERVAL = 60 # Check for inactive channels every 60 seconds - CHANNEL_SHUTDOWN_DELAY = 0 # How long to wait after last client before shutdown (seconds) # Client tracking settings CLIENT_RECORD_TTL = 5 # How long client records persist in Redis (seconds). Client will be considered MIA after this time. CLEANUP_CHECK_INTERVAL = 1 # How often to check for disconnected clients (seconds) - CHANNEL_INIT_GRACE_PERIOD = 5 # How long to wait for first client after initialization (seconds) CLIENT_HEARTBEAT_INTERVAL = 1 # How often to send client heartbeats (seconds) GHOST_CLIENT_MULTIPLIER = 5.0 # How many heartbeat intervals before client considered ghost (5 would mean 5 secondsif heartbeat interval is 1) CLIENT_WAIT_TIMEOUT = 30 # Seconds to wait for client to connect - - # TS packets are 188 bytes - # Make chunk size a multiple of TS packet size for perfect alignment - # ~1MB is ideal for streaming (matches typical media buffer sizes) - # Stream health and recovery settings MAX_HEALTH_RECOVERY_ATTEMPTS = 2 # Maximum times to attempt recovery for a single stream MAX_RECONNECT_ATTEMPTS = 3 # Maximum reconnects to try before switching streams @@ -64,5 +74,47 @@ class TSConfig(BaseConfig): FAILOVER_GRACE_PERIOD = 20 # Extra time (seconds) to allow for stream switching before disconnecting clients URL_SWITCH_TIMEOUT = 20 # Max time allowed for a stream switch operation + # Database-dependent settings with fallbacks + @classmethod + 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 + + @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 + + @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 + + @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 + + # Dynamic property access for these settings + @property + def CHANNEL_SHUTDOWN_DELAY(self): + return self.get_channel_shutdown_delay() + + @property + def BUFFERING_TIMEOUT(self): + return self.get_buffering_timeout() + + @property + def BUFFERING_SPEED(self): + return self.get_buffering_speed() + + @property + def CHANNEL_INIT_GRACE_PERIOD(self): + return self.get_channel_init_grace_period() + diff --git a/apps/proxy/ts_proxy/config_helper.py b/apps/proxy/ts_proxy/config_helper.py index 28474b37..d59fa1f9 100644 --- a/apps/proxy/ts_proxy/config_helper.py +++ b/apps/proxy/ts_proxy/config_helper.py @@ -34,14 +34,7 @@ class ConfigHelper: @staticmethod def channel_shutdown_delay(): """Get channel shutdown delay in seconds""" - try: - from core.models import ProxySettings - settings = ProxySettings.objects.first() - if settings: - return settings.channel_shutdown_delay - except: - pass - return ConfigHelper.get('CHANNEL_SHUTDOWN_DELAY', 0) + return Config.get_channel_shutdown_delay() @staticmethod def initial_behind_chunks(): @@ -61,14 +54,7 @@ class ConfigHelper: @staticmethod def redis_chunk_ttl(): """Get Redis chunk TTL in seconds""" - try: - from core.models import ProxySettings - settings = ProxySettings.objects.first() - if settings: - return settings.redis_chunk_ttl - except: - pass - return ConfigHelper.get('REDIS_CHUNK_TTL', 60) + return Config.get_redis_chunk_ttl() @staticmethod def chunk_size(): @@ -103,35 +89,14 @@ class ConfigHelper: @staticmethod def buffering_timeout(): """Get buffering timeout in seconds""" - try: - from core.models import ProxySettings - settings = ProxySettings.objects.first() - if settings: - return settings.buffering_timeout - except: - pass - return ConfigHelper.get('BUFFERING_TIMEOUT', 15) # Default to 15 seconds + return Config.get_buffering_timeout() @staticmethod def buffering_speed(): """Get buffering speed threshold""" - try: - from core.models import ProxySettings - settings = ProxySettings.objects.first() - if settings: - return settings.buffering_speed - except: - pass - return ConfigHelper.get('BUFFERING_SPEED', 1) # Default to 1x + return Config.get_buffering_speed() @staticmethod def channel_init_grace_period(): """Get channel initialization grace period in seconds""" - try: - from core.models import ProxySettings - settings = ProxySettings.objects.first() - if settings: - return settings.channel_init_grace_period - except: - pass - return ConfigHelper.get('CHANNEL_INIT_GRACE_PERIOD', 5) # Default to 5 seconds + return Config.get_channel_init_grace_period() diff --git a/core/models.py b/core/models.py index dcb4a68f..57c76553 100644 --- a/core/models.py +++ b/core/models.py @@ -232,3 +232,18 @@ class ProxySettings(models.Model): 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, + } + ) + return settings From 2f91e0ce1c61acec2e63149133d9ab216612bee6 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 12 Jun 2025 16:02:08 -0500 Subject: [PATCH 05/19] Properly populate default values. --- core/migrations/0014_proxysettings.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/migrations/0014_proxysettings.py b/core/migrations/0014_proxysettings.py index 1f50adea..75a3096f 100644 --- a/core/migrations/0014_proxysettings.py +++ b/core/migrations/0014_proxysettings.py @@ -3,6 +3,19 @@ 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 = [ @@ -27,4 +40,5 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'Proxy Settings', }, ), + migrations.RunPython(create_default_proxy_settings), ] From 1e9ab5460909f45e131610fdb469712f6d89b814 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Thu, 12 Jun 2025 16:11:43 -0500 Subject: [PATCH 06/19] Use new methods for getting settings. --- apps/proxy/ts_proxy/server.py | 2 +- apps/proxy/ts_proxy/stream_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/proxy/ts_proxy/server.py b/apps/proxy/ts_proxy/server.py index 3d0a53d9..9061665e 100644 --- a/apps/proxy/ts_proxy/server.py +++ b/apps/proxy/ts_proxy/server.py @@ -941,7 +941,7 @@ class ProxyServer: # If waiting for clients, check grace period if connection_ready_time: - grace_period = ConfigHelper.get('CHANNEL_INIT_GRACE_PERIOD', 20) + grace_period = ConfigHelper.channel_init_grace_period() time_since_ready = time.time() - connection_ready_time # Add this debug log diff --git a/apps/proxy/ts_proxy/stream_manager.py b/apps/proxy/ts_proxy/stream_manager.py index a57f1384..06a00513 100644 --- a/apps/proxy/ts_proxy/stream_manager.py +++ b/apps/proxy/ts_proxy/stream_manager.py @@ -1240,7 +1240,7 @@ class StreamManager: redis_client.hset(metadata_key, mapping=update_data) # Get configured grace period or default - grace_period = ConfigHelper.get('CHANNEL_INIT_GRACE_PERIOD', 20) + grace_period = ConfigHelper.channel_init_grace_period() logger.info(f"STREAM MANAGER: Updated channel {channel_id} state: {current_state or 'None'} -> {ChannelState.WAITING_FOR_CLIENTS} with {current_buffer_index} buffer chunks") logger.info(f"Started initial connection grace period ({grace_period}s) for channel {channel_id}") else: From 8eec41cfbbf2a6d6d78a6849602e5f26e50c42d7 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 13 Jun 2025 10:27:51 -0500 Subject: [PATCH 07/19] Fixes a bug where heartbeat thread will exit if channel is in shutdown delay. This may also fix #129 --- apps/proxy/ts_proxy/client_manager.py | 35 ++++++++++++++++++++++----- apps/proxy/ts_proxy/server.py | 2 +- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/proxy/ts_proxy/client_manager.py b/apps/proxy/ts_proxy/client_manager.py index 98dbf072..bcc54602 100644 --- a/apps/proxy/ts_proxy/client_manager.py +++ b/apps/proxy/ts_proxy/client_manager.py @@ -4,6 +4,7 @@ import threading import logging import time import json +import gevent from typing import Set, Optional from apps.proxy.config import TSConfig as Config from redis.exceptions import ConnectionError, TimeoutError @@ -46,7 +47,7 @@ class ClientManager: while True: try: # Wait for the interval - time.sleep(self.heartbeat_interval) + gevent.sleep(self.heartbeat_interval) # Send heartbeat for all local clients with self.lock: @@ -54,13 +55,35 @@ class ClientManager: # No clients left, increment our counter no_clients_count += 1 - # If we've seen no clients for several consecutive checks, exit the thread - if no_clients_count >= max_empty_cycles: - logger.info(f"No clients for channel {self.channel_id} after {no_clients_count} consecutive checks, exiting heartbeat thread") + # Check if we're in a shutdown delay period before exiting + in_shutdown_delay = False + if self.redis_client: + try: + disconnect_key = RedisKeys.last_client_disconnect(self.channel_id) + disconnect_time_bytes = self.redis_client.get(disconnect_key) + if disconnect_time_bytes: + disconnect_time = float(disconnect_time_bytes.decode('utf-8')) + elapsed = time.time() - disconnect_time + shutdown_delay = ConfigHelper.channel_shutdown_delay() + + if elapsed < shutdown_delay: + in_shutdown_delay = True + logger.debug(f"Channel {self.channel_id} in shutdown delay: {elapsed:.1f}s of {shutdown_delay}s elapsed") + except Exception as e: + logger.debug(f"Error checking shutdown delay: {e}") + + # Only exit if we've seen no clients for several consecutive checks AND we're not in shutdown delay + if no_clients_count >= max_empty_cycles and not in_shutdown_delay: + logger.info(f"No clients for channel {self.channel_id} after {no_clients_count} consecutive checks and not in shutdown delay, exiting heartbeat thread") return # This exits the thread - # Skip this cycle if we have no clients - continue + # Skip this cycle if we have no clients but continue if in shutdown delay + if not in_shutdown_delay: + continue + else: + # Reset counter during shutdown delay to prevent premature exit + no_clients_count = 0 + continue else: # Reset counter when we see clients no_clients_count = 0 diff --git a/apps/proxy/ts_proxy/server.py b/apps/proxy/ts_proxy/server.py index 9061665e..bf5d4981 100644 --- a/apps/proxy/ts_proxy/server.py +++ b/apps/proxy/ts_proxy/server.py @@ -206,7 +206,7 @@ class ProxyServer: self.redis_client.setex(disconnect_key, 60, str(time.time())) # Get configured shutdown delay or default - shutdown_delay = getattr(Config, 'CHANNEL_SHUTDOWN_DELAY', 0) + shutdown_delay = ConfigHelper.channel_shutdown_delay() if shutdown_delay > 0: logger.info(f"Waiting {shutdown_delay}s before stopping channel...") From c4a6b1469ee9196a08c14bc584bd798f42508ac9 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 13 Jun 2025 14:36:08 -0500 Subject: [PATCH 08/19] Change to JSON settings --- apps/proxy/config.py | 24 +++--- core/api_views.py | 103 ++++++++++++++++---------- core/migrations/0014_proxysettings.py | 44 ----------- core/models.py | 65 ++++------------ core/serializers.py | 31 +++----- dispatcharr/settings.py | 1 - 6 files changed, 105 insertions(+), 163 deletions(-) delete mode 100644 core/migrations/0014_proxysettings.py 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) }, } From cfff51a9ebdb4a4bd698d136d74ed9624138d3fb Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 13 Jun 2025 14:40:28 -0500 Subject: [PATCH 09/19] Add missing import. --- core/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/models.py b/core/models.py index 557776c2..843a708c 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,7 @@ # core/models.py from django.db import models from django.utils.text import slugify +from django.core.exceptions import ValidationError class UserAgent(models.Model): From fa3ee35d4d8e2929bd93f8dd8afa085b3565cd78 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 13 Jun 2025 14:51:56 -0500 Subject: [PATCH 10/19] Prepopulate settings in database. --- .../migrations/0014_default_proxy_settings.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 core/migrations/0014_default_proxy_settings.py diff --git a/core/migrations/0014_default_proxy_settings.py b/core/migrations/0014_default_proxy_settings.py new file mode 100644 index 00000000..f4a61a9e --- /dev/null +++ b/core/migrations/0014_default_proxy_settings.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.6 on 2025-03-01 14:01 + +import json +from django.db import migrations +from django.utils.text import slugify + + +def preload_proxy_settings(apps, schema_editor): + CoreSettings = apps.get_model("core", "CoreSettings") + + # Default proxy settings + default_proxy_settings = { + "buffering_timeout": 15, + "buffering_speed": 1.0, + "redis_chunk_ttl": 60, + "channel_shutdown_delay": 0, + "channel_init_grace_period": 5, + } + + CoreSettings.objects.create( + key=slugify("Proxy Settings"), + name="Proxy Settings", + value=json.dumps(default_proxy_settings), + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0013_default_network_access_settings"), + ] + + operations = [ + migrations.RunPython(preload_proxy_settings), + ] From 72542bf0fd1d342b09f296d3cefc44fe30ae4e82 Mon Sep 17 00:00:00 2001 From: dekzter Date: Sat, 14 Jun 2025 08:52:56 -0400 Subject: [PATCH 11/19] default cidrs in network access form, added proxy settings to ui --- frontend/src/constants.js | 23 +++++++++ frontend/src/pages/Settings.jsx | 89 ++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/frontend/src/constants.js b/frontend/src/constants.js index f8077b12..89b081ae 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -29,3 +29,26 @@ export const NETWORK_ACCESS_OPTIONS = { description: 'Limit access to the Dispatcharr UI', }, }; + +export const PROXY_SETTINGS_OPTIONS = { + buffering_timeout: { + label: 'Buffering Timeout', + description: '', + }, + buffering_speed: { + label: 'Buffering Speed', + description: '', + }, + redis_chunk_ttl: { + label: 'Redis Chunk TTL', + description: '', + }, + channel_shutdown_delay: { + label: 'Channel Shutdown Delay', + description: '', + }, + channel_init_grace_period: { + label: 'Channel Init Grace Period', + description: '', + }, +}; diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 073af337..0242ed12 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -23,7 +23,11 @@ import UserAgentsTable from '../components/tables/UserAgentsTable'; import StreamProfilesTable from '../components/tables/StreamProfilesTable'; import useLocalStorage from '../hooks/useLocalStorage'; import useAuthStore from '../store/auth'; -import { USER_LEVELS, NETWORK_ACCESS_OPTIONS } from '../constants'; +import { + USER_LEVELS, + NETWORK_ACCESS_OPTIONS, + PROXY_SETTINGS_OPTIONS, +} from '../constants'; import ConfirmationDialog from '../components/ConfirmationDialog'; const SettingsPage = () => { @@ -40,6 +44,8 @@ const SettingsPage = () => { const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] = useState([]); + const [proxySettingsSaved, setProxySettingsSaved] = useState(false); + // UI / local storage settings const [tableSize, setTableSize] = useLocalStorage('table-size', 'default'); @@ -334,6 +340,14 @@ const SettingsPage = () => { }, {}), }); + const proxySettingsForm = useForm({ + mode: 'uncontrolled', + initialValues: Object.keys(PROXY_SETTINGS_OPTIONS).reduce((acc, key) => { + acc[key] = ''; + return acc; + }, {}), + }); + useEffect(() => { if (settings) { const formValues = Object.entries(settings).reduce( @@ -371,7 +385,17 @@ const SettingsPage = () => { ); networkAccessForm.setValues( Object.keys(NETWORK_ACCESS_OPTIONS).reduce((acc, key) => { - acc[key] = networkAccessSettings[key]; + acc[key] = networkAccessSettings[key] || '0.0.0.0/0'; + return acc; + }, {}) + ); + + const proxySettings = JSON.parse( + settings['proxy-settings'].value || '{}' + ); + proxySettingsForm.setValues( + Object.keys(PROXY_SETTINGS_OPTIONS).reduce((acc, key) => { + acc[key] = proxySettings[key] || ''; return acc; }, {}) ); @@ -420,6 +444,17 @@ const SettingsPage = () => { setNetworkAccessConfirmOpen(true); }; + const onProxySettingsSubmit = async () => { + setProxySettingsSaved(false); + + await API.updateSetting({ + ...settings['proxy-settings'], + value: JSON.stringify(proxySettingsForm.getValues()), + }); + + setProxySettingsSaved(true); + }; + const saveNetworkAccess = async () => { setNetworkAccessSaved(false); try { @@ -689,6 +724,56 @@ const SettingsPage = () => { , + + + + Proxy Settings + + +
+ + {proxySettingsSaved && ( + + )} + {Object.entries(PROXY_SETTINGS_OPTIONS).map( + ([key, config]) => { + return ( + + ); + } + )} + + + + + +
+
+
, ] : [] )} From 0c4d320dc23373fd3a7bc1344188fe5a569f901b Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 14 Jun 2025 12:39:07 -0500 Subject: [PATCH 12/19] Add descriptions to proxy settings --- frontend/src/constants.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/constants.js b/frontend/src/constants.js index 89b081ae..62336929 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -33,22 +33,22 @@ export const NETWORK_ACCESS_OPTIONS = { export const PROXY_SETTINGS_OPTIONS = { buffering_timeout: { label: 'Buffering Timeout', - description: '', + description: 'Maximum time (in seconds) to wait for buffering before switching streams', }, buffering_speed: { label: 'Buffering Speed', - description: '', + description: 'Speed threshold below which buffering is detected (1.0 = normal speed)', }, redis_chunk_ttl: { label: 'Redis Chunk TTL', - description: '', + description: 'Time-to-live for Redis chunks in seconds (how long stream data is cached)', }, channel_shutdown_delay: { label: 'Channel Shutdown Delay', - description: '', + description: 'Delay in seconds before shutting down a channel after last client disconnects', }, channel_init_grace_period: { label: 'Channel Init Grace Period', - description: '', + description: 'Grace period in seconds during channel initialization', }, }; From 51ce2d241c99257f930ad3ee6ad4950658b5a9a1 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 14 Jun 2025 12:42:49 -0500 Subject: [PATCH 13/19] Use integers and floats instead of strings for proxy settings. --- frontend/src/pages/Settings.jsx | 483 +++++++++++++++++--------------- 1 file changed, 261 insertions(+), 222 deletions(-) diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 0242ed12..0e8f61e8 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -17,6 +17,7 @@ import { Switch, Text, TextInput, + NumberInput, } from '@mantine/core'; import { isNotEmpty, useForm } from '@mantine/form'; import UserAgentsTable from '../components/tables/UserAgentsTable'; @@ -521,115 +522,190 @@ const SettingsPage = () => { ].concat( authUser.user_level == USER_LEVELS.ADMIN ? [ - - Stream Settings - -
- ({ + value: `${option.id}`, + label: option.name, + }))} + /> - ({ - label: r.label, - value: `${r.value}`, - }))} - /> + ({ + label: r.label, + value: `${r.value}`, + }))} + /> - + + Auto-Import Mapped Files + + + + + + + + + + +
+
, - + + User-Agents + + + + , + + + Stream Profiles + + + + , + + + + Network Access + {accordianValue == 'network-access' && ( + + Comma-Delimited CIDR ranges + + )} + + +
+ + {networkAccessSaved && ( + + )} + {networkAccessError && ( + + )} + {Object.entries(NETWORK_ACCESS_OPTIONS).map( + ([key, config]) => { + return ( + + ); + } + )} { > - -
-
, + + + + , - - User-Agents - - - - , - - - Stream Profiles - - - - , - - - - Network Access - {accordianValue == 'network-access' && ( - - Comma-Delimited CIDR ranges - + + + Proxy Settings + + +
- - + + {proxySettingsSaved && ( + )} - > - - {networkAccessSaved && ( - - )} - {networkAccessError && ( - - )} - {Object.entries(NETWORK_ACCESS_OPTIONS).map( - ([key, config]) => { - return ( - - ); - } - )} + {Object.entries(PROXY_SETTINGS_OPTIONS).map( + ([key, config]) => { + // Determine if this field should be a NumberInput + const isNumericField = [ + 'buffering_timeout', + 'redis_chunk_ttl', + 'channel_shutdown_delay', + 'channel_init_grace_period' + ].includes(key); - - - - - -
-
, + const isFloatField = key === 'buffering_speed'; - - - Proxy Settings - - -
- - {proxySettingsSaved && ( - - )} - {Object.entries(PROXY_SETTINGS_OPTIONS).map( - ([key, config]) => { + if (isNumericField) { return ( - + ); + } else if (isFloatField) { + return ( + + ); + } else { + return ( + ); } - )} + } + )} - + - - -
-
-
, - ] + Save + + + + + +
, + ] : [] )} From 9c5a174409f2cb08c9d65316172639e09aaa80e6 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 14 Jun 2025 13:08:56 -0500 Subject: [PATCH 14/19] Move regions to constants. --- frontend/src/constants.js | 251 +++++++++++++++++++++++++++++++ frontend/src/pages/Settings.jsx | 252 +------------------------------- 2 files changed, 253 insertions(+), 250 deletions(-) diff --git a/frontend/src/constants.js b/frontend/src/constants.js index 62336929..d87a1f7d 100644 --- a/frontend/src/constants.js +++ b/frontend/src/constants.js @@ -52,3 +52,254 @@ export const PROXY_SETTINGS_OPTIONS = { description: 'Grace period in seconds during channel initialization', }, }; + +export const REGION_CHOICES = [ + { value: 'ad', label: 'AD' }, + { value: 'ae', label: 'AE' }, + { value: 'af', label: 'AF' }, + { value: 'ag', label: 'AG' }, + { value: 'ai', label: 'AI' }, + { value: 'al', label: 'AL' }, + { value: 'am', label: 'AM' }, + { value: 'ao', label: 'AO' }, + { value: 'aq', label: 'AQ' }, + { value: 'ar', label: 'AR' }, + { value: 'as', label: 'AS' }, + { value: 'at', label: 'AT' }, + { value: 'au', label: 'AU' }, + { value: 'aw', label: 'AW' }, + { value: 'ax', label: 'AX' }, + { value: 'az', label: 'AZ' }, + { value: 'ba', label: 'BA' }, + { value: 'bb', label: 'BB' }, + { value: 'bd', label: 'BD' }, + { value: 'be', label: 'BE' }, + { value: 'bf', label: 'BF' }, + { value: 'bg', label: 'BG' }, + { value: 'bh', label: 'BH' }, + { value: 'bi', label: 'BI' }, + { value: 'bj', label: 'BJ' }, + { value: 'bl', label: 'BL' }, + { value: 'bm', label: 'BM' }, + { value: 'bn', label: 'BN' }, + { value: 'bo', label: 'BO' }, + { value: 'bq', label: 'BQ' }, + { value: 'br', label: 'BR' }, + { value: 'bs', label: 'BS' }, + { value: 'bt', label: 'BT' }, + { value: 'bv', label: 'BV' }, + { value: 'bw', label: 'BW' }, + { value: 'by', label: 'BY' }, + { value: 'bz', label: 'BZ' }, + { value: 'ca', label: 'CA' }, + { value: 'cc', label: 'CC' }, + { value: 'cd', label: 'CD' }, + { value: 'cf', label: 'CF' }, + { value: 'cg', label: 'CG' }, + { value: 'ch', label: 'CH' }, + { value: 'ci', label: 'CI' }, + { value: 'ck', label: 'CK' }, + { value: 'cl', label: 'CL' }, + { value: 'cm', label: 'CM' }, + { value: 'cn', label: 'CN' }, + { value: 'co', label: 'CO' }, + { value: 'cr', label: 'CR' }, + { value: 'cu', label: 'CU' }, + { value: 'cv', label: 'CV' }, + { value: 'cw', label: 'CW' }, + { value: 'cx', label: 'CX' }, + { value: 'cy', label: 'CY' }, + { value: 'cz', label: 'CZ' }, + { value: 'de', label: 'DE' }, + { value: 'dj', label: 'DJ' }, + { value: 'dk', label: 'DK' }, + { value: 'dm', label: 'DM' }, + { value: 'do', label: 'DO' }, + { value: 'dz', label: 'DZ' }, + { value: 'ec', label: 'EC' }, + { value: 'ee', label: 'EE' }, + { value: 'eg', label: 'EG' }, + { value: 'eh', label: 'EH' }, + { value: 'er', label: 'ER' }, + { value: 'es', label: 'ES' }, + { value: 'et', label: 'ET' }, + { value: 'fi', label: 'FI' }, + { value: 'fj', label: 'FJ' }, + { value: 'fk', label: 'FK' }, + { value: 'fm', label: 'FM' }, + { value: 'fo', label: 'FO' }, + { value: 'fr', label: 'FR' }, + { value: 'ga', label: 'GA' }, + { value: 'gb', label: 'GB' }, + { value: 'gd', label: 'GD' }, + { value: 'ge', label: 'GE' }, + { value: 'gf', label: 'GF' }, + { value: 'gg', label: 'GG' }, + { value: 'gh', label: 'GH' }, + { value: 'gi', label: 'GI' }, + { value: 'gl', label: 'GL' }, + { value: 'gm', label: 'GM' }, + { value: 'gn', label: 'GN' }, + { value: 'gp', label: 'GP' }, + { value: 'gq', label: 'GQ' }, + { value: 'gr', label: 'GR' }, + { value: 'gs', label: 'GS' }, + { value: 'gt', label: 'GT' }, + { value: 'gu', label: 'GU' }, + { value: 'gw', label: 'GW' }, + { value: 'gy', label: 'GY' }, + { value: 'hk', label: 'HK' }, + { value: 'hm', label: 'HM' }, + { value: 'hn', label: 'HN' }, + { value: 'hr', label: 'HR' }, + { value: 'ht', label: 'HT' }, + { value: 'hu', label: 'HU' }, + { value: 'id', label: 'ID' }, + { value: 'ie', label: 'IE' }, + { value: 'il', label: 'IL' }, + { value: 'im', label: 'IM' }, + { value: 'in', label: 'IN' }, + { value: 'io', label: 'IO' }, + { value: 'iq', label: 'IQ' }, + { value: 'ir', label: 'IR' }, + { value: 'is', label: 'IS' }, + { value: 'it', label: 'IT' }, + { value: 'je', label: 'JE' }, + { value: 'jm', label: 'JM' }, + { value: 'jo', label: 'JO' }, + { value: 'jp', label: 'JP' }, + { value: 'ke', label: 'KE' }, + { value: 'kg', label: 'KG' }, + { value: 'kh', label: 'KH' }, + { value: 'ki', label: 'KI' }, + { value: 'km', label: 'KM' }, + { value: 'kn', label: 'KN' }, + { value: 'kp', label: 'KP' }, + { value: 'kr', label: 'KR' }, + { value: 'kw', label: 'KW' }, + { value: 'ky', label: 'KY' }, + { value: 'kz', label: 'KZ' }, + { value: 'la', label: 'LA' }, + { value: 'lb', label: 'LB' }, + { value: 'lc', label: 'LC' }, + { value: 'li', label: 'LI' }, + { value: 'lk', label: 'LK' }, + { value: 'lr', label: 'LR' }, + { value: 'ls', label: 'LS' }, + { value: 'lt', label: 'LT' }, + { value: 'lu', label: 'LU' }, + { value: 'lv', label: 'LV' }, + { value: 'ly', label: 'LY' }, + { value: 'ma', label: 'MA' }, + { value: 'mc', label: 'MC' }, + { value: 'md', label: 'MD' }, + { value: 'me', label: 'ME' }, + { value: 'mf', label: 'MF' }, + { value: 'mg', label: 'MG' }, + { value: 'mh', label: 'MH' }, + { value: 'ml', label: 'ML' }, + { value: 'mm', label: 'MM' }, + { value: 'mn', label: 'MN' }, + { value: 'mo', label: 'MO' }, + { value: 'mp', label: 'MP' }, + { value: 'mq', label: 'MQ' }, + { value: 'mr', label: 'MR' }, + { value: 'ms', label: 'MS' }, + { value: 'mt', label: 'MT' }, + { value: 'mu', label: 'MU' }, + { value: 'mv', label: 'MV' }, + { value: 'mw', label: 'MW' }, + { value: 'mx', label: 'MX' }, + { value: 'my', label: 'MY' }, + { value: 'mz', label: 'MZ' }, + { value: 'na', label: 'NA' }, + { value: 'nc', label: 'NC' }, + { value: 'ne', label: 'NE' }, + { value: 'nf', label: 'NF' }, + { value: 'ng', label: 'NG' }, + { value: 'ni', label: 'NI' }, + { value: 'nl', label: 'NL' }, + { value: 'no', label: 'NO' }, + { value: 'np', label: 'NP' }, + { value: 'nr', label: 'NR' }, + { value: 'nu', label: 'NU' }, + { value: 'nz', label: 'NZ' }, + { value: 'om', label: 'OM' }, + { value: 'pa', label: 'PA' }, + { value: 'pe', label: 'PE' }, + { value: 'pf', label: 'PF' }, + { value: 'pg', label: 'PG' }, + { value: 'ph', label: 'PH' }, + { value: 'pk', label: 'PK' }, + { value: 'pl', label: 'PL' }, + { value: 'pm', label: 'PM' }, + { value: 'pn', label: 'PN' }, + { value: 'pr', label: 'PR' }, + { value: 'ps', label: 'PS' }, + { value: 'pt', label: 'PT' }, + { value: 'pw', label: 'PW' }, + { value: 'py', label: 'PY' }, + { value: 'qa', label: 'QA' }, + { value: 're', label: 'RE' }, + { value: 'ro', label: 'RO' }, + { value: 'rs', label: 'RS' }, + { value: 'ru', label: 'RU' }, + { value: 'rw', label: 'RW' }, + { value: 'sa', label: 'SA' }, + { value: 'sb', label: 'SB' }, + { value: 'sc', label: 'SC' }, + { value: 'sd', label: 'SD' }, + { value: 'se', label: 'SE' }, + { value: 'sg', label: 'SG' }, + { value: 'sh', label: 'SH' }, + { value: 'si', label: 'SI' }, + { value: 'sj', label: 'SJ' }, + { value: 'sk', label: 'SK' }, + { value: 'sl', label: 'SL' }, + { value: 'sm', label: 'SM' }, + { value: 'sn', label: 'SN' }, + { value: 'so', label: 'SO' }, + { value: 'sr', label: 'SR' }, + { value: 'ss', label: 'SS' }, + { value: 'st', label: 'ST' }, + { value: 'sv', label: 'SV' }, + { value: 'sx', label: 'SX' }, + { value: 'sy', label: 'SY' }, + { value: 'sz', label: 'SZ' }, + { value: 'tc', label: 'TC' }, + { value: 'td', label: 'TD' }, + { value: 'tf', label: 'TF' }, + { value: 'tg', label: 'TG' }, + { value: 'th', label: 'TH' }, + { value: 'tj', label: 'TJ' }, + { value: 'tk', label: 'TK' }, + { value: 'tl', label: 'TL' }, + { value: 'tm', label: 'TM' }, + { value: 'tn', label: 'TN' }, + { value: 'to', label: 'TO' }, + { value: 'tr', label: 'TR' }, + { value: 'tt', label: 'TT' }, + { value: 'tv', label: 'TV' }, + { value: 'tw', label: 'TW' }, + { value: 'tz', label: 'TZ' }, + { value: 'ua', label: 'UA' }, + { value: 'ug', label: 'UG' }, + { value: 'um', label: 'UM' }, + { value: 'us', label: 'US' }, + { value: 'uy', label: 'UY' }, + { value: 'uz', label: 'UZ' }, + { value: 'va', label: 'VA' }, + { value: 'vc', label: 'VC' }, + { value: 've', label: 'VE' }, + { value: 'vg', label: 'VG' }, + { value: 'vi', label: 'VI' }, + { value: 'vn', label: 'VN' }, + { value: 'vu', label: 'VU' }, + { value: 'wf', label: 'WF' }, + { value: 'ws', label: 'WS' }, + { value: 'ye', label: 'YE' }, + { value: 'yt', label: 'YT' }, + { value: 'za', label: 'ZA' }, + { value: 'zm', label: 'ZM' }, + { value: 'zw', label: 'ZW' }, +]; diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 0e8f61e8..ac1688e0 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -28,6 +28,7 @@ import { USER_LEVELS, NETWORK_ACCESS_OPTIONS, PROXY_SETTINGS_OPTIONS, + REGION_CHOICES, } from '../constants'; import ConfirmationDialog from '../components/ConfirmationDialog'; @@ -50,256 +51,7 @@ const SettingsPage = () => { // UI / local storage settings const [tableSize, setTableSize] = useLocalStorage('table-size', 'default'); - const regionChoices = [ - { value: 'ad', label: 'AD' }, - { value: 'ae', label: 'AE' }, - { value: 'af', label: 'AF' }, - { value: 'ag', label: 'AG' }, - { value: 'ai', label: 'AI' }, - { value: 'al', label: 'AL' }, - { value: 'am', label: 'AM' }, - { value: 'ao', label: 'AO' }, - { value: 'aq', label: 'AQ' }, - { value: 'ar', label: 'AR' }, - { value: 'as', label: 'AS' }, - { value: 'at', label: 'AT' }, - { value: 'au', label: 'AU' }, - { value: 'aw', label: 'AW' }, - { value: 'ax', label: 'AX' }, - { value: 'az', label: 'AZ' }, - { value: 'ba', label: 'BA' }, - { value: 'bb', label: 'BB' }, - { value: 'bd', label: 'BD' }, - { value: 'be', label: 'BE' }, - { value: 'bf', label: 'BF' }, - { value: 'bg', label: 'BG' }, - { value: 'bh', label: 'BH' }, - { value: 'bi', label: 'BI' }, - { value: 'bj', label: 'BJ' }, - { value: 'bl', label: 'BL' }, - { value: 'bm', label: 'BM' }, - { value: 'bn', label: 'BN' }, - { value: 'bo', label: 'BO' }, - { value: 'bq', label: 'BQ' }, - { value: 'br', label: 'BR' }, - { value: 'bs', label: 'BS' }, - { value: 'bt', label: 'BT' }, - { value: 'bv', label: 'BV' }, - { value: 'bw', label: 'BW' }, - { value: 'by', label: 'BY' }, - { value: 'bz', label: 'BZ' }, - { value: 'ca', label: 'CA' }, - { value: 'cc', label: 'CC' }, - { value: 'cd', label: 'CD' }, - { value: 'cf', label: 'CF' }, - { value: 'cg', label: 'CG' }, - { value: 'ch', label: 'CH' }, - { value: 'ci', label: 'CI' }, - { value: 'ck', label: 'CK' }, - { value: 'cl', label: 'CL' }, - { value: 'cm', label: 'CM' }, - { value: 'cn', label: 'CN' }, - { value: 'co', label: 'CO' }, - { value: 'cr', label: 'CR' }, - { value: 'cu', label: 'CU' }, - { value: 'cv', label: 'CV' }, - { value: 'cw', label: 'CW' }, - { value: 'cx', label: 'CX' }, - { value: 'cy', label: 'CY' }, - { value: 'cz', label: 'CZ' }, - { value: 'de', label: 'DE' }, - { value: 'dj', label: 'DJ' }, - { value: 'dk', label: 'DK' }, - { value: 'dm', label: 'DM' }, - { value: 'do', label: 'DO' }, - { value: 'dz', label: 'DZ' }, - { value: 'ec', label: 'EC' }, - { value: 'ee', label: 'EE' }, - { value: 'eg', label: 'EG' }, - { value: 'eh', label: 'EH' }, - { value: 'er', label: 'ER' }, - { value: 'es', label: 'ES' }, - { value: 'et', label: 'ET' }, - { value: 'fi', label: 'FI' }, - { value: 'fj', label: 'FJ' }, - { value: 'fk', label: 'FK' }, - { value: 'fm', label: 'FM' }, - { value: 'fo', label: 'FO' }, - { value: 'fr', label: 'FR' }, - { value: 'ga', label: 'GA' }, - { value: 'gb', label: 'GB' }, - { value: 'gd', label: 'GD' }, - { value: 'ge', label: 'GE' }, - { value: 'gf', label: 'GF' }, - { value: 'gg', label: 'GG' }, - { value: 'gh', label: 'GH' }, - { value: 'gi', label: 'GI' }, - { value: 'gl', label: 'GL' }, - { value: 'gm', label: 'GM' }, - { value: 'gn', label: 'GN' }, - { value: 'gp', label: 'GP' }, - { value: 'gq', label: 'GQ' }, - { value: 'gr', label: 'GR' }, - { value: 'gs', label: 'GS' }, - { value: 'gt', label: 'GT' }, - { value: 'gu', label: 'GU' }, - { value: 'gw', label: 'GW' }, - { value: 'gy', label: 'GY' }, - { value: 'hk', label: 'HK' }, - { value: 'hm', label: 'HM' }, - { value: 'hn', label: 'HN' }, - { value: 'hr', label: 'HR' }, - { value: 'ht', label: 'HT' }, - { value: 'hu', label: 'HU' }, - { value: 'id', label: 'ID' }, - { value: 'ie', label: 'IE' }, - { value: 'il', label: 'IL' }, - { value: 'im', label: 'IM' }, - { value: 'in', label: 'IN' }, - { value: 'io', label: 'IO' }, - { value: 'iq', label: 'IQ' }, - { value: 'ir', label: 'IR' }, - { value: 'is', label: 'IS' }, - { value: 'it', label: 'IT' }, - { value: 'je', label: 'JE' }, - { value: 'jm', label: 'JM' }, - { value: 'jo', label: 'JO' }, - { value: 'jp', label: 'JP' }, - { value: 'ke', label: 'KE' }, - { value: 'kg', label: 'KG' }, - { value: 'kh', label: 'KH' }, - { value: 'ki', label: 'KI' }, - { value: 'km', label: 'KM' }, - { value: 'kn', label: 'KN' }, - { value: 'kp', label: 'KP' }, - { value: 'kr', label: 'KR' }, - { value: 'kw', label: 'KW' }, - { value: 'ky', label: 'KY' }, - { value: 'kz', label: 'KZ' }, - { value: 'la', label: 'LA' }, - { value: 'lb', label: 'LB' }, - { value: 'lc', label: 'LC' }, - { value: 'li', label: 'LI' }, - { value: 'lk', label: 'LK' }, - { value: 'lr', label: 'LR' }, - { value: 'ls', label: 'LS' }, - { value: 'lt', label: 'LT' }, - { value: 'lu', label: 'LU' }, - { value: 'lv', label: 'LV' }, - { value: 'ly', label: 'LY' }, - { value: 'ma', label: 'MA' }, - { value: 'mc', label: 'MC' }, - { value: 'md', label: 'MD' }, - { value: 'me', label: 'ME' }, - { value: 'mf', label: 'MF' }, - { value: 'mg', label: 'MG' }, - { value: 'mh', label: 'MH' }, - { value: 'ml', label: 'ML' }, - { value: 'mm', label: 'MM' }, - { value: 'mn', label: 'MN' }, - { value: 'mo', label: 'MO' }, - { value: 'mp', label: 'MP' }, - { value: 'mq', label: 'MQ' }, - { value: 'mr', label: 'MR' }, - { value: 'ms', label: 'MS' }, - { value: 'mt', label: 'MT' }, - { value: 'mu', label: 'MU' }, - { value: 'mv', label: 'MV' }, - { value: 'mw', label: 'MW' }, - { value: 'mx', label: 'MX' }, - { value: 'my', label: 'MY' }, - { value: 'mz', label: 'MZ' }, - { value: 'na', label: 'NA' }, - { value: 'nc', label: 'NC' }, - { value: 'ne', label: 'NE' }, - { value: 'nf', label: 'NF' }, - { value: 'ng', label: 'NG' }, - { value: 'ni', label: 'NI' }, - { value: 'nl', label: 'NL' }, - { value: 'no', label: 'NO' }, - { value: 'np', label: 'NP' }, - { value: 'nr', label: 'NR' }, - { value: 'nu', label: 'NU' }, - { value: 'nz', label: 'NZ' }, - { value: 'om', label: 'OM' }, - { value: 'pa', label: 'PA' }, - { value: 'pe', label: 'PE' }, - { value: 'pf', label: 'PF' }, - { value: 'pg', label: 'PG' }, - { value: 'ph', label: 'PH' }, - { value: 'pk', label: 'PK' }, - { value: 'pl', label: 'PL' }, - { value: 'pm', label: 'PM' }, - { value: 'pn', label: 'PN' }, - { value: 'pr', label: 'PR' }, - { value: 'ps', label: 'PS' }, - { value: 'pt', label: 'PT' }, - { value: 'pw', label: 'PW' }, - { value: 'py', label: 'PY' }, - { value: 'qa', label: 'QA' }, - { value: 're', label: 'RE' }, - { value: 'ro', label: 'RO' }, - { value: 'rs', label: 'RS' }, - { value: 'ru', label: 'RU' }, - { value: 'rw', label: 'RW' }, - { value: 'sa', label: 'SA' }, - { value: 'sb', label: 'SB' }, - { value: 'sc', label: 'SC' }, - { value: 'sd', label: 'SD' }, - { value: 'se', label: 'SE' }, - { value: 'sg', label: 'SG' }, - { value: 'sh', label: 'SH' }, - { value: 'si', label: 'SI' }, - { value: 'sj', label: 'SJ' }, - { value: 'sk', label: 'SK' }, - { value: 'sl', label: 'SL' }, - { value: 'sm', label: 'SM' }, - { value: 'sn', label: 'SN' }, - { value: 'so', label: 'SO' }, - { value: 'sr', label: 'SR' }, - { value: 'ss', label: 'SS' }, - { value: 'st', label: 'ST' }, - { value: 'sv', label: 'SV' }, - { value: 'sx', label: 'SX' }, - { value: 'sy', label: 'SY' }, - { value: 'sz', label: 'SZ' }, - { value: 'tc', label: 'TC' }, - { value: 'td', label: 'TD' }, - { value: 'tf', label: 'TF' }, - { value: 'tg', label: 'TG' }, - { value: 'th', label: 'TH' }, - { value: 'tj', label: 'TJ' }, - { value: 'tk', label: 'TK' }, - { value: 'tl', label: 'TL' }, - { value: 'tm', label: 'TM' }, - { value: 'tn', label: 'TN' }, - { value: 'to', label: 'TO' }, - { value: 'tr', label: 'TR' }, - { value: 'tt', label: 'TT' }, - { value: 'tv', label: 'TV' }, - { value: 'tw', label: 'TW' }, - { value: 'tz', label: 'TZ' }, - { value: 'ua', label: 'UA' }, - { value: 'ug', label: 'UG' }, - { value: 'um', label: 'UM' }, - { value: 'us', label: 'US' }, - { value: 'uy', label: 'UY' }, - { value: 'uz', label: 'UZ' }, - { value: 'va', label: 'VA' }, - { value: 'vc', label: 'VC' }, - { value: 've', label: 'VE' }, - { value: 'vg', label: 'VG' }, - { value: 'vi', label: 'VI' }, - { value: 'vn', label: 'VN' }, - { value: 'vu', label: 'VU' }, - { value: 'wf', label: 'WF' }, - { value: 'ws', label: 'WS' }, - { value: 'ye', label: 'YE' }, - { value: 'yt', label: 'YT' }, - { value: 'za', label: 'ZA' }, - { value: 'zm', label: 'ZM' }, - { value: 'zw', label: 'ZW' }, - ]; + const regionChoices = REGION_CHOICES; const form = useForm({ mode: 'uncontrolled', From e80d30689cdf6e10692bedff44a5416996ead623 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sat, 14 Jun 2025 13:42:01 -0500 Subject: [PATCH 15/19] Settings load correctly during first open. --- core/api_views.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core/api_views.py b/core/api_views.py index 04e39f11..dfb52b44 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -144,16 +144,12 @@ class ProxySettingsViewSet(viewsets.ViewSet): 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) + return Response(settings_data) 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) + return Response(settings_data) def update(self, request, pk=None): """Update proxy settings""" @@ -166,7 +162,7 @@ class ProxySettingsViewSet(viewsets.ViewSet): settings_obj.value = json.dumps(serializer.validated_data) settings_obj.save() - return Response(serializer.data) + return Response(serializer.validated_data) def partial_update(self, request, pk=None): """Partially update proxy settings""" @@ -182,7 +178,7 @@ class ProxySettingsViewSet(viewsets.ViewSet): settings_obj.value = json.dumps(serializer.validated_data) settings_obj.save() - return Response(serializer.data) + return Response(serializer.validated_data) @action(detail=False, methods=['get', 'patch']) def settings(self, request): @@ -271,6 +267,7 @@ def environment(request): operation_description="Get application version information", responses={200: "Version information"}, ) + @api_view(["GET"]) def version(request): # Import version information From 9757f6a48dbaebb066dec16b0c52571f0e775df0 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Sun, 15 Jun 2025 11:28:57 -0500 Subject: [PATCH 16/19] Fix key error with react. --- core/api_views.py | 9 +- frontend/src/pages/Settings.jsx | 597 ++++++++++++++++---------------- 2 files changed, 302 insertions(+), 304 deletions(-) diff --git a/core/api_views.py b/core/api_views.py index dfb52b44..b416cf92 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -203,7 +203,7 @@ def environment(request): country_code = None country_name = None - # 1) Get the public IP + # 1) Get the public IP from ipify.org API try: r = requests.get("https://api64.ipify.org?format=json", timeout=5) r.raise_for_status() @@ -211,17 +211,17 @@ def environment(request): except requests.RequestException as e: public_ip = f"Error: {e}" - # 2) Get the local IP + # 2) Get the local IP by connecting to a public DNS server try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - # connect to a “public” address so the OS can determine our local interface + # connect to a "public" address so the OS can determine our local interface s.connect(("8.8.8.8", 80)) local_ip = s.getsockname()[0] s.close() except Exception as e: local_ip = f"Error: {e}" - # 3) If we got a valid public_ip, fetch geo info from ipapi.co or ip-api.com + # 3) Get geolocation data from ipapi.co or ip-api.com if public_ip and "Error" not in public_ip: try: # Attempt to get geo information from ipapi.co first @@ -250,6 +250,7 @@ def environment(request): country_code = None country_name = None + # 4) Get environment mode from system environment variable return Response( { "authenticated": True, diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index ac1688e0..e471602e 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -143,15 +143,14 @@ const SettingsPage = () => { }, {}) ); - const proxySettings = JSON.parse( - settings['proxy-settings'].value || '{}' - ); - proxySettingsForm.setValues( - Object.keys(PROXY_SETTINGS_OPTIONS).reduce((acc, key) => { - acc[key] = proxySettings[key] || ''; - return acc; - }, {}) - ); + if (settings['proxy-settings']?.value) { + try { + const proxySettings = JSON.parse(settings['proxy-settings'].value); + proxySettingsForm.setValues(proxySettings); + } catch (error) { + console.error('Error parsing proxy settings:', error); + } + } } }, [settings]); @@ -246,143 +245,217 @@ const SettingsPage = () => { defaultValue="ui-settings" onChange={setAccordianValue} > - {[ - - UI Settings - - ({ - value: `${option.id}`, - label: option.name, - }))} - /> + + UI Settings + + ({ - value: `${option.id}`, - label: option.name, - }))} - /> - ({ + value: `${option.id}`, + label: option.name, + }))} + /> - ({ + value: `${option.id}`, + label: option.name, + }))} + /> +