Proyecto LCX Dispatcharr multicuenta
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled

This commit is contained in:
root
2026-05-09 21:24:50 +02:00
commit f56b088643
721 changed files with 177870 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Generated by Django 5.1.6 on 2025-03-05 22:07
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='CoreSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('key', models.CharField(max_length=255, unique=True)),
('name', models.CharField(max_length=255)),
('value', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name='StreamProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('profile_name', models.CharField(help_text='Name of the stream profile', max_length=255)),
('command', models.CharField(help_text="Command to execute (e.g., 'yt.sh', 'streamlink', or 'vlc')", max_length=255)),
('parameters', models.TextField(help_text='Command-line parameters. Use {userAgent} and {streamUrl} as placeholders.')),
('is_active', models.BooleanField(default=True, help_text='Whether this profile is active')),
('user_agent', models.CharField(blank=True, help_text='Optional user agent to use. If not set, you can fall back to a default.', max_length=512, null=True)),
],
),
migrations.CreateModel(
name='UserAgent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_agent_name', models.CharField(help_text='The User-Agent name.', max_length=512, unique=True)),
('user_agent', models.CharField(help_text='The complete User-Agent string sent by the client.', max_length=512, unique=True)),
('description', models.CharField(blank=True, help_text='An optional description of the client or device type.', max_length=255)),
('is_active', models.BooleanField(default=True, help_text='Whether this user agent is currently allowed/recognized.')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
),
]
@@ -0,0 +1,36 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
def preload_user_agent(apps, schema_editor):
UserAgent = apps.get_model("core", "UserAgent")
UserAgent.objects.create(
user_agent_name="TiviMate",
user_agent="TiviMate/5.16 (Android 12)",
description="",
is_active=True,
)
UserAgent.objects.create(
user_agent_name="VLC",
user_agent="VLC/3.0.21 LibVLC/3.0.21",
description="",
is_active=True,
)
UserAgent.objects.create(
user_agent_name="Chrome",
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.3",
description="",
is_active=True,
)
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.RunPython(preload_user_agent),
]
@@ -0,0 +1,31 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
def preload_stream_profiles(apps, schema_editor):
StreamProfile = apps.get_model("core", "StreamProfile")
StreamProfile.objects.create(
profile_name="ffmpeg",
command="ffmpeg",
parameters="-i {streamUrl} -c:v copy -c:a copy -f mpegts pipe:1",
is_active=True,
user_agent="1",
)
StreamProfile.objects.create(
profile_name="streamlink",
command="streamlink",
parameters="{streamUrl} best --stdout",
is_active=True,
user_agent="1",
)
class Migration(migrations.Migration):
dependencies = [
('core', '0002_preload_user_agents'),
]
operations = [
migrations.RunPython(preload_stream_profiles),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from django.utils.text import slugify
def preload_core_settings(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
CoreSettings.objects.create(
key=slugify("Default Stream Profile"),
name="Default Stream Profile",
value=1,
)
CoreSettings.objects.create(
key=slugify("Default User-Agent"),
name="Default User-Agent",
value=1,
)
class Migration(migrations.Migration):
dependencies = [
('core', '0003_preload_stream_profiles'),
]
operations = [
migrations.RunPython(preload_core_settings),
]
@@ -0,0 +1,34 @@
# Generated by Django 5.1.6 on 2025-03-14 18:12
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_preload_core_settings'),
]
operations = [
migrations.AddField(
model_name='streamprofile',
name='locked',
field=models.BooleanField(default=False, help_text="Protected - can't be deleted or modified"),
),
migrations.AlterField(
model_name='streamprofile',
name='command',
field=models.CharField(blank=True, help_text="Command to execute (e.g., 'yt.sh', 'streamlink', or 'vlc')", max_length=255),
),
migrations.AlterField(
model_name='streamprofile',
name='parameters',
field=models.TextField(blank=True, help_text='Command-line parameters. Use {userAgent} and {streamUrl} as placeholders.'),
),
migrations.AlterField(
model_name='streamprofile',
name='user_agent',
field=models.ForeignKey(blank=True, help_text='Optional user agent to use. If not set, you can fall back to a default.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.useragent'),
),
]
@@ -0,0 +1,59 @@
# Generated by Django 5.1.6 on 2025-03-14 17:46
from django.db import migrations, models
from core.models import CoreSettings, StreamProfile
def lock_or_create_profiles(apps, schema_editor):
StreamProfile = apps.get_model("core", "StreamProfile")
UserAgent = apps.get_model("core", "UserAgent")
# Define the system profiles that should exist
system_profiles = [
{
"name": "ffmpeg",
"command": "ffmpeg",
"parameters": "-i {streamUrl} -c:v copy -c:a copy -f mpegts pipe:1",
"new_parameters": "-user_agent {userAgent} -i {streamUrl} -c copy -f mpegts pipe:1",
},
{
"name": "streamlink",
"command": "streamlink",
"parameters": "{streamUrl} best --stdout",
"new_parameters": "{streamUrl} --http-header {userAgent} best --stdout",
},
]
for profile_data in system_profiles:
existing_profile = StreamProfile.objects.filter(
profile_name=profile_data["name"],
command=profile_data["command"],
parameters=profile_data["parameters"],
).first()
if existing_profile:
# Lock existing profile
existing_profile.locked = True
existing_profile.parameters = profile_data["new_parameters"]
existing_profile.save()
else:
# Create a new locked profile
new_profile = StreamProfile.objects.create(
profile_name=profile_data["name"],
command=profile_data["command"],
parameters=profile_data["new_parameters"],
locked=True,
)
def reverse_migration(apps, schema_editor):
# No need to reverse changes
pass
class Migration(migrations.Migration):
dependencies = [
('core', '0005_streamprofile_locked_alter_streamprofile_command_and_more'),
]
operations = [
migrations.RunPython(lock_or_create_profiles, reverse_code=reverse_migration),
]
@@ -0,0 +1,36 @@
# Generated by Django 5.1.6 on 2025-03-14 17:16
from django.db import migrations
from core.models import CoreSettings
def create_proxy_stream_profile(apps, schema_editor):
default_user_agent_id = CoreSettings.get_default_user_agent_id()
StreamProfile = apps.get_model("core", "StreamProfile")
StreamProfile.objects.create(
profile_name="Proxy",
command="",
parameters="",
locked=True,
is_active=True,
user_agent_id=default_user_agent_id,
)
StreamProfile.objects.create(
profile_name="Redirect",
command="",
parameters="",
locked=True,
is_active=True,
user_agent_id=default_user_agent_id,
)
class Migration(migrations.Migration):
dependencies = [
('core', '0006_set_locked_stream_profiles'),
]
operations = [
migrations.RunPython(create_proxy_stream_profile)
]
@@ -0,0 +1,23 @@
# Generated by Django 5.1.6 on 2025-03-16 12:21
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('core', '0007_create_proxy_and_redirect_stream_profiles'),
]
operations = [
migrations.RenameField(
model_name='streamprofile',
old_name='profile_name',
new_name='name',
),
migrations.RenameField(
model_name='useragent',
old_name='user_agent_name',
new_name='name',
),
]
+22
View File
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from django.utils.text import slugify
def preload_core_settings(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
CoreSettings.objects.create(
key=slugify("M3U Hash Key"),
name="M3U Hash Key",
value="url",
)
class Migration(migrations.Migration):
dependencies = [
('core', '0008_rename_profile_name_streamprofile_name_and_more'),
]
operations = [
migrations.RunPython(preload_core_settings),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from django.utils.text import slugify
def preload_core_settings(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
CoreSettings.objects.create(
key=slugify("Preferred Region"),
name="Preferred Region",
value="us",
)
class Migration(migrations.Migration):
dependencies = [
('core', '0009_m3u_hash_settings'),
]
operations = [
migrations.RunPython(preload_core_settings),
]
@@ -0,0 +1,27 @@
# Generated by Django 5.1.6 on 2025-04-04
from django.db import migrations
def fix_stream_profiles_and_user_agents(apps, schema_editor):
# Get the model
StreamProfile = apps.get_model("core", "StreamProfile")
streamlink_profile = StreamProfile.objects.get(name="streamlink", locked=True)
streamlink_profile.parameters = "{streamUrl} --http-header User-Agent={userAgent} best --stdout"
streamlink_profile.save()
UserAgent = apps.get_model("core", "UserAgent")
tivimate = UserAgent.objects.get(name="TiviMate", user_agent="TiviMate/5.16 (Android 12)")
if tivimate:
tivimate.user_agent = "TiviMate/5.1.6 (Android 12)"
tivimate.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0010_reload_additional_settings'),
]
operations = [
migrations.RunPython(fix_stream_profiles_and_user_agents),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from django.utils.text import slugify
def preload_core_settings(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
CoreSettings.objects.create(
key=slugify("Auto-Import Mapped Files"),
name="Auto-Import Mapped Files",
value=True,
)
class Migration(migrations.Migration):
dependencies = [
('core', '0011_fix_stream_profiles_and_user_agents'),
]
operations = [
migrations.RunPython(preload_core_settings),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from django.utils.text import slugify
def preload_network_access_settings(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
CoreSettings.objects.create(
key=slugify("Network Access"),
name="Network Access",
value="{}",
)
class Migration(migrations.Migration):
dependencies = [
("core", "0012_default_active_m3u_accounts"),
]
operations = [
migrations.RunPython(preload_network_access_settings),
]
@@ -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),
]
+30
View File
@@ -0,0 +1,30 @@
# Generated by Django 5.1.6 on 2025-03-01 14:10
from django.db import migrations
from django.utils.text import slugify
def add_dvr_defaults(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
defaults = [
(slugify("DVR TV Template"), "DVR TV Template", "Recordings/TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"),
(slugify("DVR Movie Template"), "DVR Movie Template", "Recordings/Movies/{title} ({year}).mkv"),
(slugify("DVR TV Fallback Template"), "DVR TV Fallback Template", "Recordings/TV_Shows/{show}/{start}.mkv"),
(slugify("DVR Movie Fallback Template"), "DVR Movie Fallback Template", "Recordings/Movies/{start}.mkv"),
(slugify("DVR Comskip Enabled"), "DVR Comskip Enabled", "false"),
]
for key, name, value in defaults:
CoreSettings.objects.get_or_create(key=key, defaults={"name": name, "value": value})
class Migration(migrations.Migration):
dependencies = [
("core", "0014_default_proxy_settings"),
]
operations = [
migrations.RunPython(add_dvr_defaults),
]
@@ -0,0 +1,61 @@
# Generated manually to update DVR template paths
from django.db import migrations
from django.utils.text import slugify
def update_dvr_template_paths(apps, schema_editor):
"""Remove 'Recordings/' prefix from DVR template paths"""
CoreSettings = apps.get_model("core", "CoreSettings")
# Define the updates needed
updates = [
(slugify("DVR TV Template"), "TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"),
(slugify("DVR Movie Template"), "Movies/{title} ({year}).mkv"),
(slugify("DVR TV Fallback Template"), "TV_Shows/{show}/{start}.mkv"),
(slugify("DVR Movie Fallback Template"), "Movies/{start}.mkv"),
]
# Update each setting
for key, new_value in updates:
try:
setting = CoreSettings.objects.get(key=key)
setting.value = new_value
setting.save()
print(f"Updated {setting.name}: {new_value}")
except CoreSettings.DoesNotExist:
print(f"Setting with key '{key}' not found - skipping")
def reverse_dvr_template_paths(apps, schema_editor):
"""Add back 'Recordings/' prefix to DVR template paths"""
CoreSettings = apps.get_model("core", "CoreSettings")
# Define the reverse updates (add back Recordings/ prefix)
updates = [
(slugify("DVR TV Template"), "Recordings/TV_Shows/{show}/S{season:02d}E{episode:02d}.mkv"),
(slugify("DVR Movie Template"), "Recordings/Movies/{title} ({year}).mkv"),
(slugify("DVR TV Fallback Template"), "Recordings/TV_Shows/{show}/{start}.mkv"),
(slugify("DVR Movie Fallback Template"), "Recordings/Movies/{start}.mkv"),
]
# Update each setting back to original
for key, original_value in updates:
try:
setting = CoreSettings.objects.get(key=key)
setting.value = original_value
setting.save()
print(f"Reverted {setting.name}: {original_value}")
except CoreSettings.DoesNotExist:
print(f"Setting with key '{key}' not found - skipping")
class Migration(migrations.Migration):
dependencies = [
("core", "0015_dvr_templates"),
]
operations = [
migrations.RunPython(update_dvr_template_paths, reverse_dvr_template_paths),
]
+28
View File
@@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-11-20 20:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0016_update_dvr_template_paths'),
]
operations = [
migrations.CreateModel(
name='SystemEvent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event_type', models.CharField(choices=[('channel_start', 'Channel Started'), ('channel_stop', 'Channel Stopped'), ('channel_buffering', 'Channel Buffering'), ('channel_failover', 'Channel Failover'), ('channel_reconnect', 'Channel Reconnected'), ('channel_error', 'Channel Error'), ('client_connect', 'Client Connected'), ('client_disconnect', 'Client Disconnected'), ('recording_start', 'Recording Started'), ('recording_end', 'Recording Ended'), ('stream_switch', 'Stream Switched'), ('m3u_refresh', 'M3U Refreshed'), ('m3u_download', 'M3U Downloaded'), ('epg_refresh', 'EPG Refreshed'), ('epg_download', 'EPG Downloaded')], db_index=True, max_length=50)),
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
('channel_id', models.UUIDField(blank=True, db_index=True, null=True)),
('channel_name', models.CharField(blank=True, max_length=255, null=True)),
('details', models.JSONField(blank=True, default=dict)),
],
options={
'ordering': ['-timestamp'],
'indexes': [models.Index(fields=['-timestamp'], name='core_system_timesta_c6c3d1_idx'), models.Index(fields=['event_type', '-timestamp'], name='core_system_event_t_4267d9_idx')],
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-11-21 15:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0017_systemevent'),
]
operations = [
migrations.AlterField(
model_name='systemevent',
name='event_type',
field=models.CharField(choices=[('channel_start', 'Channel Started'), ('channel_stop', 'Channel Stopped'), ('channel_buffering', 'Channel Buffering'), ('channel_failover', 'Channel Failover'), ('channel_reconnect', 'Channel Reconnected'), ('channel_error', 'Channel Error'), ('client_connect', 'Client Connected'), ('client_disconnect', 'Client Disconnected'), ('recording_start', 'Recording Started'), ('recording_end', 'Recording Ended'), ('stream_switch', 'Stream Switched'), ('m3u_refresh', 'M3U Refreshed'), ('m3u_download', 'M3U Downloaded'), ('epg_refresh', 'EPG Refreshed'), ('epg_download', 'EPG Downloaded'), ('login_success', 'Login Successful'), ('login_failed', 'Login Failed'), ('logout', 'User Logged Out'), ('m3u_blocked', 'M3U Download Blocked'), ('epg_blocked', 'EPG Download Blocked')], db_index=True, max_length=50),
),
]
@@ -0,0 +1,42 @@
# Generated migration to add VLC stream profile
from django.db import migrations
def add_vlc_profile(apps, schema_editor):
StreamProfile = apps.get_model("core", "StreamProfile")
UserAgent = apps.get_model("core", "UserAgent")
# Check if VLC profile already exists
if not StreamProfile.objects.filter(name="VLC").exists():
# Get the TiviMate user agent (should be pk=1)
try:
tivimate_ua = UserAgent.objects.get(pk=1)
except UserAgent.DoesNotExist:
# Fallback: get first available user agent
tivimate_ua = UserAgent.objects.first()
if not tivimate_ua:
# No user agents exist, skip creating profile
return
StreamProfile.objects.create(
name="VLC",
command="cvlc",
parameters="-vv -I dummy --no-video-title-show --http-user-agent {userAgent} {streamUrl} --sout #standard{access=file,mux=ts,dst=-}",
is_active=True,
user_agent=tivimate_ua,
locked=True, # Make it read-only like ffmpeg/streamlink
)
def remove_vlc_profile(apps, schema_editor):
StreamProfile = apps.get_model("core", "StreamProfile")
StreamProfile.objects.filter(name="VLC").delete()
class Migration(migrations.Migration):
dependencies = [
('core', '0018_alter_systemevent_event_type'),
]
operations = [
migrations.RunPython(add_vlc_profile, remove_vlc_profile),
]
@@ -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),
]
@@ -0,0 +1,52 @@
# Generated by Django 5.2.9 on 2026-02-02 20:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0020_change_coresettings_value_to_jsonfield'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='SystemNotification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notification_key', models.CharField(db_index=True, max_length=255, unique=True)),
('notification_type', models.CharField(choices=[('version_update', 'Version Update Available'), ('setting_recommendation', 'Recommended Setting Change'), ('announcement', 'System Announcement'), ('warning', 'Warning'), ('info', 'Information')], db_index=True, default='info', max_length=50)),
('priority', models.CharField(choices=[('low', 'Low'), ('normal', 'Normal'), ('high', 'High'), ('critical', 'Critical')], default='normal', max_length=20)),
('source', models.CharField(choices=[('system', 'System Generated'), ('developer', 'Developer Notification')], db_index=True, default='system', max_length=20)),
('title', models.CharField(max_length=255)),
('message', models.TextField()),
('action_data', models.JSONField(blank=True, default=dict)),
('is_active', models.BooleanField(db_index=True, default=True)),
('admin_only', models.BooleanField(default=False)),
('expires_at', models.DateTimeField(blank=True, db_index=True, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['-priority', '-created_at'],
'indexes': [models.Index(fields=['is_active', '-created_at'], name='core_system_is_acti_afab03_idx'), models.Index(fields=['notification_type', 'is_active'], name='core_system_notific_2179e3_idx'), models.Index(fields=['source', 'is_active'], name='core_system_source_a35829_idx')],
},
),
migrations.CreateModel(
name='NotificationDismissal',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('dismissed_at', models.DateTimeField(auto_now_add=True)),
('action_taken', models.CharField(blank=True, max_length=50, null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dismissed_notifications', to=settings.AUTH_USER_MODEL)),
('notification', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dismissals', to='core.systemnotification')),
],
options={
'indexes': [models.Index(fields=['user', 'notification'], name='core_notifi_user_id_93e02e_idx')],
'unique_together': {('user', 'notification')},
},
),
]
@@ -0,0 +1,24 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from django.utils.text import slugify
def preload_user_limit_settings(apps, schema_editor):
CoreSettings = apps.get_model("core", "CoreSettings")
CoreSettings.objects.create(
key="user_limit_settings",
name="User Limit Settings",
value={},
)
class Migration(migrations.Migration):
dependencies = [
("core", "0021_systemnotification_notificationdismissal"),
]
operations = [
migrations.RunPython(preload_user_limit_settings),
]
View File