diff --git a/apps/channels/admin.py b/apps/channels/admin.py
index 49ef04a9..709dc81a 100644
--- a/apps/channels/admin.py
+++ b/apps/channels/admin.py
@@ -5,27 +5,27 @@ from .models import Stream, Channel, ChannelGroup
class StreamAdmin(admin.ModelAdmin):
list_display = (
'id', # Primary Key
- 'name',
- 'group_name',
- 'custom_url',
- 'current_viewers',
+ 'name',
+ 'group_name',
+ 'url',
+ 'current_viewers',
'updated_at',
)
list_filter = ('group_name',)
- search_fields = ('id', 'name', 'custom_url', 'group_name') # Added 'id' for searching by ID
+ search_fields = ('id', 'name', 'url', 'group_name') # Added 'id' for searching by ID
ordering = ('-updated_at',)
@admin.register(Channel)
class ChannelAdmin(admin.ModelAdmin):
list_display = (
'id', # Primary Key
- 'channel_number',
- 'channel_name',
- 'channel_group',
+ 'channel_number',
+ 'name',
+ 'channel_group',
'tvg_name'
)
list_filter = ('channel_group',)
- search_fields = ('id', 'channel_name', 'channel_group__name', 'tvg_name') # Added 'id'
+ search_fields = ('id', 'name', 'channel_group__name', 'tvg_name') # Added 'id'
ordering = ('channel_number',)
@admin.register(ChannelGroup)
diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py
index 27b07af5..9744f7f0 100644
--- a/apps/channels/api_views.py
+++ b/apps/channels/api_views.py
@@ -145,7 +145,7 @@ class ChannelViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER,
description="(Optional) Desired channel number. Must not be in use."
),
- "channel_name": openapi.Schema(
+ "name": openapi.Schema(
type=openapi.TYPE_STRING, description="Desired channel name"
)
}
@@ -176,13 +176,13 @@ class ChannelViewSet(viewsets.ModelViewSet):
status=status.HTTP_400_BAD_REQUEST
)
- channel_name = request.data.get('channel_name')
- if channel_name is None:
- channel_name = stream.name
+ name = request.data.get('name')
+ if name is None:
+ name = stream.name
channel_data = {
'channel_number': channel_number,
- 'channel_name': channel_name,
+ 'name': name,
'tvg_id': stream.tvg_id,
'channel_group_id': channel_group.id,
'logo_url': stream.logo_url,
@@ -199,7 +199,7 @@ class ChannelViewSet(viewsets.ModelViewSet):
operation_description=(
"Bulk create channels from existing streams. For each object, if 'channel_number' is provided, "
"it is used (if available); otherwise, the next available number is auto-assigned. "
- "Each object must include 'stream_id' and 'channel_name'."
+ "Each object must include 'stream_id' and 'name'."
),
request_body=openapi.Schema(
type=openapi.TYPE_ARRAY,
@@ -214,7 +214,7 @@ class ChannelViewSet(viewsets.ModelViewSet):
type=openapi.TYPE_INTEGER,
description="(Optional) Desired channel number. Must not be in use."
),
- "channel_name": openapi.Schema(
+ "name": openapi.Schema(
type=openapi.TYPE_STRING, description="Desired channel name"
)
}
@@ -245,7 +245,7 @@ class ChannelViewSet(viewsets.ModelViewSet):
for item in data_list:
stream_id = item.get('stream_id')
if not all([stream_id]):
- errors.append({"item": item, "error": "Missing required fields: stream_id and channel_name are required."})
+ errors.append({"item": item, "error": "Missing required fields: stream_id and name are required."})
continue
try:
@@ -271,13 +271,13 @@ class ChannelViewSet(viewsets.ModelViewSet):
continue
used_numbers.add(channel_number)
- channel_name = item.get('channel_name')
- if channel_name is None:
- channel_name = stream.name
+ name = item.get('name')
+ if name is None:
+ name = stream.name
channel_data = {
"channel_number": channel_number,
- "channel_name": channel_name,
+ "name": name,
"tvg_id": stream.tvg_id,
"channel_group_id": channel_group.id,
"logo_url": stream.logo_url,
diff --git a/apps/channels/forms.py b/apps/channels/forms.py
index 171c77d9..baf169af 100644
--- a/apps/channels/forms.py
+++ b/apps/channels/forms.py
@@ -25,7 +25,7 @@ class ChannelForm(forms.ModelForm):
model = Channel
fields = [
'channel_number',
- 'channel_name',
+ 'name',
'channel_group',
]
@@ -39,7 +39,6 @@ class StreamForm(forms.ModelForm):
fields = [
'name',
'url',
- 'custom_url',
'logo_url',
'tvg_id',
'local_file',
diff --git a/apps/channels/migrations/0002_rename_channel_name_channel_name_and_more.py b/apps/channels/migrations/0002_rename_channel_name_channel_name_and_more.py
new file mode 100644
index 00000000..55a134a2
--- /dev/null
+++ b/apps/channels/migrations/0002_rename_channel_name_channel_name_and_more.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.1.6 on 2025-03-16 12:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dispatcharr_channels', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='channel',
+ old_name='channel_name',
+ new_name='name',
+ ),
+ migrations.RemoveField(
+ model_name='stream',
+ name='url',
+ ),
+ migrations.RenameField(
+ model_name='stream',
+ old_name='custom_url',
+ new_name='url',
+ ),
+ ]
diff --git a/apps/channels/models.py b/apps/channels/models.py
index 54f8b11a..207f5b77 100644
--- a/apps/channels/models.py
+++ b/apps/channels/models.py
@@ -5,6 +5,7 @@ from django.conf import settings
from core.models import StreamProfile, CoreSettings
from core.utils import redis_client
import logging
+import uuid
logger = logging.getLogger(__name__)
@@ -16,8 +17,7 @@ class Stream(models.Model):
Represents a single stream (e.g. from an M3U source or custom URL).
"""
name = models.CharField(max_length=255, default="Default Stream")
- url = models.URLField()
- custom_url = models.URLField(max_length=2000, blank=True, null=True)
+ url = models.URLField(max_length=2000, blank=True, null=True)
m3u_account = models.ForeignKey(
M3UAccount,
on_delete=models.CASCADE,
@@ -38,14 +38,15 @@ class Stream(models.Model):
on_delete=models.SET_NULL,
related_name='streams'
)
+
class Meta:
- # If you use m3u_account, you might do unique_together = ('name','custom_url','m3u_account')
+ # If you use m3u_account, you might do unique_together = ('name','url','m3u_account')
verbose_name = "Stream"
verbose_name_plural = "Streams"
ordering = ['-updated_at']
def __str__(self):
- return self.name or self.custom_url or f"Stream ID {self.id}"
+ return self.name or self.url or f"Stream ID {self.id}"
class ChannelManager(models.Manager):
@@ -55,7 +56,7 @@ class ChannelManager(models.Manager):
class Channel(models.Model):
channel_number = models.IntegerField()
- channel_name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255)
logo_url = models.URLField(max_length=2000, blank=True, null=True)
logo_file = models.ImageField(
upload_to='logos/', # Will store in MEDIA_ROOT/logos
@@ -104,7 +105,7 @@ class Channel(models.Model):
)
def __str__(self):
- return f"{self.channel_number} - {self.channel_name}"
+ return f"{self.channel_number} - {self.name}"
def get_stream_profile(self):
stream_profile = self.stream_profile
diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py
index d58c7bcb..744e4504 100644
--- a/apps/channels/serializers.py
+++ b/apps/channels/serializers.py
@@ -19,7 +19,6 @@ class StreamSerializer(serializers.ModelSerializer):
'id',
'name',
'url',
- 'custom_url',
'm3u_account', # Uncomment if using M3U fields
'logo_url',
'tvg_id',
@@ -84,7 +83,7 @@ class ChannelSerializer(serializers.ModelSerializer):
fields = [
'id',
'channel_number',
- 'channel_name',
+ 'name',
'logo_url',
'logo_file',
'channel_group',
@@ -99,9 +98,6 @@ class ChannelSerializer(serializers.ModelSerializer):
def get_streams(self, obj):
"""Retrieve ordered stream objects for GET requests."""
ordered_streams = obj.streams.all().order_by('channelstream__order')
- print(f'Retrieving streams in order')
- for index, stream in enumerate(ordered_streams):
- print(f'Stream {stream.id}, index {index}')
return StreamSerializer(ordered_streams, many=True).data
# def get_stream_ids(self, obj):
@@ -119,13 +115,11 @@ class ChannelSerializer(serializers.ModelSerializer):
return channel
def update(self, instance, validated_data):
- print("Validated Data:", validated_data)
streams = validated_data.pop('stream_ids', None)
- print(f'stream ids: {streams}')
# 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.name = validated_data.get('name', instance.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)
diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py
index c4bf8177..c1d16e1b 100644
--- a/apps/channels/tasks.py
+++ b/apps/channels/tasks.py
@@ -31,7 +31,7 @@ COMMON_EXTRANEOUS_WORDS = [
"arabic", "latino", "film", "movie", "movies"
]
-def normalize_channel_name(name: str) -> str:
+def normalize_name(name: str) -> str:
"""
A more aggressive normalization that:
- Lowercases
@@ -90,8 +90,8 @@ def match_epg_channels():
epg_rows.append({
"epg_id": e.id,
"tvg_id": e.tvg_id or "", # e.g. "Fox News.us"
- "raw_name": e.channel_name,
- "norm_name": normalize_channel_name(e.channel_name),
+ "raw_name": e.name,
+ "norm_name": normalize_name(e.name),
})
# 2) Pre-encode embeddings if possible
@@ -115,16 +115,16 @@ def match_epg_channels():
epg_match = EPGData.objects.filter(tvg_id=chan.tvg_id).first()
if epg_match:
logger.info(
- f"Channel {chan.id} '{chan.channel_name}' => found EPG by tvg_id={chan.tvg_id}"
+ f"Channel {chan.id} '{chan.name}' => found EPG by tvg_id={chan.tvg_id}"
)
continue
# C) No valid tvg_id => name-based matching
- fallback_name = chan.tvg_name.strip() if chan.tvg_name else chan.channel_name
- norm_chan = normalize_channel_name(fallback_name)
+ fallback_name = chan.tvg_name.strip() if chan.tvg_name else chan.name
+ norm_chan = normalize_name(fallback_name)
if not norm_chan:
logger.info(
- f"Channel {chan.id} '{chan.channel_name}' => empty after normalization, skipping"
+ f"Channel {chan.id} '{chan.name}' => empty after normalization, skipping"
)
continue
diff --git a/apps/channels/views.py b/apps/channels/views.py
index 2292a128..b28cc123 100644
--- a/apps/channels/views.py
+++ b/apps/channels/views.py
@@ -15,7 +15,7 @@ class StreamDashboardView(View):
"""
def get(self, request, *args, **kwargs):
streams = Stream.objects.values(
- 'id', 'name', 'url', 'custom_url',
+ 'id', 'name', 'url',
'group_name', 'current_viewers'
)
return JsonResponse({'data': list(streams)}, safe=False)
@@ -38,4 +38,4 @@ class StreamDashboardView(View):
@login_required
def channels_dashboard_view(request):
- return render(request, 'channels/channels.html')
\ No newline at end of file
+ return render(request, 'channels/channels.html')
diff --git a/apps/dashboard/views.py b/apps/dashboard/views.py
index 8548f652..2d143114 100644
--- a/apps/dashboard/views.py
+++ b/apps/dashboard/views.py
@@ -23,7 +23,7 @@ def dashboard_view(request):
# Fetch active streams and related channels
active_streams = Stream.objects.filter(current_viewers__gt=0).prefetch_related('channels')
active_streams_list = [
- f"Stream {i + 1}: {stream.custom_url or 'Unknown'} ({stream.current_viewers} viewers)"
+ f"Stream {i + 1}: {stream.url or 'Unknown'} ({stream.current_viewers} viewers)"
for i, stream in enumerate(active_streams)
]
@@ -58,7 +58,7 @@ def live_dashboard_data(request):
active_streams = Stream.objects.filter(current_viewers__gt=0)
active_streams_list = [
- f"Stream {i + 1}: {stream.custom_url or 'Unknown'} ({stream.current_viewers} viewers)"
+ f"Stream {i + 1}: {stream.url or 'Unknown'} ({stream.current_viewers} viewers)"
for i, stream in enumerate(active_streams)
]
@@ -77,4 +77,3 @@ def live_dashboard_data(request):
"error": str(e)
}
return JsonResponse(data)
-
diff --git a/apps/epg/admin.py b/apps/epg/admin.py
index 54a9d7a6..24cc2823 100644
--- a/apps/epg/admin.py
+++ b/apps/epg/admin.py
@@ -11,7 +11,7 @@ class EPGSourceAdmin(admin.ModelAdmin):
class ProgramAdmin(admin.ModelAdmin):
list_display = ['title', 'get_epg_tvg_id', 'start_time', 'end_time']
list_filter = ['epg__tvg_id', 'tvg_id']
- search_fields = ['title', 'epg__channel_name']
+ search_fields = ['title', 'epg__name']
def get_epg_tvg_id(self, obj):
return obj.epg.tvg_id if obj.epg else ''
diff --git a/apps/epg/migrations/0001_initial.py b/apps/epg/migrations/0001_initial.py
index 7c77ba5e..fd49cb5d 100644
--- a/apps/epg/migrations/0001_initial.py
+++ b/apps/epg/migrations/0001_initial.py
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tvg_id', models.CharField(blank=True, max_length=255, null=True)),
- ('channel_name', models.CharField(max_length=255)),
+ ('name', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
diff --git a/apps/epg/models.py b/apps/epg/models.py
index 64961c89..7a6a49b4 100644
--- a/apps/epg/models.py
+++ b/apps/epg/models.py
@@ -17,12 +17,12 @@ class EPGSource(models.Model):
class EPGData(models.Model):
# Removed the Channel foreign key. We now just store the original tvg_id
- # and a channel_name (which might simply be the tvg_id if no real channel exists).
+ # and a name (which might simply be the tvg_id if no real channel exists).
tvg_id = models.CharField(max_length=255, null=True, blank=True)
- channel_name = models.CharField(max_length=255)
+ name = models.CharField(max_length=255)
def __str__(self):
- return f"EPG Data for {self.channel_name}"
+ return f"EPG Data for {self.name}"
class ProgramData(models.Model):
# Each programme is associated with an EPGData record.
diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py
index 4812a9b0..9a62e74e 100644
--- a/apps/epg/serializers.py
+++ b/apps/epg/serializers.py
@@ -17,8 +17,8 @@ class EPGDataSerializer(serializers.ModelSerializer):
channel = serializers.SerializerMethodField()
def get_channel(self, obj):
- return {"id": obj.channel.id, "name": obj.channel.channel_name} if obj.channel else None
+ return {"id": obj.channel.id, "name": obj.channel.name} if obj.channel else None
class Meta:
model = EPGData
- fields = ['id', 'channel', 'channel_name', 'programs']
+ fields = ['id', 'channel', 'name', 'programs']
diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py
index 532b4de0..4d62f556 100644
--- a/apps/epg/tasks.py
+++ b/apps/epg/tasks.py
@@ -30,7 +30,7 @@ def fetch_xmltv(source):
response = requests.get(source.url, timeout=30)
response.raise_for_status()
logger.debug("XMLTV data fetched successfully.")
-
+
# If the URL ends with '.gz', decompress the response content
if source.url.lower().endswith('.gz'):
logger.debug("Detected .gz file. Decompressing...")
@@ -64,7 +64,7 @@ def fetch_xmltv(source):
# Create (or get) an EPGData record using the tvg_id.
epg_data, created = EPGData.objects.get_or_create(
tvg_id=tvg_id,
- defaults={'channel_name': tvg_id} # Use tvg_id as a fallback name
+ defaults={'name': tvg_id} # Use tvg_id as a fallback name
)
if created:
logger.info(f"Created new EPGData for tvg_id '{tvg_id}'.")
@@ -120,7 +120,7 @@ def fetch_schedules_direct(source):
# Create (or get) an EPGData record using the tvg_id.
epg_data, created = EPGData.objects.get_or_create(
tvg_id=tvg_id,
- defaults={'channel_name': tvg_id}
+ defaults={'name': tvg_id}
)
if created:
logger.info(f"Created new EPGData for tvg_id '{tvg_id}'.")
diff --git a/apps/hdhr/api_views.py b/apps/hdhr/api_views.py
index 844ee8fe..7eda441c 100644
--- a/apps/hdhr/api_views.py
+++ b/apps/hdhr/api_views.py
@@ -80,7 +80,7 @@ class LineupAPIView(APIView):
lineup = [
{
"GuideNumber": str(ch.channel_number),
- "GuideName": ch.channel_name,
+ "GuideName": ch.name,
"URL": request.build_absolute_uri(f"/output/stream/{ch.id}")
}
for ch in channels
@@ -128,5 +128,5 @@ class HDHRDeviceXMLAPIView(APIView):
{base_url}
{base_url}/lineup.json
"""
-
+
return HttpResponse(xml_response, content_type="application/xml")
diff --git a/apps/hdhr/views.py b/apps/hdhr/views.py
index 844ee8fe..7eda441c 100644
--- a/apps/hdhr/views.py
+++ b/apps/hdhr/views.py
@@ -80,7 +80,7 @@ class LineupAPIView(APIView):
lineup = [
{
"GuideNumber": str(ch.channel_number),
- "GuideName": ch.channel_name,
+ "GuideName": ch.name,
"URL": request.build_absolute_uri(f"/output/stream/{ch.id}")
}
for ch in channels
@@ -128,5 +128,5 @@ class HDHRDeviceXMLAPIView(APIView):
{base_url}
{base_url}/lineup.json
"""
-
+
return HttpResponse(xml_response, content_type="application/xml")
diff --git a/apps/m3u/migrations/0001_initial.py b/apps/m3u/migrations/0001_initial.py
index 7a20a713..58c1f473 100644
--- a/apps/m3u/migrations/0001_initial.py
+++ b/apps/m3u/migrations/0001_initial.py
@@ -59,7 +59,7 @@ class Migration(migrations.Migration):
('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_profile_name')],
+ 'constraints': [models.UniqueConstraint(fields=('m3u_account', 'name'), name='unique_account_name')],
},
),
]
diff --git a/apps/m3u/models.py b/apps/m3u/models.py
index 36f49374..8561456f 100644
--- a/apps/m3u/models.py
+++ b/apps/m3u/models.py
@@ -186,7 +186,7 @@ class M3UAccountProfile(models.Model):
class Meta:
constraints = [
- models.UniqueConstraint(fields=['m3u_account', 'name'], name='unique_account_profile_name')
+ models.UniqueConstraint(fields=['m3u_account', 'name'], name='unique_account_name')
]
def __str__(self):
@@ -210,7 +210,7 @@ def create_profile_for_m3u_account(sender, instance, created, **kwargs):
m3u_account=instance,
is_default=True,
)
-
+
profile.max_streams = instance.max_streams
profile.save()
diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py
index 211524d6..cbb7bd94 100644
--- a/apps/m3u/tasks.py
+++ b/apps/m3u/tasks.py
@@ -21,7 +21,7 @@ def parse_extinf_line(line: str) -> dict:
Parse an EXTINF line from an M3U file.
This function removes the "#EXTINF:" prefix, then splits the remaining
string on the first comma that is not enclosed in quotes.
-
+
Returns a dictionary with:
- 'attributes': a dict of attribute key/value pairs (e.g. tvg-id, tvg-logo, group-title)
- 'display_name': the text after the comma (the fallback display name)
@@ -186,7 +186,7 @@ def refresh_single_m3u_account(account_id):
try:
obj, created = Stream.objects.update_or_create(
name=current_info["name"],
- custom_url=line,
+ url=line,
m3u_account=account,
group_name=current_info["group_title"],
defaults=defaults
@@ -267,7 +267,7 @@ def parse_m3u_file(file_path, account):
try:
obj, created = Stream.objects.update_or_create(
name=current_info["name"],
- custom_url=line,
+ url=line,
m3u_account=account,
defaults=defaults
)
diff --git a/apps/output/views.py b/apps/output/views.py
index aeaa5d52..92319e93 100644
--- a/apps/output/views.py
+++ b/apps/output/views.py
@@ -14,13 +14,13 @@ def generate_m3u(request):
for channel in channels:
group_title = channel.channel_group.name if channel.channel_group else "Default"
tvg_id = channel.tvg_id or ""
- tvg_name = channel.tvg_name or channel.channel_name
+ tvg_name = channel.tvg_name or channel.name
tvg_logo = channel.logo_url or ""
channel_number = channel.channel_number
extinf_line = (
f'#EXTINF:-1 tvg-id="{tvg_id}" tvg-name="{tvg_name}" tvg-logo="{tvg_logo}" '
- f'tvg-chno="{channel_number}" group-title="{group_title}",{channel.channel_name}\n'
+ f'tvg-chno="{channel_number}" group-title="{group_title}",{channel.name}\n'
)
stream_url = request.build_absolute_uri(reverse('output:stream', args=[channel.id]))
m3u_content += extinf_line + stream_url + "\n"
@@ -48,15 +48,15 @@ def generate_epg(request):
xml_lines = []
xml_lines.append('')
xml_lines.append('')
-
+
# Output channel definitions based on EPGData.
# Use the EPGData's tvg_id (or a fallback) as the channel identifier.
for epg in epg_programs.keys():
channel_id = epg.tvg_id if epg.tvg_id else f"default-{epg.id}"
xml_lines.append(f' ')
- xml_lines.append(f' {epg.channel_name}')
+ xml_lines.append(f' {epg.name}')
xml_lines.append(' ')
-
+
# Output programme entries referencing the channel id from EPGData.
for epg, progs in epg_programs.items():
channel_id = epg.tvg_id if epg.tvg_id else f"default-{epg.id}"
@@ -67,7 +67,7 @@ def generate_epg(request):
xml_lines.append(f' {prog.title}')
xml_lines.append(f' {prog.description}')
xml_lines.append(' ')
-
+
xml_lines.append('')
xml_content = "\n".join(xml_lines)
diff --git a/apps/proxy/ts_proxy/views.py b/apps/proxy/ts_proxy/views.py
index 0d703745..1b5a15b9 100644
--- a/apps/proxy/ts_proxy/views.py
+++ b/apps/proxy/ts_proxy/views.py
@@ -13,7 +13,7 @@ from .channel_status import ChannelStatus
import logging
from apps.channels.models import Channel, Stream
from apps.m3u.models import M3UAccount, M3UAccountProfile
-from core.models import UserAgent, CoreSettings
+from core.models import UserAgent, CoreSettings, PROXY_PROFILE_NAME
# Configure logging properly
logger = logging.getLogger("ts_proxy")
@@ -43,7 +43,7 @@ def stream_ts(request, channel_id):
if not proxy_server.check_if_channel_exists(channel_id):
# Initialize the channel (but don't wait for completion)
logger.info(f"[{client_id}] Starting channel {channel_id} initialization")
-
+
# Get stream details from channel model
stream_id, profile_id = channel.get_stream()
if stream_id is None or profile_id is None:
@@ -63,9 +63,9 @@ def stream_ts(request, channel_id):
logger.debug(f"No user agent found for account, using default: {stream_user_agent}")
else:
logger.debug(f"User agent found for account: {stream_user_agent}")
-
+
# Generate stream URL based on the selected profile
- input_url = stream.custom_url or stream.url
+ input_url = stream.url
logger.debug("Executing the following pattern replacement:")
logger.debug(f" search: {profile.search_pattern}")
safe_replace_pattern = re.sub(r'\$(\d+)', r'\\\1', profile.replace_pattern)
@@ -78,9 +78,10 @@ def stream_ts(request, channel_id):
stream_profile = channel.get_stream_profile()
if stream_profile.is_redirect():
return HttpResponseRedirect(stream_url)
+
# Need to check if profile is transcoded
logger.debug(f"Using profile {stream_profile} for stream {stream_id}")
- if stream_profile == 'PROXY' or stream_profile is None:
+ if stream_profile.is_proxy() or stream_profile is None:
transcode = False
else:
transcode = True
@@ -90,7 +91,7 @@ def stream_ts(request, channel_id):
if proxy_server.redis_client:
metadata_key = f"ts_proxy:channel:{channel_id}:metadata"
profile_value = str(stream_profile)
- proxy_server.redis_client.hset(metadata_key, "profile", profile_value)
+ proxy_server.redis_client.hset(metadata_key, "profile", profile_value)
if not success:
return JsonResponse({'error': 'Failed to initialize channel'}, status=500)
@@ -107,11 +108,11 @@ def stream_ts(request, channel_id):
proxy_server.stop_channel(channel_id)
return JsonResponse({'error': 'Failed to connect'}, status=502)
time.sleep(0.1)
-
+
logger.info(f"[{client_id}] Successfully initialized channel {channel_id}")
channel_initializing = True
logger.info(f"[{client_id}] Channel {channel_id} initialization started")
-
+
# Register client - can do this regardless of initialization state
# Create local resources if needed
if channel_id not in proxy_server.stream_buffers or channel_id not in proxy_server.client_managers:
@@ -120,21 +121,21 @@ def stream_ts(request, channel_id):
# Get URL from Redis metadata
url = None
stream_user_agent = None # Initialize the variable
-
+
if proxy_server.redis_client:
metadata_key = f"ts_proxy:channel:{channel_id}:metadata"
url_bytes = proxy_server.redis_client.hget(metadata_key, "url")
ua_bytes = proxy_server.redis_client.hget(metadata_key, "user_agent")
profile_bytes = proxy_server.redis_client.hget(metadata_key, "profile")
-
+
if url_bytes:
url = url_bytes.decode('utf-8')
if ua_bytes:
stream_user_agent = ua_bytes.decode('utf-8')
# Extract transcode setting from Redis
profile_str = profile_bytes.decode('utf-8')
- use_transcode = (profile_str == 'PROXY' or profile_str == 'None')
-
+ use_transcode = (profile_str == PROXY_PROFILE_NAME or profile_str == 'None')
+
# Use client_user_agent as fallback if stream_user_agent is None
success = proxy_server.initialize_channel(url, channel_id, stream_user_agent or client_user_agent, use_transcode)
if not success:
@@ -142,7 +143,7 @@ def stream_ts(request, channel_id):
return JsonResponse({'error': 'Failed to initialize channel locally'}, status=500)
logger.info(f"[{client_id}] Successfully initialized channel {channel_id} locally")
-
+
# Register client
buffer = proxy_server.stream_buffers[channel_id]
client_manager = proxy_server.client_managers[channel_id]
@@ -154,17 +155,17 @@ def stream_ts(request, channel_id):
stream_start_time = time.time()
bytes_sent = 0
chunks_sent = 0
-
+
# Keep track of initialization state
initialization_start = time.time()
max_init_wait = getattr(Config, 'CLIENT_WAIT_TIMEOUT', 30)
channel_ready = not channel_initializing
keepalive_interval = 0.5
last_keepalive = 0
-
+
try:
logger.info(f"[{client_id}] Stream generator started, channel_ready={channel_ready}")
-
+
# Wait for initialization to complete if needed
if not channel_ready:
# While init is happening, send keepalive packets
@@ -173,7 +174,7 @@ def stream_ts(request, channel_id):
if proxy_server.redis_client:
metadata_key = f"ts_proxy:channel:{channel_id}:metadata"
metadata = proxy_server.redis_client.hgetall(metadata_key)
-
+
if metadata and b'state' in metadata:
state = metadata[b'state'].decode('utf-8')
if state in ['waiting_for_clients', 'active']:
@@ -199,19 +200,19 @@ def stream_ts(request, channel_id):
keepalive_packet[0] = 0x47 # Sync byte
keepalive_packet[1] = 0x1F # PID high bits (null packet)
keepalive_packet[2] = 0xFF # PID low bits (null packet)
-
+
# Add status info in packet payload (will be ignored by players)
status_msg = f"Initializing: {state}".encode('utf-8')
keepalive_packet[4:4+min(len(status_msg), 180)] = status_msg[:180]
-
+
logger.debug(f"[{client_id}] Sending keepalive packet during initialization, state={state}")
yield bytes(keepalive_packet)
bytes_sent += len(keepalive_packet)
last_keepalive = time.time()
-
+
# Wait a bit before checking again (don't send too many keepalives)
time.sleep(0.1)
-
+
# Check if we timed out waiting
if not channel_ready:
logger.warning(f"[{client_id}] Timed out waiting for initialization")
@@ -223,13 +224,13 @@ def stream_ts(request, channel_id):
error_packet[4:4+min(len(error_msg), 180)] = error_msg[:180]
yield bytes(error_packet)
return
-
+
# Channel is now ready - original streaming code goes here
logger.info(f"[{client_id}] Channel {channel_id} ready, starting normal streaming")
-
+
# Reset start time for real streaming
stream_start_time = time.time()
-
+
# Get buffer - stream manager may not exist in this worker
buffer = proxy_server.stream_buffers.get(channel_id)
stream_manager = proxy_server.stream_managers.get(channel_id)
@@ -558,7 +559,7 @@ def channel_status(request, channel_id=None):
try:
# Check if Redis is available
if not proxy_server.redis_client:
- return JsonResponse({'error': 'Redis connection not available'}, status=500)
+ return JsonResponse({'error': 'Redis connection not available'}, status=500)
# Handle single channel or all channels
if channel_id:
diff --git a/core/admin.py b/core/admin.py
index 823e6a5a..45bb7a2e 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -6,26 +6,26 @@ from .models import UserAgent, StreamProfile, CoreSettings
@admin.register(UserAgent)
class UserAgentAdmin(admin.ModelAdmin):
list_display = (
- "user_agent_name",
+ "name",
"user_agent",
"description",
"is_active",
"created_at",
"updated_at",
)
- search_fields = ("user_agent_name", "user_agent", "description")
+ search_fields = ("name", "user_agent", "description")
list_filter = ("is_active",)
readonly_fields = ("created_at", "updated_at")
@admin.register(StreamProfile)
class StreamProfileAdmin(admin.ModelAdmin):
list_display = (
- "profile_name",
+ "name",
"command",
"is_active",
"user_agent",
)
- search_fields = ("profile_name", "command", "user_agent")
+ search_fields = ("name", "command", "user_agent")
list_filter = ("is_active",)
@admin.register(CoreSettings)
diff --git a/core/fixtures/initial_data.json b/core/fixtures/initial_data.json
index 57bafd05..c037fa78 100644
--- a/core/fixtures/initial_data.json
+++ b/core/fixtures/initial_data.json
@@ -3,7 +3,7 @@
"model": "core.useragent",
"pk": 1,
"fields": {
- "user_agent_name": "TiviMate",
+ "name": "TiviMate",
"user_agent": "TiviMate/5.1.6 (Android 12)",
"description": "",
"is_active": true
@@ -13,7 +13,7 @@
"model": "core.useragent",
"pk": 2,
"fields": {
- "user_agent_name": "VLC",
+ "name": "VLC",
"user_agent": "VLC/3.0.21 LibVLC 3.0.21",
"description": "",
"is_active": true
@@ -23,7 +23,7 @@
"model": "core.streamprofile",
"pk": 1,
"fields": {
- "profile_name": "ffmpeg",
+ "name": "ffmpeg",
"command": "ffmpeg",
"parameters": "-i {streamUrl} -c:v copy -c:a copy -f mpegts pipe:1",
"is_active": true,
@@ -34,7 +34,7 @@
"model": "core.streamprofile",
"pk": 2,
"fields": {
- "profile_name": "streamlink",
+ "name": "streamlink",
"command": "streamlink",
"parameters": "{streamUrl} best --stdout",
"is_active": true,
diff --git a/core/migrations/0007_create_proxy_and_redirect_stream_profiles.py b/core/migrations/0007_create_proxy_and_redirect_stream_profiles.py
index cf10496a..41ca0eeb 100644
--- a/core/migrations/0007_create_proxy_and_redirect_stream_profiles.py
+++ b/core/migrations/0007_create_proxy_and_redirect_stream_profiles.py
@@ -1,8 +1,11 @@
# 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",
@@ -10,7 +13,7 @@ def create_proxy_stream_profile(apps, schema_editor):
parameters="",
locked=True,
is_active=True,
- user_agent="1",
+ user_agent_id=default_user_agent_id,
)
StreamProfile.objects.create(
@@ -19,7 +22,7 @@ def create_proxy_stream_profile(apps, schema_editor):
parameters="",
locked=True,
is_active=True,
- user_agent="1",
+ user_agent_id=default_user_agent_id,
)
class Migration(migrations.Migration):
@@ -29,4 +32,5 @@ class Migration(migrations.Migration):
]
operations = [
+ migrations.RunPython(create_proxy_stream_profile)
]
diff --git a/core/migrations/0008_rename_profile_name_streamprofile_name_and_more.py b/core/migrations/0008_rename_profile_name_streamprofile_name_and_more.py
new file mode 100644
index 00000000..6f0b2824
--- /dev/null
+++ b/core/migrations/0008_rename_profile_name_streamprofile_name_and_more.py
@@ -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',
+ ),
+ ]
diff --git a/core/models.py b/core/models.py
index 65eb2ef8..e0171aee 100644
--- a/core/models.py
+++ b/core/models.py
@@ -3,7 +3,7 @@ from django.db import models
from django.utils.text import slugify
class UserAgent(models.Model):
- user_agent_name = models.CharField(
+ name = models.CharField(
max_length=512,
unique=True,
help_text="The User-Agent name."
@@ -26,13 +26,13 @@ class UserAgent(models.Model):
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
- return self.user_agent_name
+ return self.name
-PROXY_PROFILE = 'Proxy'
-REDIRECT_PROFILE = 'Redirect'
+PROXY_PROFILE_NAME = 'Proxy'
+REDIRECT_PROFILE_NAME = 'Redirect'
class StreamProfile(models.Model):
- profile_name = models.CharField(max_length=255, help_text="Name of the stream profile")
+ name = models.CharField(max_length=255, help_text="Name of the stream profile")
command = models.CharField(
max_length=255,
help_text="Command to execute (e.g., 'yt.sh', 'streamlink', or 'vlc')",
@@ -56,7 +56,7 @@ class StreamProfile(models.Model):
)
def __str__(self):
- return self.profile_name
+ return self.name
def delete(self):
if self.locked():
@@ -108,12 +108,12 @@ class StreamProfile(models.Model):
return instance
def is_proxy(self):
- if self.locked and self.profile_name == PROXY_PROFILE:
+ if self.locked and self.name == PROXY_PROFILE_NAME:
return True
return False
def is_redirect(self):
- if self.locked and self.profile_name == REDIRECT_PROFILE:
+ if self.locked and self.name == REDIRECT_PROFILE_NAME:
return True
return False
@@ -149,12 +149,10 @@ class CoreSettings(models.Model):
return "Core Settings"
@classmethod
- def get_default_user_agent(cls):
+ def get_default_user_agent_id(cls):
"""Retrieve a system profile by name (or return None if not found)."""
- default_ua_id = cls.objects.get(key=DEFAULT_USER_AGENT_KEY).value
- return UserAgent.objects.get(id=default_ua_id)
+ return cls.objects.get(key=DEFAULT_USER_AGENT_KEY).value
@classmethod
- def get_default_stream_profile(cls):
- default_sp_id = cls.objects.get(key=DEFAULT_STREAM_PROFILE_KEY).value
- return StreamProfile.objects.get(id=default_sp_id)
+ def get_default_stream_profile_id(cls):
+ return cls.objects.get(key=DEFAULT_STREAM_PROFILE_KEY).value
diff --git a/core/serializers.py b/core/serializers.py
index 32f70ebd..c80ad630 100644
--- a/core/serializers.py
+++ b/core/serializers.py
@@ -6,12 +6,12 @@ from .models import UserAgent, StreamProfile, CoreSettings
class UserAgentSerializer(serializers.ModelSerializer):
class Meta:
model = UserAgent
- fields = ['id', 'user_agent_name', 'user_agent', 'description', 'is_active', 'created_at', 'updated_at']
+ fields = ['id', 'name', 'user_agent', 'description', 'is_active', 'created_at', 'updated_at']
class StreamProfileSerializer(serializers.ModelSerializer):
class Meta:
model = StreamProfile
- fields = ['id', 'profile_name', 'command', 'parameters', 'is_active', 'user_agent']
+ fields = ['id', 'name', 'command', 'parameters', 'is_active', 'user_agent', 'locked']
class CoreSettingsSerializer(serializers.ModelSerializer):
class Meta:
diff --git a/core/views.py b/core/views.py
index 73652b7e..a5852171 100644
--- a/core/views.py
+++ b/core/views.py
@@ -41,7 +41,7 @@ def stream_view(request, stream_id):
# Retrieve the channel by the provided stream_id.
channel = Channel.objects.get(channel_number=stream_id)
- logger.debug("Channel retrieved: ID=%s, Name=%s", channel.id, channel.channel_name)
+ logger.debug("Channel retrieved: ID=%s, Name=%s", channel.id, channel.name)
# Ensure the channel has at least one stream.
if not channel.streams.exists():
@@ -65,7 +65,7 @@ def stream_view(request, stream_id):
logger.debug("Stream M3U account ID=%s, Name=%s", m3u_account.id, m3u_account.name)
# Use the custom URL if available; otherwise, use the standard URL.
- input_url = stream.custom_url or stream.url
+ input_url = stream.url
logger.debug("Input URL: %s", input_url)
# Determine which profile we can use.
@@ -132,7 +132,7 @@ def stream_view(request, stream_id):
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)
- logger.debug("Stream profile used: %s", stream_profile.profile_name)
+ logger.debug("Stream profile used: %s", stream_profile.name)
# Determine the user agent to use.
user_agent = stream_profile.user_agent or getattr(settings, "DEFAULT_USER_AGENT", "Mozilla/5.0")
diff --git a/dispatcharr/settings.py b/dispatcharr/settings.py
index 6005effc..5c3ee116 100644
--- a/dispatcharr/settings.py
+++ b/dispatcharr/settings.py
@@ -20,7 +20,7 @@ INSTALLED_APPS = [
'apps.hdhr',
'apps.m3u',
'apps.output',
- 'apps.proxy.apps.ProxyConfig',
+ 'apps.proxy.apps.ProxyConfig',
'core',
'drf_yasg',
'daphne',
diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx
index 28a0fab1..af56d041 100644
--- a/frontend/src/components/forms/Channel.jsx
+++ b/frontend/src/components/forms/Channel.jsx
@@ -58,7 +58,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
const formik = useFormik({
initialValues: {
- channel_name: '',
+ name: '',
channel_number: '',
channel_group_id: '',
stream_profile_id: '0',
@@ -66,7 +66,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
tvg_name: '',
},
validationSchema: Yup.object({
- channel_name: Yup.string().required('Name is required'),
+ name: Yup.string().required('Name is required'),
channel_number: Yup.string().required('Invalid channel number').min(0),
channel_group_id: Yup.string().required('Channel group is required'),
}),
@@ -102,7 +102,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
useEffect(() => {
if (channel) {
formik.setValues({
- channel_name: channel.channel_name,
+ name: channel.name,
channel_number: channel.channel_number,
channel_group_id: channel.channel_group?.id,
stream_profile_id: channel.stream_profile_id || '0',
@@ -256,14 +256,12 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
@@ -312,7 +310,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
}
data={streamProfiles.map((option) => ({
value: `${option.id}`,
- label: option.profile_name,
+ label: option.name,
}))}
/>
diff --git a/frontend/src/components/forms/M3U.jsx b/frontend/src/components/forms/M3U.jsx
index 051826ac..86131ea2 100644
--- a/frontend/src/components/forms/M3U.jsx
+++ b/frontend/src/components/forms/M3U.jsx
@@ -138,7 +138,7 @@ const M3U = ({ playlist = null, isOpen, onClose }) => {
onChange={formik.handleChange}
error={formik.errors.user_agent ? formik.touched.user_agent : ''}
data={userAgents.map((ua) => ({
- label: ua.user_agent_name,
+ label: ua.name,
value: `${ua.id}`,
}))}
/>
diff --git a/frontend/src/components/forms/Stream.jsx b/frontend/src/components/forms/Stream.jsx
index adc42485..6c058345 100644
--- a/frontend/src/components/forms/Stream.jsx
+++ b/frontend/src/components/forms/Stream.jsx
@@ -91,7 +91,7 @@ const Stream = ({ stream = null, isOpen, onClose }) => {
onChange={setSelectedStreamProfile}
error={formik.errors.stream_profile_id}
data={streamProfiles.map((profile) => ({
- label: profile.profile_name,
+ label: profile.name,
value: `${profile.id}`,
}))}
comboboxProps={{ withinPortal: false, zIndex: 1000 }}
diff --git a/frontend/src/components/forms/StreamProfile.jsx b/frontend/src/components/forms/StreamProfile.jsx
index eadcca13..ec7d4d8a 100644
--- a/frontend/src/components/forms/StreamProfile.jsx
+++ b/frontend/src/components/forms/StreamProfile.jsx
@@ -11,14 +11,14 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
const formik = useFormik({
initialValues: {
- profile_name: '',
+ name: '',
command: '',
parameters: '',
is_active: true,
user_agent: '',
},
validationSchema: Yup.object({
- profile_name: Yup.string().required('Name is required'),
+ name: Yup.string().required('Name is required'),
command: Yup.string().required('Command is required'),
parameters: Yup.string().required('Parameters are is required'),
}),
@@ -38,7 +38,7 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
useEffect(() => {
if (profile) {
formik.setValues({
- profile_name: profile.profile_name,
+ name: profile.name,
command: profile.command,
parameters: profile.parameters,
is_active: profile.is_active,
@@ -57,12 +57,13 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {