This commit is contained in:
SergeantPanda 2025-03-07 08:34:21 -06:00
commit fe83e8a03e
49 changed files with 813 additions and 401 deletions

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 13:52
# Generated by Django 5.1.6 on 2025-03-05 22:07
import django.contrib.auth.models
import django.contrib.auth.validators
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('channels', '0001_initial'),
('dispatcharr_channels', '0001_initial'),
]
operations = [
@ -31,7 +31,7 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('avatar_config', models.JSONField(blank=True, default=dict, null=True)),
('channel_groups', models.ManyToManyField(blank=True, related_name='users', to='channels.channelgroup')),
('channel_groups', models.ManyToManyField(blank=True, related_name='users', to='dispatcharr_channels.channelgroup')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],

View file

@ -9,7 +9,7 @@ class User(AbstractUser):
"""
avatar_config = models.JSONField(default=dict, blank=True, null=True)
channel_groups = models.ManyToManyField(
'channels.ChannelGroup', # Updated reference to renamed model
'dispatcharr_channels.ChannelGroup', # Updated reference to renamed model
blank=True,
related_name="users"
)

View file

@ -4,7 +4,8 @@ class ChannelsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.channels'
verbose_name = "Channel & Stream Management"
label = 'dispatcharr_channels'
def ready(self):
# Import signals so they get registered.
import apps.channels.signals

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 13:52
# Generated by Django 5.1.6 on 2025-03-05 22:07
import django.db.models.deletion
from django.db import migrations, models
@ -32,7 +32,7 @@ class Migration(migrations.Migration):
('tvg_id', models.CharField(blank=True, max_length=255, null=True)),
('tvg_name', models.CharField(blank=True, max_length=255, null=True)),
('stream_profile', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channels', to='core.streamprofile')),
('channel_group', models.ForeignKey(blank=True, help_text='Channel group this channel belongs to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channels', to='channels.channelgroup')),
('channel_group', models.ForeignKey(blank=True, help_text='Channel group this channel belongs to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channels', to='dispatcharr_channels.channelgroup')),
],
),
migrations.CreateModel(
@ -62,8 +62,8 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.PositiveIntegerField(default=0)),
('channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='channels.channel')),
('stream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='channels.stream')),
('channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dispatcharr_channels.channel')),
('stream', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dispatcharr_channels.stream')),
],
options={
'ordering': ['order'],
@ -72,6 +72,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='channel',
name='streams',
field=models.ManyToManyField(blank=True, related_name='channels', through='channels.ChannelStream', to='channels.stream'),
field=models.ManyToManyField(blank=True, related_name='channels', through='dispatcharr_channels.ChannelStream', to='dispatcharr_channels.stream'),
),
]

View file

@ -44,6 +44,7 @@ class StreamSerializer(serializers.ModelSerializer):
return fields
#
# Channel Group
#
@ -74,7 +75,8 @@ class ChannelSerializer(serializers.ModelSerializer):
)
streams = serializers.ListField(
child=serializers.IntegerField(), write_only=True
child=serializers.IntegerField(),
write_only=True
)
stream_ids = serializers.SerializerMethodField()
@ -111,17 +113,28 @@ class ChannelSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
print("Validated Data:", validated_data)
stream_ids = validated_data.get('streams', None)
stream_ids = validated_data.pop('streams', None)
print(f'stream ids: {stream_ids}')
# Update basic fields
instance.name = validated_data.get('channel_name', instance.channel_name)
# Update the actual Channel fields
instance.channel_number = validated_data.get('channel_number', instance.channel_number)
instance.channel_name = validated_data.get('channel_name', instance.channel_name)
instance.logo_url = validated_data.get('logo_url', instance.logo_url)
instance.tvg_id = validated_data.get('tvg_id', instance.tvg_id)
instance.tvg_name = validated_data.get('tvg_name', instance.tvg_name)
# If serializer allows changing channel_group or stream_profile:
if 'channel_group' in validated_data:
instance.channel_group = validated_data['channel_group']
if 'stream_profile' in validated_data:
instance.stream_profile = validated_data['stream_profile']
instance.save()
# Handle the many-to-many 'streams'
if stream_ids is not None:
# Clear existing relationships
instance.channelstream_set.all().delete()
# Add new streams in order
for index, stream_id in enumerate(stream_ids):
ChannelStream.objects.create(channel=instance, stream_id=stream_id, order=index)

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 13:52
# Generated by Django 5.1.6 on 2025-03-05 22:07
from django.db import migrations, models

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 13:52
# Generated by Django 5.1.6 on 2025-03-05 22:07
import django.db.models.deletion
from django.db import migrations, models

View file

@ -1,4 +1,5 @@
import logging
import gzip # <-- New import for gzip support
from celery import shared_task
from .models import EPGSource, EPGData, ProgramData
from django.utils import timezone
@ -29,7 +30,16 @@ def fetch_xmltv(source):
response = requests.get(source.url, timeout=30)
response.raise_for_status()
logger.debug("XMLTV data fetched successfully.")
root = ET.fromstring(response.content)
# If the URL ends with '.gz', decompress the response content
if source.url.lower().endswith('.gz'):
logger.debug("Detected .gz file. Decompressing...")
decompressed_bytes = gzip.decompress(response.content)
xml_data = decompressed_bytes.decode('utf-8')
else:
xml_data = response.text
root = ET.fromstring(xml_data)
logger.debug("Parsed XMLTV XML content.")
# Group programmes by their tvg_id from the XMLTV file

View file

@ -81,7 +81,7 @@ class LineupAPIView(APIView):
{
"GuideNumber": str(ch.channel_number),
"GuideName": ch.channel_name,
"URL": request.build_absolute_uri(f"/player/stream/{ch.id}")
"URL": request.build_absolute_uri(f"/output/stream/{ch.id}")
}
for ch in channels
]

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 13:52
# Generated by Django 5.1.6 on 2025-03-05 22:07
from django.db import migrations, models

View file

@ -81,7 +81,7 @@ class LineupAPIView(APIView):
{
"GuideNumber": str(ch.channel_number),
"GuideName": ch.channel_name,
"URL": request.build_absolute_uri(f"/player/stream/{ch.id}")
"URL": request.build_absolute_uri(f"/output/stream/{ch.id}")
}
for ch in channels
]

View file

@ -1,4 +1,4 @@
# Generated by Django 5.1.6 on 2025-03-02 13:52
# Generated by Django 5.1.6 on 2025-03-05 22:07
import django.db.models.deletion
from django.db import migrations, models

View file

@ -9,6 +9,8 @@ from django.conf import settings
from django.core.cache import cache
from .models import M3UAccount
from apps.channels.models import Stream
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
logger = logging.getLogger(__name__)
@ -120,7 +122,7 @@ def refresh_single_m3u_account(account_id):
# Extract tvg-id
tvg_id_match = re.search(r'tvg-id="([^"]*)"', line)
tvg_id = tvg_id_match.group(1) if tvg_id_match else ""
fallback_name = line.split(",", 1)[-1].strip() if "," in line else "Default Stream"
name = tvg_name_match.group(1) if tvg_name_match else fallback_name
@ -178,6 +180,14 @@ def refresh_single_m3u_account(account_id):
logger.info(f"Completed parsing. Created {created_count} new Streams, updated {updated_count} existing Streams, excluded {excluded_count} Streams.")
release_lock('refresh_single_m3u_account', account_id)
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
"updates",
{
"type": "m3u_refresh",
"message": {"success": True, "message": "M3U refresh completed successfully"}
},
)
return f"Account {account_id} => Created {created_count}, updated {updated_count}, excluded {excluded_count} Streams."
def process_uploaded_m3u_file(file, account):