mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Refactor CoreSettings to use JSONField for value storage and update related logic for proper type handling. Adjusted serializers and forms to accommodate new data structure, ensuring seamless integration across the application.
This commit is contained in:
parent
4bfdd15b37
commit
36967c10ce
32 changed files with 866 additions and 519 deletions
|
|
@ -15,8 +15,9 @@ from .models import (
|
|||
UserAgent,
|
||||
StreamProfile,
|
||||
CoreSettings,
|
||||
STREAM_HASH_KEY,
|
||||
NETWORK_ACCESS,
|
||||
STREAM_SETTINGS_KEY,
|
||||
DVR_SETTINGS_KEY,
|
||||
NETWORK_ACCESS_KEY,
|
||||
PROXY_SETTINGS_KEY,
|
||||
)
|
||||
from .serializers import (
|
||||
|
|
@ -68,16 +69,28 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
old_value = instance.value
|
||||
response = super().update(request, *args, **kwargs)
|
||||
if instance.key == STREAM_HASH_KEY:
|
||||
if instance.value != request.data["value"]:
|
||||
rehash_streams.delay(request.data["value"].split(","))
|
||||
|
||||
# If DVR pre/post offsets changed, reschedule upcoming recordings
|
||||
try:
|
||||
from core.models import DVR_PRE_OFFSET_MINUTES_KEY, DVR_POST_OFFSET_MINUTES_KEY
|
||||
if instance.key in (DVR_PRE_OFFSET_MINUTES_KEY, DVR_POST_OFFSET_MINUTES_KEY):
|
||||
if instance.value != request.data.get("value"):
|
||||
# If stream settings changed and m3u_hash_key is different, rehash streams
|
||||
if instance.key == STREAM_SETTINGS_KEY:
|
||||
new_value = request.data.get("value", {})
|
||||
if isinstance(new_value, dict) and isinstance(old_value, dict):
|
||||
old_hash = old_value.get("m3u_hash_key", "")
|
||||
new_hash = new_value.get("m3u_hash_key", "")
|
||||
if old_hash != new_hash:
|
||||
hash_keys = new_hash.split(",") if isinstance(new_hash, str) else new_hash
|
||||
rehash_streams.delay(hash_keys)
|
||||
|
||||
# If DVR settings changed and pre/post offsets are different, reschedule upcoming recordings
|
||||
if instance.key == DVR_SETTINGS_KEY:
|
||||
new_value = request.data.get("value", {})
|
||||
if isinstance(new_value, dict) and isinstance(old_value, dict):
|
||||
old_pre = old_value.get("pre_offset_minutes")
|
||||
new_pre = new_value.get("pre_offset_minutes")
|
||||
old_post = old_value.get("post_offset_minutes")
|
||||
new_post = new_value.get("post_offset_minutes")
|
||||
if old_pre != new_pre or old_post != new_post:
|
||||
try:
|
||||
# Prefer async task if Celery is available
|
||||
from apps.channels.tasks import reschedule_upcoming_recordings_for_offset_change
|
||||
|
|
@ -86,24 +99,23 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
# Fallback to synchronous implementation
|
||||
from apps.channels.tasks import reschedule_upcoming_recordings_for_offset_change_impl
|
||||
reschedule_upcoming_recordings_for_offset_change_impl()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return response
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
response = super().create(request, *args, **kwargs)
|
||||
# If creating DVR pre/post offset settings, also reschedule upcoming recordings
|
||||
# If creating DVR settings with offset values, reschedule upcoming recordings
|
||||
try:
|
||||
key = request.data.get("key")
|
||||
from core.models import DVR_PRE_OFFSET_MINUTES_KEY, DVR_POST_OFFSET_MINUTES_KEY
|
||||
if key in (DVR_PRE_OFFSET_MINUTES_KEY, DVR_POST_OFFSET_MINUTES_KEY):
|
||||
try:
|
||||
from apps.channels.tasks import reschedule_upcoming_recordings_for_offset_change
|
||||
reschedule_upcoming_recordings_for_offset_change.delay()
|
||||
except Exception:
|
||||
from apps.channels.tasks import reschedule_upcoming_recordings_for_offset_change_impl
|
||||
reschedule_upcoming_recordings_for_offset_change_impl()
|
||||
if key == DVR_SETTINGS_KEY:
|
||||
value = request.data.get("value", {})
|
||||
if isinstance(value, dict) and ("pre_offset_minutes" in value or "post_offset_minutes" in value):
|
||||
try:
|
||||
from apps.channels.tasks import reschedule_upcoming_recordings_for_offset_change
|
||||
reschedule_upcoming_recordings_for_offset_change.delay()
|
||||
except Exception:
|
||||
from apps.channels.tasks import reschedule_upcoming_recordings_for_offset_change_impl
|
||||
reschedule_upcoming_recordings_for_offset_change_impl()
|
||||
except Exception:
|
||||
pass
|
||||
return response
|
||||
|
|
@ -111,13 +123,13 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
def check(self, request, *args, **kwargs):
|
||||
data = request.data
|
||||
|
||||
if data.get("key") == NETWORK_ACCESS:
|
||||
if data.get("key") == NETWORK_ACCESS_KEY:
|
||||
client_ip = ipaddress.ip_address(get_client_ip(request))
|
||||
|
||||
in_network = {}
|
||||
invalid = []
|
||||
|
||||
value = json.loads(data.get("value", "{}"))
|
||||
value = data.get("value", {})
|
||||
for key, val in value.items():
|
||||
in_network[key] = []
|
||||
cidrs = val.split(",")
|
||||
|
|
@ -142,7 +154,7 @@ class CoreSettingsViewSet(viewsets.ModelViewSet):
|
|||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
response_data = {
|
||||
**in_network,
|
||||
"client_ip": str(client_ip)
|
||||
|
|
@ -161,8 +173,8 @@ class ProxySettingsViewSet(viewsets.ViewSet):
|
|||
"""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):
|
||||
settings_data = settings_obj.value
|
||||
except CoreSettings.DoesNotExist:
|
||||
# Create default settings
|
||||
settings_data = {
|
||||
"buffering_timeout": 15,
|
||||
|
|
@ -175,7 +187,7 @@ class ProxySettingsViewSet(viewsets.ViewSet):
|
|||
key=PROXY_SETTINGS_KEY,
|
||||
defaults={
|
||||
"name": "Proxy Settings",
|
||||
"value": json.dumps(settings_data)
|
||||
"value": settings_data
|
||||
}
|
||||
)
|
||||
return settings_obj, settings_data
|
||||
|
|
@ -197,8 +209,8 @@ class ProxySettingsViewSet(viewsets.ViewSet):
|
|||
serializer = ProxySettingsSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Update the JSON data
|
||||
settings_obj.value = json.dumps(serializer.validated_data)
|
||||
# Update the JSON data - store as dict directly
|
||||
settings_obj.value = serializer.validated_data
|
||||
settings_obj.save()
|
||||
|
||||
return Response(serializer.validated_data)
|
||||
|
|
@ -213,8 +225,8 @@ class ProxySettingsViewSet(viewsets.ViewSet):
|
|||
serializer = ProxySettingsSerializer(data=updated_data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
# Update the JSON data
|
||||
settings_obj.value = json.dumps(serializer.validated_data)
|
||||
# Update the JSON data - store as dict directly
|
||||
settings_obj.value = serializer.validated_data
|
||||
settings_obj.save()
|
||||
|
||||
return Response(serializer.validated_data)
|
||||
|
|
@ -332,8 +344,8 @@ def rehash_streams_endpoint(request):
|
|||
"""Trigger the rehash streams task"""
|
||||
try:
|
||||
# Get the current hash keys from settings
|
||||
hash_key_setting = CoreSettings.objects.get(key=STREAM_HASH_KEY)
|
||||
hash_keys = hash_key_setting.value.split(",")
|
||||
hash_key = CoreSettings.get_m3u_hash_key()
|
||||
hash_keys = hash_key.split(",") if isinstance(hash_key, str) else hash_key
|
||||
|
||||
# Queue the rehash task
|
||||
task = rehash_streams.delay(hash_keys)
|
||||
|
|
@ -344,10 +356,10 @@ def rehash_streams_endpoint(request):
|
|||
"task_id": task.id
|
||||
}, status=status.HTTP_200_OK)
|
||||
|
||||
except CoreSettings.DoesNotExist:
|
||||
except Exception as e:
|
||||
return Response({
|
||||
"success": False,
|
||||
"message": "Hash key settings not found"
|
||||
"message": f"Error triggering rehash: {str(e)}"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
# your_app/management/commands/update_column.py
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from core.models import CoreSettings, NETWORK_ACCESS
|
||||
from core.models import CoreSettings, NETWORK_ACCESS_KEY
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Reset network access settings"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
setting = CoreSettings.objects.get(key=NETWORK_ACCESS)
|
||||
setting.value = "{}"
|
||||
setting = CoreSettings.objects.get(key=NETWORK_ACCESS_KEY)
|
||||
setting.value = {}
|
||||
setting.save()
|
||||
|
|
|
|||
267
core/migrations/0020_change_coresettings_value_to_jsonfield.py
Normal file
267
core/migrations/0020_change_coresettings_value_to_jsonfield.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
# Generated migration to change CoreSettings value field to JSONField and consolidate settings
|
||||
|
||||
import json
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def convert_string_to_json(apps, schema_editor):
|
||||
"""Convert existing string values to appropriate JSON types before changing column type"""
|
||||
CoreSettings = apps.get_model("core", "CoreSettings")
|
||||
|
||||
for setting in CoreSettings.objects.all():
|
||||
value = setting.value
|
||||
|
||||
if not value:
|
||||
# Empty strings become empty string in JSON
|
||||
setting.value = json.dumps("")
|
||||
setting.save(update_fields=['value'])
|
||||
continue
|
||||
|
||||
# Try to parse as JSON if it looks like JSON (objects/arrays)
|
||||
if value.startswith('{') or value.startswith('['):
|
||||
try:
|
||||
parsed = json.loads(value)
|
||||
# Store as JSON string temporarily (column is still CharField)
|
||||
setting.value = json.dumps(parsed)
|
||||
setting.save(update_fields=['value'])
|
||||
continue
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
# Try to parse as number
|
||||
try:
|
||||
# Check if it's an integer
|
||||
if '.' not in value and value.lstrip('-').isdigit():
|
||||
setting.value = json.dumps(int(value))
|
||||
setting.save(update_fields=['value'])
|
||||
continue
|
||||
# Check if it's a float
|
||||
float_val = float(value)
|
||||
setting.value = json.dumps(float_val)
|
||||
setting.save(update_fields=['value'])
|
||||
continue
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
# Check for booleans
|
||||
if value.lower() in ('true', 'false', '1', '0', 'yes', 'no', 'on', 'off'):
|
||||
bool_val = value.lower() in ('true', '1', 'yes', 'on')
|
||||
setting.value = json.dumps(bool_val)
|
||||
setting.save(update_fields=['value'])
|
||||
continue
|
||||
|
||||
# Default: store as JSON string
|
||||
setting.value = json.dumps(value)
|
||||
setting.save(update_fields=['value'])
|
||||
|
||||
|
||||
def consolidate_settings(apps, schema_editor):
|
||||
"""Consolidate individual setting rows into grouped JSON objects."""
|
||||
CoreSettings = apps.get_model("core", "CoreSettings")
|
||||
|
||||
# Helper to get setting value
|
||||
def get_value(key, default=None):
|
||||
try:
|
||||
obj = CoreSettings.objects.get(key=key)
|
||||
return obj.value if obj.value is not None else default
|
||||
except CoreSettings.DoesNotExist:
|
||||
return default
|
||||
|
||||
# STREAM SETTINGS
|
||||
stream_settings = {
|
||||
"default_user_agent": get_value("default-user-agent"),
|
||||
"default_stream_profile": get_value("default-stream-profile"),
|
||||
"m3u_hash_key": get_value("m3u-hash-key", ""),
|
||||
"preferred_region": get_value("preferred-region"),
|
||||
"auto_import_mapped_files": get_value("auto-import-mapped-files"),
|
||||
}
|
||||
CoreSettings.objects.update_or_create(
|
||||
key="stream_settings",
|
||||
defaults={"name": "Stream Settings", "value": stream_settings}
|
||||
)
|
||||
|
||||
# DVR SETTINGS
|
||||
dvr_settings = {
|
||||
"tv_template": get_value("dvr-tv-template", "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"),
|
||||
"movie_template": get_value("dvr-movie-template", "Movies/{title} ({year}).mkv"),
|
||||
"tv_fallback_dir": get_value("dvr-tv-fallback-dir", "TV_Shows"),
|
||||
"tv_fallback_template": get_value("dvr-tv-fallback-template", "TV_Shows/{show}/{start}.mkv"),
|
||||
"movie_fallback_template": get_value("dvr-movie-fallback-template", "Movies/{start}.mkv"),
|
||||
"comskip_enabled": bool(get_value("dvr-comskip-enabled", False)),
|
||||
"comskip_custom_path": get_value("dvr-comskip-custom-path", ""),
|
||||
"pre_offset_minutes": int(get_value("dvr-pre-offset-minutes", 0) or 0),
|
||||
"post_offset_minutes": int(get_value("dvr-post-offset-minutes", 0) or 0),
|
||||
"series_rules": get_value("dvr-series-rules", []),
|
||||
}
|
||||
CoreSettings.objects.update_or_create(
|
||||
key="dvr_settings",
|
||||
defaults={"name": "DVR Settings", "value": dvr_settings}
|
||||
)
|
||||
|
||||
# BACKUP SETTINGS - using underscore keys (not dashes)
|
||||
backup_settings = {
|
||||
"schedule_enabled": get_value("backup_schedule_enabled") if get_value("backup_schedule_enabled") is not None else True,
|
||||
"schedule_frequency": get_value("backup_schedule_frequency") or "daily",
|
||||
"schedule_time": get_value("backup_schedule_time") or "03:00",
|
||||
"schedule_day_of_week": get_value("backup_schedule_day_of_week") if get_value("backup_schedule_day_of_week") is not None else 0,
|
||||
"retention_count": get_value("backup_retention_count") if get_value("backup_retention_count") is not None else 3,
|
||||
"schedule_cron_expression": get_value("backup_schedule_cron_expression") or "",
|
||||
}
|
||||
CoreSettings.objects.update_or_create(
|
||||
key="backup_settings",
|
||||
defaults={"name": "Backup Settings", "value": backup_settings}
|
||||
)
|
||||
|
||||
# SYSTEM SETTINGS
|
||||
system_settings = {
|
||||
"time_zone": get_value("system-time-zone", "UTC"),
|
||||
"max_system_events": int(get_value("max-system-events", 100) or 100),
|
||||
}
|
||||
CoreSettings.objects.update_or_create(
|
||||
key="system_settings",
|
||||
defaults={"name": "System Settings", "value": system_settings}
|
||||
)
|
||||
|
||||
# Rename proxy-settings to proxy_settings (if it exists with old name)
|
||||
try:
|
||||
old_proxy = CoreSettings.objects.get(key="proxy-settings")
|
||||
old_proxy.key = "proxy_settings"
|
||||
old_proxy.save()
|
||||
except CoreSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Ensure proxy_settings exists with defaults if not present
|
||||
proxy_obj, proxy_created = CoreSettings.objects.get_or_create(
|
||||
key="proxy_settings",
|
||||
defaults={
|
||||
"name": "Proxy Settings",
|
||||
"value": {
|
||||
"buffering_timeout": 15,
|
||||
"buffering_speed": 1.0,
|
||||
"redis_chunk_ttl": 60,
|
||||
"channel_shutdown_delay": 0,
|
||||
"channel_init_grace_period": 5,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Rename network-access to network_access (if it exists with old name)
|
||||
try:
|
||||
old_network = CoreSettings.objects.get(key="network-access")
|
||||
old_network.key = "network_access"
|
||||
old_network.save()
|
||||
except CoreSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Ensure network_access exists with defaults if not present
|
||||
network_obj, network_created = CoreSettings.objects.get_or_create(
|
||||
key="network_access",
|
||||
defaults={
|
||||
"name": "Network Access",
|
||||
"value": {}
|
||||
}
|
||||
)
|
||||
# Delete old individual setting rows (keep only the new grouped settings)
|
||||
grouped_keys = ["stream_settings", "dvr_settings", "backup_settings", "system_settings", "proxy_settings", "network_access"]
|
||||
CoreSettings.objects.exclude(key__in=grouped_keys).delete()
|
||||
|
||||
|
||||
def reverse_migration(apps, schema_editor):
|
||||
"""Reverse migration: split grouped settings and convert JSON back to strings"""
|
||||
CoreSettings = apps.get_model("core", "CoreSettings")
|
||||
|
||||
# Helper to create individual setting
|
||||
def create_setting(key, name, value):
|
||||
# Convert value back to string representation for CharField
|
||||
if isinstance(value, str):
|
||||
str_value = value
|
||||
elif isinstance(value, bool):
|
||||
str_value = "true" if value else "false"
|
||||
elif isinstance(value, (int, float)):
|
||||
str_value = str(value)
|
||||
elif isinstance(value, (dict, list)):
|
||||
str_value = json.dumps(value)
|
||||
elif value is None:
|
||||
str_value = ""
|
||||
else:
|
||||
str_value = str(value)
|
||||
|
||||
CoreSettings.objects.update_or_create(
|
||||
key=key,
|
||||
defaults={"name": name, "value": str_value}
|
||||
)
|
||||
|
||||
# Split stream_settings
|
||||
try:
|
||||
stream = CoreSettings.objects.get(key="stream_settings")
|
||||
if isinstance(stream.value, dict):
|
||||
create_setting("default_user_agent", "Default User Agent", stream.value.get("default_user_agent"))
|
||||
create_setting("default_stream_profile", "Default Stream Profile", stream.value.get("default_stream_profile"))
|
||||
create_setting("stream_hash_key", "Stream Hash Key", stream.value.get("m3u_hash_key", ""))
|
||||
create_setting("preferred_region", "Preferred Region", stream.value.get("preferred_region"))
|
||||
create_setting("auto_import_mapped_files", "Auto Import Mapped Files", stream.value.get("auto_import_mapped_files"))
|
||||
stream.delete()
|
||||
except CoreSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Split dvr_settings
|
||||
try:
|
||||
dvr = CoreSettings.objects.get(key="dvr_settings")
|
||||
if isinstance(dvr.value, dict):
|
||||
create_setting("dvr_tv_template", "DVR TV Template", dvr.value.get("tv_template", "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"))
|
||||
create_setting("dvr_movie_template", "DVR Movie Template", dvr.value.get("movie_template", "Movies/{title} ({year}).mkv"))
|
||||
create_setting("dvr_tv_fallback_dir", "DVR TV Fallback Dir", dvr.value.get("tv_fallback_dir", "TV_Shows"))
|
||||
create_setting("dvr_tv_fallback_template", "DVR TV Fallback Template", dvr.value.get("tv_fallback_template", "TV_Shows/{show}/{start}.mkv"))
|
||||
create_setting("dvr_movie_fallback_template", "DVR Movie Fallback Template", dvr.value.get("movie_fallback_template", "Movies/{start}.mkv"))
|
||||
create_setting("dvr_comskip_enabled", "DVR Comskip Enabled", dvr.value.get("comskip_enabled", False))
|
||||
create_setting("dvr_comskip_custom_path", "DVR Comskip Custom Path", dvr.value.get("comskip_custom_path", ""))
|
||||
create_setting("dvr_pre_offset_minutes", "DVR Pre Offset Minutes", dvr.value.get("pre_offset_minutes", 0))
|
||||
create_setting("dvr_post_offset_minutes", "DVR Post Offset Minutes", dvr.value.get("post_offset_minutes", 0))
|
||||
create_setting("dvr_series_rules", "DVR Series Rules", dvr.value.get("series_rules", []))
|
||||
dvr.delete()
|
||||
except CoreSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Split backup_settings
|
||||
try:
|
||||
backup = CoreSettings.objects.get(key="backup_settings")
|
||||
if isinstance(backup.value, dict):
|
||||
create_setting("backup_schedule_enabled", "Backup Schedule Enabled", backup.value.get("schedule_enabled", False))
|
||||
create_setting("backup_schedule_frequency", "Backup Schedule Frequency", backup.value.get("schedule_frequency", "weekly"))
|
||||
create_setting("backup_schedule_time", "Backup Schedule Time", backup.value.get("schedule_time", "02:00"))
|
||||
create_setting("backup_schedule_day_of_week", "Backup Schedule Day of Week", backup.value.get("schedule_day_of_week", 0))
|
||||
create_setting("backup_retention_count", "Backup Retention Count", backup.value.get("retention_count", 7))
|
||||
create_setting("backup_schedule_cron_expression", "Backup Schedule Cron Expression", backup.value.get("schedule_cron_expression", ""))
|
||||
backup.delete()
|
||||
except CoreSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Split system_settings
|
||||
try:
|
||||
system = CoreSettings.objects.get(key="system_settings")
|
||||
if isinstance(system.value, dict):
|
||||
create_setting("system_time_zone", "System Time Zone", system.value.get("time_zone", "UTC"))
|
||||
create_setting("max_system_events", "Max System Events", system.value.get("max_system_events", 100))
|
||||
system.delete()
|
||||
except CoreSettings.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0019_add_vlc_stream_profile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# First, convert all data to valid JSON strings while column is still CharField
|
||||
migrations.RunPython(convert_string_to_json, migrations.RunPython.noop),
|
||||
# Then change the field type to JSONField
|
||||
migrations.AlterField(
|
||||
model_name='coresettings',
|
||||
name='value',
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
# Finally, consolidate individual settings into grouped JSON objects
|
||||
migrations.RunPython(consolidate_settings, reverse_migration),
|
||||
]
|
||||
269
core/models.py
269
core/models.py
|
|
@ -148,24 +148,13 @@ class StreamProfile(models.Model):
|
|||
return part
|
||||
|
||||
|
||||
DEFAULT_USER_AGENT_KEY = slugify("Default User-Agent")
|
||||
DEFAULT_STREAM_PROFILE_KEY = slugify("Default Stream Profile")
|
||||
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")
|
||||
DVR_TV_TEMPLATE_KEY = slugify("DVR TV Template")
|
||||
DVR_MOVIE_TEMPLATE_KEY = slugify("DVR Movie Template")
|
||||
DVR_SERIES_RULES_KEY = slugify("DVR Series Rules")
|
||||
DVR_TV_FALLBACK_DIR_KEY = slugify("DVR TV Fallback Dir")
|
||||
DVR_TV_FALLBACK_TEMPLATE_KEY = slugify("DVR TV Fallback Template")
|
||||
DVR_MOVIE_FALLBACK_TEMPLATE_KEY = slugify("DVR Movie Fallback Template")
|
||||
DVR_COMSKIP_ENABLED_KEY = slugify("DVR Comskip Enabled")
|
||||
DVR_COMSKIP_CUSTOM_PATH_KEY = slugify("DVR Comskip Custom Path")
|
||||
DVR_PRE_OFFSET_MINUTES_KEY = slugify("DVR Pre-Offset Minutes")
|
||||
DVR_POST_OFFSET_MINUTES_KEY = slugify("DVR Post-Offset Minutes")
|
||||
SYSTEM_TIME_ZONE_KEY = slugify("System Time Zone")
|
||||
# Setting group keys
|
||||
STREAM_SETTINGS_KEY = "stream_settings"
|
||||
DVR_SETTINGS_KEY = "dvr_settings"
|
||||
BACKUP_SETTINGS_KEY = "backup_settings"
|
||||
PROXY_SETTINGS_KEY = "proxy_settings"
|
||||
NETWORK_ACCESS_KEY = "network_access"
|
||||
SYSTEM_SETTINGS_KEY = "system_settings"
|
||||
|
||||
|
||||
class CoreSettings(models.Model):
|
||||
|
|
@ -176,208 +165,166 @@ class CoreSettings(models.Model):
|
|||
name = models.CharField(
|
||||
max_length=255,
|
||||
)
|
||||
value = models.CharField(
|
||||
max_length=255,
|
||||
value = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Core Settings"
|
||||
|
||||
# Helper methods to get/set grouped settings
|
||||
@classmethod
|
||||
def _get_group(cls, key, defaults=None):
|
||||
"""Get a settings group, returning defaults if not found."""
|
||||
try:
|
||||
return cls.objects.get(key=key).value or (defaults or {})
|
||||
except cls.DoesNotExist:
|
||||
return defaults or {}
|
||||
|
||||
@classmethod
|
||||
def _update_group(cls, key, name, updates):
|
||||
"""Update specific fields in a settings group."""
|
||||
obj, created = cls.objects.get_or_create(
|
||||
key=key,
|
||||
defaults={"name": name, "value": {}}
|
||||
)
|
||||
current = obj.value if isinstance(obj.value, dict) else {}
|
||||
current.update(updates)
|
||||
obj.value = current
|
||||
obj.save()
|
||||
return current
|
||||
|
||||
# Stream Settings
|
||||
@classmethod
|
||||
def get_stream_settings(cls):
|
||||
"""Get all stream-related settings."""
|
||||
return cls._get_group(STREAM_SETTINGS_KEY, {
|
||||
"default_user_agent": None,
|
||||
"default_stream_profile": None,
|
||||
"m3u_hash_key": "",
|
||||
"preferred_region": None,
|
||||
"auto_import_mapped_files": None,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def get_default_user_agent_id(cls):
|
||||
"""Retrieve a system profile by name (or return None if not found)."""
|
||||
return cls.objects.get(key=DEFAULT_USER_AGENT_KEY).value
|
||||
return cls.get_stream_settings().get("default_user_agent")
|
||||
|
||||
@classmethod
|
||||
def get_default_stream_profile_id(cls):
|
||||
return cls.objects.get(key=DEFAULT_STREAM_PROFILE_KEY).value
|
||||
return cls.get_stream_settings().get("default_stream_profile")
|
||||
|
||||
@classmethod
|
||||
def get_m3u_hash_key(cls):
|
||||
return cls.objects.get(key=STREAM_HASH_KEY).value
|
||||
return cls.get_stream_settings().get("m3u_hash_key", "")
|
||||
|
||||
@classmethod
|
||||
def get_preferred_region(cls):
|
||||
"""Retrieve the preferred region setting (or return None if not found)."""
|
||||
try:
|
||||
return cls.objects.get(key=PREFERRED_REGION_KEY).value
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
return cls.get_stream_settings().get("preferred_region")
|
||||
|
||||
@classmethod
|
||||
def get_auto_import_mapped_files(cls):
|
||||
"""Retrieve the preferred region setting (or return None if not found)."""
|
||||
try:
|
||||
return cls.objects.get(key=AUTO_IMPORT_MAPPED_FILES).value
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
return cls.get_stream_settings().get("auto_import_mapped_files")
|
||||
|
||||
# DVR Settings
|
||||
@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,
|
||||
}
|
||||
def get_dvr_settings(cls):
|
||||
"""Get all DVR-related settings."""
|
||||
return cls._get_group(DVR_SETTINGS_KEY, {
|
||||
"tv_template": "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv",
|
||||
"movie_template": "Movies/{title} ({year}).mkv",
|
||||
"tv_fallback_dir": "TV_Shows",
|
||||
"tv_fallback_template": "TV_Shows/{show}/{start}.mkv",
|
||||
"movie_fallback_template": "Movies/{start}.mkv",
|
||||
"comskip_enabled": False,
|
||||
"comskip_custom_path": "",
|
||||
"pre_offset_minutes": 0,
|
||||
"post_offset_minutes": 0,
|
||||
"series_rules": [],
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def get_dvr_tv_template(cls):
|
||||
try:
|
||||
return cls.objects.get(key=DVR_TV_TEMPLATE_KEY).value
|
||||
except cls.DoesNotExist:
|
||||
# Default: relative to recordings root (/data/recordings)
|
||||
return "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"
|
||||
return cls.get_dvr_settings().get("tv_template", "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv")
|
||||
|
||||
@classmethod
|
||||
def get_dvr_movie_template(cls):
|
||||
try:
|
||||
return cls.objects.get(key=DVR_MOVIE_TEMPLATE_KEY).value
|
||||
except cls.DoesNotExist:
|
||||
return "Movies/{title} ({year}).mkv"
|
||||
return cls.get_dvr_settings().get("movie_template", "Movies/{title} ({year}).mkv")
|
||||
|
||||
@classmethod
|
||||
def get_dvr_tv_fallback_dir(cls):
|
||||
"""Folder name to use when a TV episode has no season/episode information.
|
||||
Defaults to 'TV_Show' to match existing behavior but can be overridden in settings.
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(key=DVR_TV_FALLBACK_DIR_KEY).value or "TV_Shows"
|
||||
except cls.DoesNotExist:
|
||||
return "TV_Shows"
|
||||
return cls.get_dvr_settings().get("tv_fallback_dir", "TV_Shows")
|
||||
|
||||
@classmethod
|
||||
def get_dvr_tv_fallback_template(cls):
|
||||
"""Full path template used when season/episode are missing for a TV airing."""
|
||||
try:
|
||||
return cls.objects.get(key=DVR_TV_FALLBACK_TEMPLATE_KEY).value
|
||||
except cls.DoesNotExist:
|
||||
# default requested by user
|
||||
return "TV_Shows/{show}/{start}.mkv"
|
||||
return cls.get_dvr_settings().get("tv_fallback_template", "TV_Shows/{show}/{start}.mkv")
|
||||
|
||||
@classmethod
|
||||
def get_dvr_movie_fallback_template(cls):
|
||||
"""Full path template used when movie metadata is incomplete."""
|
||||
try:
|
||||
return cls.objects.get(key=DVR_MOVIE_FALLBACK_TEMPLATE_KEY).value
|
||||
except cls.DoesNotExist:
|
||||
return "Movies/{start}.mkv"
|
||||
return cls.get_dvr_settings().get("movie_fallback_template", "Movies/{start}.mkv")
|
||||
|
||||
@classmethod
|
||||
def get_dvr_comskip_enabled(cls):
|
||||
"""Return boolean-like string value ('true'/'false') for comskip enablement."""
|
||||
try:
|
||||
val = cls.objects.get(key=DVR_COMSKIP_ENABLED_KEY).value
|
||||
return str(val).lower() in ("1", "true", "yes", "on")
|
||||
except cls.DoesNotExist:
|
||||
return False
|
||||
return bool(cls.get_dvr_settings().get("comskip_enabled", False))
|
||||
|
||||
@classmethod
|
||||
def get_dvr_comskip_custom_path(cls):
|
||||
"""Return configured comskip.ini path or empty string if unset."""
|
||||
try:
|
||||
return cls.objects.get(key=DVR_COMSKIP_CUSTOM_PATH_KEY).value
|
||||
except cls.DoesNotExist:
|
||||
return ""
|
||||
return cls.get_dvr_settings().get("comskip_custom_path", "")
|
||||
|
||||
@classmethod
|
||||
def set_dvr_comskip_custom_path(cls, path: str | None):
|
||||
"""Persist the comskip.ini path setting, normalizing nulls to empty string."""
|
||||
value = (path or "").strip()
|
||||
obj, _ = cls.objects.get_or_create(
|
||||
key=DVR_COMSKIP_CUSTOM_PATH_KEY,
|
||||
defaults={"name": "DVR Comskip Custom Path", "value": value},
|
||||
)
|
||||
if obj.value != value:
|
||||
obj.value = value
|
||||
obj.save(update_fields=["value"])
|
||||
cls._update_group(DVR_SETTINGS_KEY, "DVR Settings", {"comskip_custom_path": value})
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def get_dvr_pre_offset_minutes(cls):
|
||||
"""Minutes to start recording before scheduled start (default 0)."""
|
||||
try:
|
||||
val = cls.objects.get(key=DVR_PRE_OFFSET_MINUTES_KEY).value
|
||||
return int(val)
|
||||
except cls.DoesNotExist:
|
||||
return 0
|
||||
except Exception:
|
||||
try:
|
||||
return int(float(val))
|
||||
except Exception:
|
||||
return 0
|
||||
return int(cls.get_dvr_settings().get("pre_offset_minutes", 0) or 0)
|
||||
|
||||
@classmethod
|
||||
def get_dvr_post_offset_minutes(cls):
|
||||
"""Minutes to stop recording after scheduled end (default 0)."""
|
||||
try:
|
||||
val = cls.objects.get(key=DVR_POST_OFFSET_MINUTES_KEY).value
|
||||
return int(val)
|
||||
except cls.DoesNotExist:
|
||||
return 0
|
||||
except Exception:
|
||||
try:
|
||||
return int(float(val))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def get_system_time_zone(cls):
|
||||
"""Return configured system time zone or fall back to Django settings."""
|
||||
try:
|
||||
value = cls.objects.get(key=SYSTEM_TIME_ZONE_KEY).value
|
||||
if value:
|
||||
return value
|
||||
except cls.DoesNotExist:
|
||||
pass
|
||||
return getattr(settings, "TIME_ZONE", "UTC") or "UTC"
|
||||
|
||||
@classmethod
|
||||
def set_system_time_zone(cls, tz_name: str | None):
|
||||
"""Persist the desired system time zone identifier."""
|
||||
value = (tz_name or "").strip() or getattr(settings, "TIME_ZONE", "UTC") or "UTC"
|
||||
obj, _ = cls.objects.get_or_create(
|
||||
key=SYSTEM_TIME_ZONE_KEY,
|
||||
defaults={"name": "System Time Zone", "value": value},
|
||||
)
|
||||
if obj.value != value:
|
||||
obj.value = value
|
||||
obj.save(update_fields=["value"])
|
||||
return value
|
||||
return int(cls.get_dvr_settings().get("post_offset_minutes", 0) or 0)
|
||||
|
||||
@classmethod
|
||||
def get_dvr_series_rules(cls):
|
||||
"""Return list of series recording rules. Each: {tvg_id, title, mode: 'all'|'new'}"""
|
||||
import json
|
||||
try:
|
||||
raw = cls.objects.get(key=DVR_SERIES_RULES_KEY).value
|
||||
rules = json.loads(raw) if raw else []
|
||||
if isinstance(rules, list):
|
||||
return rules
|
||||
return []
|
||||
except cls.DoesNotExist:
|
||||
# Initialize empty if missing
|
||||
cls.objects.create(key=DVR_SERIES_RULES_KEY, name="DVR Series Rules", value="[]")
|
||||
return []
|
||||
return cls.get_dvr_settings().get("series_rules", [])
|
||||
|
||||
@classmethod
|
||||
def set_dvr_series_rules(cls, rules):
|
||||
import json
|
||||
try:
|
||||
obj, _ = cls.objects.get_or_create(key=DVR_SERIES_RULES_KEY, defaults={"name": "DVR Series Rules", "value": "[]"})
|
||||
obj.value = json.dumps(rules)
|
||||
obj.save(update_fields=["value"])
|
||||
return rules
|
||||
except Exception:
|
||||
return rules
|
||||
cls._update_group(DVR_SETTINGS_KEY, "DVR Settings", {"series_rules": rules})
|
||||
return rules
|
||||
|
||||
# Proxy Settings
|
||||
@classmethod
|
||||
def get_proxy_settings(cls):
|
||||
"""Get proxy settings."""
|
||||
return cls._get_group(PROXY_SETTINGS_KEY, {
|
||||
"buffering_timeout": 15,
|
||||
"buffering_speed": 1.0,
|
||||
"redis_chunk_ttl": 60,
|
||||
"channel_shutdown_delay": 0,
|
||||
"channel_init_grace_period": 5,
|
||||
})
|
||||
|
||||
# System Settings
|
||||
@classmethod
|
||||
def get_system_settings(cls):
|
||||
"""Get all system-related settings."""
|
||||
return cls._get_group(SYSTEM_SETTINGS_KEY, {
|
||||
"time_zone": getattr(settings, "TIME_ZONE", "UTC") or "UTC",
|
||||
"max_system_events": 100,
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def get_system_time_zone(cls):
|
||||
return cls.get_system_settings().get("time_zone") or getattr(settings, "TIME_ZONE", "UTC") or "UTC"
|
||||
|
||||
@classmethod
|
||||
def set_system_time_zone(cls, tz_name: str | None):
|
||||
value = (tz_name or "").strip() or getattr(settings, "TIME_ZONE", "UTC") or "UTC"
|
||||
cls._update_group(SYSTEM_SETTINGS_KEY, "System Settings", {"time_zone": value})
|
||||
return value
|
||||
|
||||
|
||||
class SystemEvent(models.Model):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import json
|
|||
import ipaddress
|
||||
|
||||
from rest_framework import serializers
|
||||
from .models import CoreSettings, UserAgent, StreamProfile, NETWORK_ACCESS
|
||||
from .models import CoreSettings, UserAgent, StreamProfile, NETWORK_ACCESS_KEY
|
||||
|
||||
|
||||
class UserAgentSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -40,10 +40,10 @@ class CoreSettingsSerializer(serializers.ModelSerializer):
|
|||
fields = "__all__"
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
if instance.key == NETWORK_ACCESS:
|
||||
if instance.key == NETWORK_ACCESS_KEY:
|
||||
errors = False
|
||||
invalid = {}
|
||||
value = json.loads(validated_data.get("value"))
|
||||
value = validated_data.get("value")
|
||||
for key, val in value.items():
|
||||
cidrs = val.split(",")
|
||||
for cidr in cidrs:
|
||||
|
|
|
|||
|
|
@ -417,8 +417,12 @@ def log_system_event(event_type, channel_id=None, channel_name=None, **details):
|
|||
|
||||
# Get max events from settings (default 100)
|
||||
try:
|
||||
max_events_setting = CoreSettings.objects.filter(key='max-system-events').first()
|
||||
max_events = int(max_events_setting.value) if max_events_setting else 100
|
||||
from .models import CoreSettings
|
||||
system_settings = CoreSettings.objects.filter(key='system_settings').first()
|
||||
if system_settings and isinstance(system_settings.value, dict):
|
||||
max_events = int(system_settings.value.get('max_system_events', 100))
|
||||
else:
|
||||
max_events = 100
|
||||
except Exception:
|
||||
max_events = 100
|
||||
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ def stream_view(request, channel_uuid):
|
|||
stream_profile = channel.stream_profile
|
||||
if not stream_profile:
|
||||
logger.error("No stream profile set for channel ID=%s, using default", channel.id)
|
||||
stream_profile = StreamProfile.objects.get(id=CoreSettings.objects.get(key="default-stream-profile").value)
|
||||
stream_profile = StreamProfile.objects.get(id=CoreSettings.get_default_stream_profile_id())
|
||||
|
||||
logger.debug("Stream profile used: %s", stream_profile.name)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue