mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-28 20:41:19 +00:00
commit
7b16ca6ff7
12 changed files with 934 additions and 512 deletions
|
|
@ -9,11 +9,34 @@ 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 proxy settings from CoreSettings JSON data with fallback to defaults"""
|
||||
try:
|
||||
from core.models import CoreSettings
|
||||
return CoreSettings.get_proxy_settings()
|
||||
except Exception:
|
||||
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.get("redis_chunk_ttl", 60)
|
||||
|
||||
@property
|
||||
def REDIS_CHUNK_TTL(self):
|
||||
return self.get_redis_chunk_ttl()
|
||||
|
||||
class HLSConfig(BaseConfig):
|
||||
MIN_SEGMENTS = 12
|
||||
MAX_SEGMENTS = 16
|
||||
|
|
@ -42,21 +65,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 +80,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.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.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.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.get("channel_init_grace_period", 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()
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class ConfigHelper:
|
|||
@staticmethod
|
||||
def channel_shutdown_delay():
|
||||
"""Get channel shutdown delay in seconds"""
|
||||
return ConfigHelper.get('CHANNEL_SHUTDOWN_DELAY', 0)
|
||||
return Config.get_channel_shutdown_delay()
|
||||
|
||||
@staticmethod
|
||||
def initial_behind_chunks():
|
||||
|
|
@ -54,7 +54,7 @@ class ConfigHelper:
|
|||
@staticmethod
|
||||
def redis_chunk_ttl():
|
||||
"""Get Redis chunk TTL in seconds"""
|
||||
return ConfigHelper.get('REDIS_CHUNK_TTL', 60)
|
||||
return Config.get_redis_chunk_ttl()
|
||||
|
||||
@staticmethod
|
||||
def chunk_size():
|
||||
|
|
@ -85,11 +85,18 @@ 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"""
|
||||
return ConfigHelper.get('BUFFERING_TIMEOUT', 15) # Default to 15 seconds
|
||||
return Config.get_buffering_timeout()
|
||||
|
||||
@staticmethod
|
||||
def buffering_speed():
|
||||
"""Get buffering speed in bytes per second"""
|
||||
return ConfigHelper.get('BUFFERING_SPEED',1) # Default to 1x
|
||||
"""Get buffering speed threshold"""
|
||||
return Config.get_buffering_speed()
|
||||
|
||||
@staticmethod
|
||||
def channel_init_grace_period():
|
||||
"""Get channel initialization grace period in seconds"""
|
||||
return Config.get_channel_init_grace_period()
|
||||
|
|
|
|||
|
|
@ -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...")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -6,20 +6,24 @@ import logging
|
|||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.decorators import api_view, permission_classes, action
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from .models import (
|
||||
UserAgent,
|
||||
StreamProfile,
|
||||
CoreSettings,
|
||||
STREAM_HASH_KEY,
|
||||
NETWORK_ACCESS,
|
||||
PROXY_SETTINGS_KEY,
|
||||
)
|
||||
from .serializers import (
|
||||
UserAgentSerializer,
|
||||
StreamProfileSerializer,
|
||||
CoreSettingsSerializer,
|
||||
ProxySettingsSerializer,
|
||||
)
|
||||
from rest_framework.decorators import api_view, permission_classes, action
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
import socket
|
||||
import requests
|
||||
import os
|
||||
|
|
@ -68,7 +72,6 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
rehash_streams.delay(request.data["value"].split(","))
|
||||
|
||||
return response
|
||||
|
||||
@action(detail=False, methods=["post"], url_path="check")
|
||||
def check(self, request, *args, **kwargs):
|
||||
data = request.data
|
||||
|
|
@ -109,6 +112,83 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
|
||||
return Response({}, status=status.HTTP_200_OK)
|
||||
|
||||
class ProxySettingsViewSet(viewsets.ViewSet):
|
||||
"""
|
||||
API endpoint for proxy settings stored as JSON in CoreSettings.
|
||||
"""
|
||||
serializer_class = ProxySettingsSerializer
|
||||
|
||||
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 list(self, request):
|
||||
"""Return proxy settings"""
|
||||
settings_obj, settings_data = self._get_or_create_settings()
|
||||
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()
|
||||
return Response(settings_data)
|
||||
|
||||
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)
|
||||
|
||||
# Update the JSON data
|
||||
settings_obj.value = json.dumps(serializer.validated_data)
|
||||
settings_obj.save()
|
||||
|
||||
return Response(serializer.validated_data)
|
||||
|
||||
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.validated_data)
|
||||
|
||||
@action(detail=False, methods=['get', 'patch'])
|
||||
def settings(self, request):
|
||||
"""Get or update the proxy settings."""
|
||||
if request.method == 'GET':
|
||||
return self.list(request)
|
||||
elif request.method == 'PATCH':
|
||||
return self.partial_update(request)
|
||||
|
||||
|
||||
|
||||
@swagger_auto_schema(
|
||||
method="get",
|
||||
|
|
@ -123,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()
|
||||
|
|
@ -131,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
|
||||
|
|
@ -170,6 +250,7 @@ def environment(request):
|
|||
country_code = None
|
||||
country_name = None
|
||||
|
||||
# 4) Get environment mode from system environment variable
|
||||
return Response(
|
||||
{
|
||||
"authenticated": True,
|
||||
|
|
@ -187,6 +268,7 @@ def environment(request):
|
|||
operation_description="Get application version information",
|
||||
responses={200: "Version information"},
|
||||
)
|
||||
|
||||
@api_view(["GET"])
|
||||
def version(request):
|
||||
# Import version information
|
||||
|
|
|
|||
35
core/migrations/0014_default_proxy_settings.py
Normal file
35
core/migrations/0014_default_proxy_settings.py
Normal file
|
|
@ -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),
|
||||
]
|
||||
|
|
@ -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):
|
||||
|
|
@ -149,6 +150,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):
|
||||
|
|
@ -194,3 +196,20 @@ class CoreSettings(models.Model):
|
|||
return cls.objects.get(key=AUTO_IMPORT_MAPPED_FILES).value
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import json
|
|||
import ipaddress
|
||||
|
||||
from rest_framework import serializers
|
||||
from .models import UserAgent, StreamProfile, CoreSettings, NETWORK_ACCESS
|
||||
from .models import CoreSettings, UserAgent, StreamProfile, NETWORK_ACCESS
|
||||
|
||||
|
||||
class UserAgentSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -65,3 +65,36 @@ class CoreSettingsSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
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 < 0 or value > 300:
|
||||
raise serializers.ValidationError("Buffering timeout must be between 0 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 < 0 or value > 60:
|
||||
raise serializers.ValidationError("Channel init grace period must be between 0 and 60 seconds")
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,3 +29,277 @@ export const NETWORK_ACCESS_OPTIONS = {
|
|||
description: 'Limit access to the Dispatcharr UI',
|
||||
},
|
||||
};
|
||||
|
||||
export const PROXY_SETTINGS_OPTIONS = {
|
||||
buffering_timeout: {
|
||||
label: 'Buffering Timeout',
|
||||
description: 'Maximum time (in seconds) to wait for buffering before switching streams',
|
||||
},
|
||||
buffering_speed: {
|
||||
label: 'Buffering Speed',
|
||||
description: 'Speed threshold below which buffering is detected (1.0 = normal speed)',
|
||||
},
|
||||
redis_chunk_ttl: {
|
||||
label: 'Buffer Chunk TTL',
|
||||
description: 'Time-to-live for buffer chunks in seconds (how long stream data is cached)',
|
||||
},
|
||||
channel_shutdown_delay: {
|
||||
label: 'Channel Shutdown Delay',
|
||||
description: 'Delay in seconds before shutting down a channel after last client disconnects',
|
||||
},
|
||||
channel_init_grace_period: {
|
||||
label: 'Channel Initialization Grace Period',
|
||||
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' },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -17,13 +17,19 @@ import {
|
|||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
NumberInput,
|
||||
} from '@mantine/core';
|
||||
import { isNotEmpty, useForm } from '@mantine/form';
|
||||
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,
|
||||
REGION_CHOICES,
|
||||
} from '../constants';
|
||||
import ConfirmationDialog from '../components/ConfirmationDialog';
|
||||
|
||||
const SettingsPage = () => {
|
||||
|
|
@ -40,262 +46,15 @@ const SettingsPage = () => {
|
|||
const [netNetworkAccessConfirmCIDRs, setNetNetworkAccessConfirmCIDRs] =
|
||||
useState([]);
|
||||
|
||||
const [proxySettingsSaved, setProxySettingsSaved] = useState(false);
|
||||
|
||||
// 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',
|
||||
mode: 'controlled',
|
||||
initialValues: {
|
||||
'default-user-agent': '',
|
||||
'default-stream-profile': '',
|
||||
|
|
@ -312,7 +71,7 @@ const SettingsPage = () => {
|
|||
});
|
||||
|
||||
const networkAccessForm = useForm({
|
||||
mode: 'uncontrolled',
|
||||
mode: 'controlled',
|
||||
initialValues: Object.keys(NETWORK_ACCESS_OPTIONS).reduce((acc, key) => {
|
||||
acc[key] = '0.0.0.0/0';
|
||||
return acc;
|
||||
|
|
@ -334,6 +93,14 @@ const SettingsPage = () => {
|
|||
}, {}),
|
||||
});
|
||||
|
||||
const proxySettingsForm = useForm({
|
||||
mode: 'controlled',
|
||||
initialValues: Object.keys(PROXY_SETTINGS_OPTIONS).reduce((acc, key) => {
|
||||
acc[key] = '';
|
||||
return acc;
|
||||
}, {}),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
const formValues = Object.entries(settings).reduce(
|
||||
|
|
@ -371,10 +138,19 @@ 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;
|
||||
}, {})
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
|
|
@ -420,6 +196,29 @@ const SettingsPage = () => {
|
|||
setNetworkAccessConfirmOpen(true);
|
||||
};
|
||||
|
||||
const onProxySettingsSubmit = async () => {
|
||||
setProxySettingsSaved(false);
|
||||
|
||||
await API.updateSetting({
|
||||
...settings['proxy-settings'],
|
||||
value: JSON.stringify(proxySettingsForm.getValues()),
|
||||
});
|
||||
|
||||
setProxySettingsSaved(true);
|
||||
};
|
||||
|
||||
const resetProxySettingsToDefaults = () => {
|
||||
const defaultValues = {
|
||||
buffering_timeout: 15,
|
||||
buffering_speed: 1.0,
|
||||
redis_chunk_ttl: 60,
|
||||
channel_shutdown_delay: 0,
|
||||
channel_init_grace_period: 5,
|
||||
};
|
||||
|
||||
proxySettingsForm.setValues(defaultValues);
|
||||
};
|
||||
|
||||
const saveNetworkAccess = async () => {
|
||||
setNetworkAccessSaved(false);
|
||||
try {
|
||||
|
|
@ -458,239 +257,332 @@ const SettingsPage = () => {
|
|||
defaultValue="ui-settings"
|
||||
onChange={setAccordianValue}
|
||||
>
|
||||
{[
|
||||
<Accordion.Item value="ui-settings">
|
||||
<Accordion.Control>UI Settings</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Select
|
||||
label="Table Size"
|
||||
value={tableSize}
|
||||
onChange={(val) => onUISettingsChange('table-size', val)}
|
||||
data={[
|
||||
{
|
||||
value: 'default',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
value: 'compact',
|
||||
label: 'Compact',
|
||||
},
|
||||
{
|
||||
value: 'large',
|
||||
label: 'Large',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>,
|
||||
].concat(
|
||||
authUser.user_level == USER_LEVELS.ADMIN
|
||||
? [
|
||||
<Accordion.Item value="stream-settings">
|
||||
<Accordion.Control>Stream Settings</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('default-user-agent')}
|
||||
key={form.key('default-user-agent')}
|
||||
id={
|
||||
settings['default-user-agent']?.id ||
|
||||
'default-user-agent'
|
||||
}
|
||||
name={
|
||||
settings['default-user-agent']?.key ||
|
||||
'default-user-agent'
|
||||
}
|
||||
label={
|
||||
settings['default-user-agent']?.name ||
|
||||
'Default User Agent'
|
||||
}
|
||||
data={userAgents.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
<Accordion.Item value="ui-settings">
|
||||
<Accordion.Control>UI Settings</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Select
|
||||
label="Table Size"
|
||||
value={tableSize}
|
||||
onChange={(val) => onUISettingsChange('table-size', val)}
|
||||
data={[
|
||||
{
|
||||
value: 'default',
|
||||
label: 'Default',
|
||||
},
|
||||
{
|
||||
value: 'compact',
|
||||
label: 'Compact',
|
||||
},
|
||||
{
|
||||
value: 'large',
|
||||
label: 'Large',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('default-stream-profile')}
|
||||
key={form.key('default-stream-profile')}
|
||||
id={
|
||||
settings['default-stream-profile']?.id ||
|
||||
'default-stream-profile'
|
||||
}
|
||||
name={
|
||||
settings['default-stream-profile']?.key ||
|
||||
'default-stream-profile'
|
||||
}
|
||||
label={
|
||||
settings['default-stream-profile']?.name ||
|
||||
'Default Stream Profile'
|
||||
}
|
||||
data={streamProfiles.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('preferred-region')}
|
||||
key={form.key('preferred-region')}
|
||||
id={
|
||||
settings['preferred-region']?.id ||
|
||||
'preferred-region'
|
||||
}
|
||||
name={
|
||||
settings['preferred-region']?.key ||
|
||||
'preferred-region'
|
||||
}
|
||||
label={
|
||||
settings['preferred-region']?.name ||
|
||||
'Preferred Region'
|
||||
}
|
||||
data={regionChoices.map((r) => ({
|
||||
label: r.label,
|
||||
value: `${r.value}`,
|
||||
}))}
|
||||
/>
|
||||
{authUser.user_level == USER_LEVELS.ADMIN && (
|
||||
<>
|
||||
<Accordion.Item value="stream-settings">
|
||||
<Accordion.Control>Stream Settings</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('default-user-agent')}
|
||||
key={form.key('default-user-agent')}
|
||||
id={
|
||||
settings['default-user-agent']?.id ||
|
||||
'default-user-agent'
|
||||
}
|
||||
name={
|
||||
settings['default-user-agent']?.key ||
|
||||
'default-user-agent'
|
||||
}
|
||||
label={
|
||||
settings['default-user-agent']?.name ||
|
||||
'Default User Agent'
|
||||
}
|
||||
data={userAgents.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Group
|
||||
justify="space-between"
|
||||
style={{ paddingTop: 5 }}
|
||||
>
|
||||
<Text size="sm" fw={500}>
|
||||
Auto-Import Mapped Files
|
||||
</Text>
|
||||
<Switch
|
||||
{...form.getInputProps('auto-import-mapped-files', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
key={form.key('auto-import-mapped-files')}
|
||||
id={
|
||||
settings['auto-import-mapped-files']?.id ||
|
||||
'auto-import-mapped-files'
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('default-stream-profile')}
|
||||
key={form.key('default-stream-profile')}
|
||||
id={
|
||||
settings['default-stream-profile']?.id ||
|
||||
'default-stream-profile'
|
||||
}
|
||||
name={
|
||||
settings['default-stream-profile']?.key ||
|
||||
'default-stream-profile'
|
||||
}
|
||||
label={
|
||||
settings['default-stream-profile']?.name ||
|
||||
'Default Stream Profile'
|
||||
}
|
||||
data={streamProfiles.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
<Select
|
||||
searchable
|
||||
{...form.getInputProps('preferred-region')}
|
||||
key={form.key('preferred-region')}
|
||||
id={
|
||||
settings['preferred-region']?.id ||
|
||||
'preferred-region'
|
||||
}
|
||||
name={
|
||||
settings['preferred-region']?.key ||
|
||||
'preferred-region'
|
||||
}
|
||||
label={
|
||||
settings['preferred-region']?.name ||
|
||||
'Preferred Region'
|
||||
}
|
||||
data={regionChoices.map((r) => ({
|
||||
label: r.label,
|
||||
value: `${r.value}`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
id="m3u-hash-key"
|
||||
name="m3u-hash-key"
|
||||
label="M3U Hash Key"
|
||||
data={[
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
value: 'url',
|
||||
label: 'URL',
|
||||
},
|
||||
{
|
||||
value: 'tvg_id',
|
||||
label: 'TVG-ID',
|
||||
},
|
||||
]}
|
||||
{...form.getInputProps('m3u-hash-key')}
|
||||
key={form.key('m3u-hash-key')}
|
||||
/>
|
||||
<Group
|
||||
justify="space-between"
|
||||
style={{ paddingTop: 5 }}
|
||||
>
|
||||
<Text size="sm" fw={500}>
|
||||
Auto-Import Mapped Files
|
||||
</Text>
|
||||
<Switch
|
||||
{...form.getInputProps('auto-import-mapped-files', {
|
||||
type: 'checkbox',
|
||||
})}
|
||||
key={form.key('auto-import-mapped-files')}
|
||||
id={
|
||||
settings['auto-import-mapped-files']?.id ||
|
||||
'auto-import-mapped-files'
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Flex
|
||||
mih={50}
|
||||
gap="xs"
|
||||
justify="flex-end"
|
||||
align="flex-end"
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={form.submitting}
|
||||
variant="default"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>,
|
||||
<MultiSelect
|
||||
id="m3u-hash-key"
|
||||
name="m3u-hash-key"
|
||||
label="M3U Hash Key"
|
||||
data={[
|
||||
{
|
||||
value: 'name',
|
||||
label: 'Name',
|
||||
},
|
||||
{
|
||||
value: 'url',
|
||||
label: 'URL',
|
||||
},
|
||||
{
|
||||
value: 'tvg_id',
|
||||
label: 'TVG-ID',
|
||||
},
|
||||
]}
|
||||
{...form.getInputProps('m3u-hash-key')}
|
||||
key={form.key('m3u-hash-key')}
|
||||
/>
|
||||
|
||||
<Accordion.Item value="user-agents">
|
||||
<Accordion.Control>User-Agents</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<UserAgentsTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>,
|
||||
|
||||
<Accordion.Item value="stream-profiles">
|
||||
<Accordion.Control>Stream Profiles</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<StreamProfilesTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>,
|
||||
|
||||
<Accordion.Item value="network-access">
|
||||
<Accordion.Control>
|
||||
<Box>Network Access</Box>
|
||||
{accordianValue == 'network-access' && (
|
||||
<Box>
|
||||
<Text size="sm">Comma-Delimited CIDR ranges</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<form
|
||||
onSubmit={networkAccessForm.onSubmit(
|
||||
onNetworkAccessSubmit
|
||||
)}
|
||||
<Flex
|
||||
mih={50}
|
||||
gap="xs"
|
||||
justify="flex-end"
|
||||
align="flex-end"
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={form.submitting}
|
||||
variant="default"
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{networkAccessSaved && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="green"
|
||||
title="Saved Successfully"
|
||||
></Alert>
|
||||
)}
|
||||
{networkAccessError && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="red"
|
||||
title={networkAccessError}
|
||||
></Alert>
|
||||
)}
|
||||
{Object.entries(NETWORK_ACCESS_OPTIONS).map(
|
||||
([key, config]) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={config.label}
|
||||
{...networkAccessForm.getInputProps(key)}
|
||||
key={networkAccessForm.key(key)}
|
||||
description={config.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Flex
|
||||
mih={50}
|
||||
gap="xs"
|
||||
justify="flex-end"
|
||||
align="flex-end"
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={networkAccessForm.submitting}
|
||||
variant="default"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</form>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>,
|
||||
]
|
||||
: []
|
||||
<Accordion.Item value="user-agents">
|
||||
<Accordion.Control>User-Agents</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<UserAgentsTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="stream-profiles">
|
||||
<Accordion.Control>Stream Profiles</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<StreamProfilesTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="network-access">
|
||||
<Accordion.Control>
|
||||
<Box>Network Access</Box>
|
||||
{accordianValue == 'network-access' && (
|
||||
<Box>
|
||||
<Text size="sm">Comma-Delimited CIDR ranges</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<form
|
||||
onSubmit={networkAccessForm.onSubmit(
|
||||
onNetworkAccessSubmit
|
||||
)}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{networkAccessSaved && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="green"
|
||||
title="Saved Successfully"
|
||||
></Alert>
|
||||
)}
|
||||
{networkAccessError && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="red"
|
||||
title={networkAccessError}
|
||||
></Alert>
|
||||
)}
|
||||
{Object.entries(NETWORK_ACCESS_OPTIONS).map(
|
||||
([key, config]) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={config.label}
|
||||
{...networkAccessForm.getInputProps(key)}
|
||||
key={networkAccessForm.key(key)}
|
||||
description={config.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
|
||||
<Flex
|
||||
mih={50}
|
||||
gap="xs"
|
||||
justify="flex-end"
|
||||
align="flex-end"
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={networkAccessForm.submitting}
|
||||
variant="default"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</form>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
<Accordion.Item value="proxy-settings">
|
||||
<Accordion.Control>
|
||||
<Box>Proxy Settings</Box>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<form
|
||||
onSubmit={proxySettingsForm.onSubmit(
|
||||
onProxySettingsSubmit
|
||||
)}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{proxySettingsSaved && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="green"
|
||||
title="Saved Successfully"
|
||||
></Alert>
|
||||
)}
|
||||
{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';
|
||||
|
||||
if (isNumericField) {
|
||||
return (
|
||||
<NumberInput
|
||||
key={key}
|
||||
label={config.label}
|
||||
{...proxySettingsForm.getInputProps(key)}
|
||||
description={config.description || null}
|
||||
min={0}
|
||||
max={key === 'buffering_timeout' ? 300 :
|
||||
key === 'redis_chunk_ttl' ? 3600 :
|
||||
key === 'channel_shutdown_delay' ? 300 : 60}
|
||||
/>
|
||||
);
|
||||
} else if (isFloatField) {
|
||||
return (
|
||||
<NumberInput
|
||||
key={key}
|
||||
label={config.label}
|
||||
{...proxySettingsForm.getInputProps(key)}
|
||||
description={config.description || null}
|
||||
min={0.0}
|
||||
max={10.0}
|
||||
step={0.01}
|
||||
precision={1}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TextInput
|
||||
key={key}
|
||||
label={config.label}
|
||||
{...proxySettingsForm.getInputProps(key)}
|
||||
description={config.description || null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
|
||||
<Flex
|
||||
mih={50}
|
||||
gap="xs"
|
||||
justify="space-between"
|
||||
align="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
onClick={resetProxySettingsToDefaults}
|
||||
>
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={networkAccessForm.submitting}
|
||||
variant="default"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</form>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</>
|
||||
)}
|
||||
</Accordion>
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue