mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-22 18:28:00 +00:00
modified database fields for consistency, removed custom_url from streams (no longer needed)
This commit is contained in:
parent
5da288b15b
commit
9711d7ab34
40 changed files with 216 additions and 172 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
return render(request, 'channels/channels.html')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ''
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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}'.")
|
||||
|
|
|
|||
|
|
@ -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):
|
|||
<BaseURL>{base_url}</BaseURL>
|
||||
<LineupURL>{base_url}/lineup.json</LineupURL>
|
||||
</root>"""
|
||||
|
||||
|
||||
return HttpResponse(xml_response, content_type="application/xml")
|
||||
|
|
|
|||
|
|
@ -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):
|
|||
<BaseURL>{base_url}</BaseURL>
|
||||
<LineupURL>{base_url}/lineup.json</LineupURL>
|
||||
</root>"""
|
||||
|
||||
|
||||
return HttpResponse(xml_response, content_type="application/xml")
|
||||
|
|
|
|||
|
|
@ -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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 version="1.0" encoding="UTF-8"?>')
|
||||
xml_lines.append('<tv generator-info-name="Dispatcharr" generator-info-url="https://example.com">')
|
||||
|
||||
|
||||
# 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' <channel id="{channel_id}">')
|
||||
xml_lines.append(f' <display-name>{epg.channel_name}</display-name>')
|
||||
xml_lines.append(f' <display-name>{epg.name}</display-name>')
|
||||
xml_lines.append(' </channel>')
|
||||
|
||||
|
||||
# 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' <title>{prog.title}</title>')
|
||||
xml_lines.append(f' <desc>{prog.description}</desc>')
|
||||
xml_lines.append(' </programme>')
|
||||
|
||||
|
||||
xml_lines.append('</tv>')
|
||||
xml_content = "\n".join(xml_lines)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ INSTALLED_APPS = [
|
|||
'apps.hdhr',
|
||||
'apps.m3u',
|
||||
'apps.output',
|
||||
'apps.proxy.apps.ProxyConfig',
|
||||
'apps.proxy.apps.ProxyConfig',
|
||||
'core',
|
||||
'drf_yasg',
|
||||
'daphne',
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<Grid gap={2}>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
id="channel_name"
|
||||
name="channel_name"
|
||||
id="name"
|
||||
name="name"
|
||||
label="Channel Name"
|
||||
value={formik.values.channel_name}
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
error={
|
||||
formik.errors.channel_name ? formik.touched.channel_name : ''
|
||||
}
|
||||
error={formik.errors.name ? formik.touched.name : ''}
|
||||
/>
|
||||
|
||||
<Grid>
|
||||
|
|
@ -312,7 +310,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
}
|
||||
data={streamProfiles.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.profile_name,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
}))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
<Modal opened={isOpen} onClose={onClose} title="Stream Profile">
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<TextInput
|
||||
id="profile_name"
|
||||
name="profile_name"
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
value={formik.values.profile_name}
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.errors.profile_name}
|
||||
error={formik.errors.name}
|
||||
disabled={profile ? profile.locked : false}
|
||||
/>
|
||||
<TextInput
|
||||
id="command"
|
||||
|
|
@ -71,6 +72,7 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
value={formik.values.command}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.errors.command}
|
||||
disabled={profile ? profile.locked : false}
|
||||
/>
|
||||
<TextInput
|
||||
id="parameters"
|
||||
|
|
@ -79,6 +81,7 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
value={formik.values.parameters}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.errors.parameters}
|
||||
disabled={profile ? profile.locked : false}
|
||||
/>
|
||||
|
||||
<Select
|
||||
|
|
@ -89,7 +92,7 @@ const StreamProfile = ({ profile = null, isOpen, onClose }) => {
|
|||
onChange={formik.handleChange}
|
||||
error={formik.errors.user_agent}
|
||||
data={userAgents.map((ua) => ({
|
||||
label: ua.user_agent_name,
|
||||
label: ua.name,
|
||||
value: `${ua.id}`,
|
||||
}))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ import {
|
|||
const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
user_agent_name: '',
|
||||
name: '',
|
||||
user_agent: '',
|
||||
description: '',
|
||||
is_active: true,
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
user_agent_name: Yup.string().required('Name is required'),
|
||||
name: Yup.string().required('Name is required'),
|
||||
user_agent: Yup.string().required('User-Agent is required'),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
|
|
@ -43,7 +43,7 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
useEffect(() => {
|
||||
if (userAgent) {
|
||||
formik.setValues({
|
||||
user_agent_name: userAgent.user_agent_name,
|
||||
name: userAgent.name,
|
||||
user_agent: userAgent.user_agent,
|
||||
description: userAgent.description,
|
||||
is_active: userAgent.is_active,
|
||||
|
|
@ -61,15 +61,12 @@ const UserAgent = ({ userAgent = null, isOpen, onClose }) => {
|
|||
<Modal opened={isOpen} onClose={onClose} title="User-Agent">
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<TextInput
|
||||
id="user_agent_name"
|
||||
name="user_agent_name"
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
value={formik.values.user_agent_name}
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
error={
|
||||
formik.touched.user_agent_name &&
|
||||
Boolean(formik.errors.user_agent_name)
|
||||
}
|
||||
error={formik.touched.name && Boolean(formik.errors.name)}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ const ChannelsTable = ({}) => {
|
|||
},
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'channel_name',
|
||||
accessorKey: 'name',
|
||||
mantineTableHeadCellProps: {
|
||||
sx: { textAlign: 'center' },
|
||||
},
|
||||
|
|
@ -324,7 +324,7 @@ const ChannelsTable = ({}) => {
|
|||
};
|
||||
|
||||
function handleWatchStream(channelNumber) {
|
||||
let vidUrl = `/output/stream/${channelNumber}/`;
|
||||
let vidUrl = `/proxy/ts/stream/${channelNumber}`;
|
||||
if (env_mode == 'dev') {
|
||||
vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`;
|
||||
}
|
||||
|
|
@ -554,7 +554,7 @@ const ChannelsTable = ({}) => {
|
|||
size="sm"
|
||||
variant="transparent"
|
||||
color="green.5"
|
||||
onClick={() => handleWatchStream(row.original.channel_number)}
|
||||
onClick={() => handleWatchStream(row.original.id)}
|
||||
>
|
||||
<CirclePlay size="18" />
|
||||
</ActionIcon>
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const StreamProfiles = () => {
|
|||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'profile_name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'Command',
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ const StreamsTable = ({}) => {
|
|||
// Fallback: Individual creation (optional)
|
||||
const createChannelFromStream = async (stream) => {
|
||||
await API.createChannelFromStream({
|
||||
channel_name: stream.name,
|
||||
name: stream.name,
|
||||
channel_number: null,
|
||||
stream_id: stream.id,
|
||||
});
|
||||
|
|
@ -431,7 +431,7 @@ const StreamsTable = ({}) => {
|
|||
enableTopToolbar: false,
|
||||
enableRowVirtualization: true,
|
||||
renderTopToolbar: () => null, // Removes the entire top toolbar
|
||||
renderToolbarInternalActions: () => null,
|
||||
renderToolbarInternalActions: () => null,
|
||||
rowVirtualizerInstanceRef,
|
||||
rowVirtualizerOptions: { overscan: 5 }, //optionally customize the row virtualizer
|
||||
enableBottomToolbar: true,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ const UserAgentsTable = () => {
|
|||
() => [
|
||||
{
|
||||
header: 'Name',
|
||||
accessorKey: 'user_agent_name',
|
||||
accessorKey: 'name',
|
||||
},
|
||||
{
|
||||
header: 'User-Agent',
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
/>
|
||||
{guideChannels.map((channel) => (
|
||||
<Box
|
||||
key={channel.channel_name}
|
||||
key={channel.name}
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: PROGRAM_HEIGHT,
|
||||
|
|
@ -318,7 +318,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
>
|
||||
<img
|
||||
src={channel.logo_url || logo}
|
||||
alt={channel.channel_name}
|
||||
alt={channel.name}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
|
|
@ -424,7 +424,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
);
|
||||
return (
|
||||
<Box
|
||||
key={channel.channel_name}
|
||||
key={channel.name}
|
||||
style={{
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ const SettingsPage = () => {
|
|||
error={formik.errors['default-user-agent']}
|
||||
data={userAgents.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.user_agent_name,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ const SettingsPage = () => {
|
|||
error={formik.errors['default-stream-profile']}
|
||||
data={streamProfiles.map((option) => ({
|
||||
value: `${option.id}`,
|
||||
label: option.profile_name,
|
||||
label: option.name,
|
||||
}))}
|
||||
/>
|
||||
{/* <Select
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue