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:
SergeantPanda 2026-01-13 12:18:34 -06:00
parent 4bfdd15b37
commit 36967c10ce
32 changed files with 866 additions and 519 deletions

View file

@ -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:

View file

@ -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()

View 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),
]

View file

@ -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):

View file

@ -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:

View file

@ -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

View file

@ -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)