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
+65
View File
@@ -0,0 +1,65 @@
# Generated by Django 5.1.6 on 2025-03-05 22:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ServerGroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Unique name for this server group.', max_length=100, unique=True)),
],
),
migrations.CreateModel(
name='M3UAccount',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Unique name for this M3U account', max_length=255, unique=True)),
('server_url', models.URLField(blank=True, help_text='The base URL of the M3U server (optional if a file is uploaded)', null=True)),
('uploaded_file', models.FileField(blank=True, null=True, upload_to='m3u_uploads/')),
('max_streams', models.PositiveIntegerField(default=0, help_text='Maximum number of concurrent streams (0 for unlimited)')),
('is_active', models.BooleanField(default=True, help_text='Set to false to deactivate this M3U account')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='Time when this account was created')),
('updated_at', models.DateTimeField(auto_now=True, help_text='Time when this account was last updated')),
('user_agent', models.ForeignKey(blank=True, help_text='The User-Agent associated with this M3U account.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='core.useragent')),
('server_group', models.ForeignKey(blank=True, help_text='The server group this M3U account belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='m3u.servergroup')),
],
),
migrations.CreateModel(
name='M3UFilter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('filter_type', models.CharField(choices=[('group', 'Group Title'), ('name', 'Stream Name')], default='group', help_text='Filter based on either group title or stream name.', max_length=50)),
('regex_pattern', models.CharField(help_text='A regex pattern to match streams or groups.', max_length=200)),
('exclude', models.BooleanField(default=True, help_text='If True, matching items are excluded; if False, only matches are included.')),
('m3u_account', models.ForeignKey(help_text='The M3U account this filter is applied to.', on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='m3u.m3uaccount')),
],
),
migrations.CreateModel(
name='M3UAccountProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Name for the M3U account profile', max_length=255)),
('is_default', models.BooleanField(default=False, help_text='Set to false to deactivate this profile')),
('max_streams', models.PositiveIntegerField(default=0, help_text='Maximum number of concurrent streams (0 for unlimited)')),
('is_active', models.BooleanField(default=True, help_text='Set to false to deactivate this profile')),
('search_pattern', models.CharField(max_length=255)),
('replace_pattern', models.CharField(max_length=255)),
('current_viewers', models.PositiveIntegerField(default=0)),
('m3u_account', models.ForeignKey(help_text='The M3U account this profile belongs to.', on_delete=django.db.models.deletion.CASCADE, related_name='profiles', to='m3u.m3uaccount')),
],
options={
'constraints': [models.UniqueConstraint(fields=('m3u_account', 'name'), name='unique_account_name')],
},
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-03-17 20:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='m3uaccount',
name='locked',
field=models.BooleanField(default=False, help_text="Protected - can't be deleted or modified"),
),
]
@@ -0,0 +1,40 @@
# Generated by Django 5.1.6 on 2025-03-01 14:01
from django.db import migrations
from core.models import CoreSettings
def create_custom_account(apps, schema_editor):
default_user_agent_id = CoreSettings.get_default_user_agent_id()
M3UAccount = apps.get_model("m3u", "M3UAccount")
m3u_account = M3UAccount.objects.create(
name="custom",
max_streams=0,
is_active=True,
user_agent_id=default_user_agent_id,
locked=True,
)
M3UAccountProfile = apps.get_model("m3u", "M3UAccountProfile")
M3UAccountProfile.objects.create(
m3u_account=m3u_account,
name=f"{m3u_account.name} Default",
max_streams=m3u_account.max_streams,
is_default=True,
is_active=True,
search_pattern="^(.*)$",
replace_pattern="$1",
)
class Migration(migrations.Migration):
dependencies = [
("m3u", "0002_m3uaccount_locked"),
("core", "0004_preload_core_settings"),
]
operations = [
migrations.RunPython(create_custom_account),
]
@@ -0,0 +1,20 @@
# Generated by Django 5.1.6 on 2025-03-19 16:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0009_m3u_hash_settings'),
('m3u', '0003_create_custom_account'),
]
operations = [
migrations.AddField(
model_name='m3uaccount',
name='stream_profile',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='core.streamprofile'),
),
]
@@ -0,0 +1,35 @@
# Generated by Django 5.1.6 on 2025-03-29 13:44
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("django_celery_beat", "0019_alter_periodictasks_options"),
("m3u", "0004_m3uaccount_stream_profile"),
]
operations = [
migrations.AddField(
model_name="m3uaccount",
name="custom_properties",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name="m3uaccount",
name="refresh_interval",
field=models.IntegerField(default=24),
),
migrations.AddField(
model_name="m3uaccount",
name="refresh_task",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="django_celery_beat.periodictask",
),
),
]
@@ -0,0 +1,52 @@
from django.db import migrations
import json
def create_default_refresh_tasks(apps, schema_editor):
"""
Creates a PeriodicTask for each existing M3UAccount that doesn't have one.
"""
IntervalSchedule = apps.get_model("django_celery_beat", "IntervalSchedule")
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
M3UAccount = apps.get_model("m3u", "M3UAccount")
default_interval, _ = IntervalSchedule.objects.get_or_create(
every=24,
period="hours",
)
for account in M3UAccount.objects.all():
if account.refresh_task:
continue
task_name = f"m3u_account-refresh-{account.id}"
refresh_task = PeriodicTask.objects.create(
name=task_name,
interval=default_interval,
task="apps.m3u.tasks.refresh_single_m3u_account",
kwargs=json.dumps({"account_id": account.id}),
)
account.refresh_task = refresh_task
account.save(update_fields=["refresh_task"])
def reverse_migration(apps, schema_editor):
IntervalSchedule = apps.get_model("django_celery_beat", "IntervalSchedule")
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
M3UAccount = apps.get_model("m3u", "M3UAccount")
for account in M3UAccount.objects.all():
IntervalSchedule.objects.filter(name=f"m3u_account-refresh-interval-{account.id}").delete()
PeriodicTask.objects.filter(name=f"m3u_account-refresh-{account.id}").delete()
class Migration(migrations.Migration):
dependencies = [
("m3u", "0005_m3uaccount_custom_properties_and_more"),
("django_celery_beat", "0019_alter_periodictasks_options"),
]
operations = [
migrations.RunPython(create_default_refresh_tasks, reverse_migration),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-04-06 19:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0006_populate_periodic_tasks'),
]
operations = [
migrations.RemoveField(
model_name='m3uaccount',
name='uploaded_file',
),
migrations.AddField(
model_name='m3uaccount',
name='file_path',
field=models.CharField(blank=True, max_length=255, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0007_remove_m3uaccount_uploaded_file_m3uaccount_file_path'),
]
operations = [
migrations.AddField(
model_name='m3uaccount',
name='stale_stream_days',
field=models.PositiveIntegerField(default=7, help_text='Number of days after which a stream will be removed if not seen in the M3U source.'),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-04-27 12:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0008_m3uaccount_stale_stream_days'),
]
operations = [
migrations.AddField(
model_name='m3uaccount',
name='account_type',
field=models.CharField(choices=[('STD', 'Standard'), ('XC', 'Xtream Codes')], default='STD'),
),
migrations.AddField(
model_name='m3uaccount',
name='password',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='m3uaccount',
name='username',
field=models.CharField(blank=True, max_length=255, null=True),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.1.6 on 2025-05-04 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0009_m3uaccount_account_type_m3uaccount_password_and_more'),
]
operations = [
migrations.AddField(
model_name='m3uaccount',
name='last_message',
field=models.TextField(blank=True, null=True, help_text="Last status message, including success results or error information"),
),
migrations.AddField(
model_name='m3uaccount',
name='status',
field=models.CharField(choices=[('idle', 'Idle'), ('fetching', 'Fetching'), ('parsing', 'Parsing'), ('error', 'Error'), ('success', 'Success')], default='idle', max_length=20),
),
migrations.AlterField(
model_name='m3uaccount',
name='updated_at',
field=models.DateTimeField(blank=True, help_text='Time when this account was last successfully refreshed', null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-05-15 01:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0010_add_status_fields_and_remove_auto_now'),
]
operations = [
migrations.AlterField(
model_name='m3uaccount',
name='status',
field=models.CharField(choices=[('idle', 'Idle'), ('fetching', 'Fetching'), ('parsing', 'Parsing'), ('error', 'Error'), ('success', 'Success'), ('pending_setup', 'Pending Setup'), ('disabled', 'Disabled')], default='idle', max_length=20),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-05-21 19:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0011_alter_m3uaccount_status'),
]
operations = [
migrations.AlterField(
model_name='m3uaccount',
name='refresh_interval',
field=models.IntegerField(default=0),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-07-22 21:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0012_alter_m3uaccount_refresh_interval'),
]
operations = [
migrations.AlterField(
model_name='m3ufilter',
name='filter_type',
field=models.CharField(choices=[('group', 'Group'), ('name', 'Stream Name'), ('url', 'Stream URL')], default='group', help_text='Filter based on either group title or stream name.', max_length=50),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.1.6 on 2025-07-31 17:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0013_alter_m3ufilter_filter_type'),
]
operations = [
migrations.AlterModelOptions(
name='m3ufilter',
options={'ordering': ['order']},
),
migrations.AddField(
model_name='m3ufilter',
name='order',
field=models.PositiveIntegerField(default=0),
),
]
@@ -0,0 +1,22 @@
# Generated by Django 5.2.4 on 2025-08-02 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0014_alter_m3ufilter_options_m3ufilter_order'),
]
operations = [
migrations.AlterModelOptions(
name='m3ufilter',
options={},
),
migrations.AddField(
model_name='m3ufilter',
name='custom_properties',
field=models.TextField(blank=True, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-20 22:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0015_alter_m3ufilter_options_m3ufilter_custom_properties'),
]
operations = [
migrations.AddField(
model_name='m3uaccount',
name='priority',
field=models.PositiveIntegerField(default=0, help_text='Priority for VOD provider selection (higher numbers = higher priority). Used when multiple providers offer the same content.'),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 5.2.4 on 2025-09-02 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0016_m3uaccount_priority'),
]
operations = [
migrations.AlterField(
model_name='m3uaccount',
name='custom_properties',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AlterField(
model_name='m3uaccount',
name='server_url',
field=models.URLField(blank=True, help_text='The base URL of the M3U server (optional if a file is uploaded)', max_length=1000, null=True),
),
migrations.AlterField(
model_name='m3ufilter',
name='custom_properties',
field=models.JSONField(blank=True, default=dict, null=True),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-09-09 20:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0017_alter_m3uaccount_custom_properties_and_more'),
]
operations = [
migrations.AddField(
model_name='m3uaccountprofile',
name='custom_properties',
field=models.JSONField(blank=True, default=dict, help_text='Custom properties for storing account information from provider (e.g., XC account details, expiration dates)', null=True),
),
]
@@ -0,0 +1,57 @@
# Generated by Django 6.0.3 on 2026-03-14 19:41
from datetime import datetime, timezone
from django.db import migrations, models
def populate_exp_date_from_custom_properties(apps, schema_editor):
"""Backfill exp_date from custom_properties['user_info']['exp_date']."""
M3UAccountProfile = apps.get_model('m3u', 'M3UAccountProfile')
profiles_to_update = []
for profile in M3UAccountProfile.objects.filter(
custom_properties__isnull=False,
).exclude(custom_properties={}):
user_info = profile.custom_properties.get('user_info', {})
raw_exp = user_info.get('exp_date')
if raw_exp is None:
continue
parsed = None
try:
if isinstance(raw_exp, (int, float)):
parsed = datetime.fromtimestamp(float(raw_exp), tz=timezone.utc)
elif isinstance(raw_exp, str):
try:
parsed = datetime.fromtimestamp(float(raw_exp), tz=timezone.utc)
except ValueError:
parsed = datetime.fromisoformat(raw_exp)
except (ValueError, TypeError, OSError):
pass
if parsed is not None:
profile.exp_date = parsed
profiles_to_update.append(profile)
if profiles_to_update:
M3UAccountProfile.objects.bulk_update(profiles_to_update, ['exp_date'], batch_size=500)
class Migration(migrations.Migration):
dependencies = [
('m3u', '0018_add_profile_custom_properties'),
]
operations = [
migrations.AddField(
model_name='m3uaccountprofile',
name='exp_date',
field=models.DateTimeField(blank=True, help_text='Account expiration date, auto-synced from custom_properties on save', null=True),
),
migrations.RunPython(
populate_exp_date_from_custom_properties,
reverse_code=migrations.RunPython.noop,
),
]
View File