diff --git a/apps/channels/forms.py b/apps/channels/forms.py index f57a8729..37d28916 100644 --- a/apps/channels/forms.py +++ b/apps/channels/forms.py @@ -27,9 +27,6 @@ class ChannelForm(forms.ModelForm): 'channel_number', 'channel_name', 'channel_group', - 'is_active', - 'is_looping', - 'shuffle_mode', ] @@ -47,6 +44,5 @@ class StreamForm(forms.ModelForm): 'tvg_id', 'local_file', 'is_transcoded', - 'ffmpeg_preset', 'group_name', ] diff --git a/apps/epg/admin.py b/apps/epg/admin.py index 5ccfcae4..69738bf4 100644 --- a/apps/epg/admin.py +++ b/apps/epg/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import EPGSource, Program +from .models import EPGSource, ProgramData @admin.register(EPGSource) class EPGSourceAdmin(admin.ModelAdmin): @@ -7,13 +7,13 @@ class EPGSourceAdmin(admin.ModelAdmin): list_filter = ['source_type', 'is_active'] search_fields = ['name'] -@admin.register(Program) +@admin.register(ProgramData) class ProgramAdmin(admin.ModelAdmin): list_display = ['title', 'get_channel_tvg_id', 'start_time', 'end_time'] - list_filter = ['channel'] - search_fields = ['title', 'channel__channel_name'] + list_filter = ['epg__channel'] # updated here + search_fields = ['title', 'epg__channel__channel_name'] # updated here def get_channel_tvg_id(self, obj): - return obj.channel.tvg_id if obj.channel else '' + return obj.epg.channel.tvg_id if obj.epg and obj.epg.channel else '' get_channel_tvg_id.short_description = 'Channel TVG ID' - get_channel_tvg_id.admin_order_field = 'channel__tvg_id' + get_channel_tvg_id.admin_order_field = 'epg__channel__tvg_id' diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index a16b1bd2..021917ca 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -1,4 +1,5 @@ -from rest_framework import generics, status, viewsets +import logging +from rest_framework import viewsets, status from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated @@ -6,46 +7,64 @@ from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from django.utils import timezone from datetime import timedelta -from .models import Program, EPGSource -from .serializers import ProgramSerializer, EPGSourceSerializer +from .models import EPGSource, ProgramData # Updated: use ProgramData instead of Program +from .serializers import ProgramDataSerializer, EPGSourceSerializer # Updated serializer from .tasks import refresh_epg_data +logger = logging.getLogger(__name__) -# 🔹 1) EPG Source API (CRUD) +# ───────────────────────────── +# 1) EPG Source API (CRUD) +# ───────────────────────────── class EPGSourceViewSet(viewsets.ModelViewSet): """Handles CRUD operations for EPG sources""" queryset = EPGSource.objects.all() serializer_class = EPGSourceSerializer permission_classes = [IsAuthenticated] + def list(self, request, *args, **kwargs): + logger.debug("Listing all EPG sources.") + return super().list(request, *args, **kwargs) -# 🔹 2) Program API (CRUD) +# ───────────────────────────── +# 2) Program API (CRUD) +# ───────────────────────────── class ProgramViewSet(viewsets.ModelViewSet): """Handles CRUD operations for EPG programs""" - queryset = Program.objects.all() - serializer_class = ProgramSerializer + queryset = ProgramData.objects.all() # Updated to ProgramData + serializer_class = ProgramDataSerializer # Updated serializer permission_classes = [IsAuthenticated] + def list(self, request, *args, **kwargs): + logger.debug("Listing all EPG programs.") + return super().list(request, *args, **kwargs) -# 🔹 3) EPG Grid View: Shows programs airing within the next 12 hours +# ───────────────────────────── +# 3) EPG Grid View +# ───────────────────────────── class EPGGridAPIView(APIView): """Returns all programs airing in the next 12 hours""" @swagger_auto_schema( operation_description="Retrieve upcoming EPG programs within the next 12 hours", - responses={200: ProgramSerializer(many=True)} + responses={200: ProgramDataSerializer(many=True)} ) def get(self, request, format=None): now = timezone.now() twelve_hours_later = now + timedelta(hours=12) - programs = Program.objects.select_related('channel').filter( + logger.debug(f"EPGGridAPIView: Querying programs between {now} and {twelve_hours_later}.") + # Use select_related to prefetch EPGData and Channel data + programs = ProgramData.objects.select_related('epg__channel').filter( start_time__gte=now, start_time__lte=twelve_hours_later ) - serializer = ProgramSerializer(programs, many=True) + count = programs.count() + logger.debug(f"EPGGridAPIView: Found {count} program(s).") + serializer = ProgramDataSerializer(programs, many=True) return Response({'data': serializer.data}, status=status.HTTP_200_OK) - -# 🔹 4) EPG Import View: Triggers an import of EPG data +# ───────────────────────────── +# 4) EPG Import View +# ───────────────────────────── class EPGImportAPIView(APIView): """Triggers an EPG data refresh""" @@ -54,5 +73,7 @@ class EPGImportAPIView(APIView): responses={202: "EPG data import initiated"} ) def post(self, request, format=None): + logger.info("EPGImportAPIView: Received request to import EPG data.") refresh_epg_data.delay() # Trigger Celery task + logger.info("EPGImportAPIView: Task dispatched to refresh EPG data.") return Response({'success': True, 'message': 'EPG data import initiated.'}, status=status.HTTP_202_ACCEPTED) diff --git a/apps/epg/models.py b/apps/epg/models.py index 3ed2111e..6672ce0d 100644 --- a/apps/epg/models.py +++ b/apps/epg/models.py @@ -2,7 +2,6 @@ from django.db import models from django.utils import timezone from apps.channels.models import Channel - class EPGSource(models.Model): SOURCE_TYPE_CHOICES = [ ('xmltv', 'XMLTV URL'), @@ -17,12 +16,25 @@ class EPGSource(models.Model): def __str__(self): return self.name -class Program(models.Model): - channel = models.ForeignKey('channels.Channel', on_delete=models.CASCADE, related_name="programs") - title = models.CharField(max_length=255) - description = models.TextField(blank=True, null=True) + +class EPGData(models.Model): + """ + Stores EPG data for a specific channel. + """ + channel = models.ForeignKey(Channel, on_delete=models.CASCADE, related_name="epg_data") + channel_name = models.CharField(max_length=255) + + def __str__(self): + return f"EPG Data for {self.channel_name}" + + +class ProgramData(models.Model): + epg = models.ForeignKey(EPGData, on_delete=models.CASCADE, related_name="programs") start_time = models.DateTimeField() end_time = models.DateTimeField() + title = models.CharField(max_length=255) + sub_title = models.CharField(max_length=255, blank=True, null=True) + description = models.TextField(blank=True, null=True) def __str__(self): return f"{self.title} ({self.start_time} - {self.end_time})" diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py index 4245620c..b0c5b9df 100644 --- a/apps/epg/serializers.py +++ b/apps/epg/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import Program, EPGSource +from .models import EPGSource, EPGData, ProgramData from apps.channels.models import Channel class EPGSourceSerializer(serializers.ModelSerializer): @@ -7,13 +7,18 @@ class EPGSourceSerializer(serializers.ModelSerializer): model = EPGSource fields = ['id', 'name', 'source_type', 'url', 'api_key', 'is_active'] +class ProgramDataSerializer(serializers.ModelSerializer): + class Meta: + model = ProgramData + fields = ['id', 'start_time', 'end_time', 'title', 'sub_title', 'description'] -class ProgramSerializer(serializers.ModelSerializer): +class EPGDataSerializer(serializers.ModelSerializer): + programs = ProgramDataSerializer(many=True, read_only=True) channel = serializers.SerializerMethodField() def get_channel(self, obj): - return {"id": obj.channel.id, "name": obj.channel.name} if obj.channel else None + return {"id": obj.channel.id, "name": obj.channel.channel_name} if obj.channel else None class Meta: - model = Program - fields = ['id', 'channel', 'title', 'description', 'start_time', 'end_time'] + model = EPGData + fields = ['id', 'channel', 'channel_name', 'programs'] diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 5cc4a7dc..6b6dc4d3 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -1,119 +1,180 @@ +import logging from celery import shared_task -from .models import EPGSource, Program +from .models import EPGSource, EPGData, ProgramData from apps.channels.models import Channel from django.utils import timezone import requests import xml.etree.ElementTree as ET -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone as dt_timezone from django.db import transaction +logger = logging.getLogger(__name__) + @shared_task def refresh_epg_data(): + logger.info("Starting refresh_epg_data task.") active_sources = EPGSource.objects.filter(is_active=True) + logger.debug(f"Found {active_sources.count()} active EPGSource(s).") for source in active_sources: + logger.info(f"Processing EPGSource: {source.name} (type: {source.source_type})") if source.source_type == 'xmltv': fetch_xmltv(source) elif source.source_type == 'schedules_direct': fetch_schedules_direct(source) + logger.info("Finished refresh_epg_data task.") return "EPG data refreshed." def fetch_xmltv(source): + logger.info(f"Fetching XMLTV data from source: {source.name}") try: response = requests.get(source.url, timeout=30) response.raise_for_status() + logger.debug("XMLTV data fetched successfully.") root = ET.fromstring(response.content) + logger.debug("Parsed XMLTV XML content.") - with transaction.atomic(): - for programme in root.findall('programme'): - start_time = parse_xmltv_time(programme.get('start')) - stop_time = parse_xmltv_time(programme.get('stop')) - channel_tvg_id = programme.get('channel') + # Group programmes by channel tvg_id + programmes_by_channel = {} + for programme in root.findall('programme'): + start_time = parse_xmltv_time(programme.get('start')) + stop_time = parse_xmltv_time(programme.get('stop')) + channel_tvg_id = programme.get('channel') + title = programme.findtext('title', default='No Title') + desc = programme.findtext('desc', default='') - title = programme.findtext('title', default='No Title') - desc = programme.findtext('desc', default='') + programmes_by_channel.setdefault(channel_tvg_id, []).append({ + 'start_time': start_time, + 'end_time': stop_time, + 'title': title, + 'description': desc, + }) + + # Process each channel group + for tvg_id, programmes in programmes_by_channel.items(): + try: + channel = Channel.objects.get(tvg_id=tvg_id) + logger.debug(f"Found Channel: {channel}") + except Channel.DoesNotExist: + logger.warning(f"No channel found for tvg_id '{tvg_id}'. Skipping programmes.") + continue - # Find or create the channel - try: - channel = Channel.objects.get(tvg_id=channel_tvg_id) - except Channel.DoesNotExist: - # Optionally, skip programs for unknown channels - continue - - # Create or update the program - Program.objects.update_or_create( - channel=channel, - title=title, - start_time=start_time, - end_time=stop_time, - defaults={'description': desc} - ) + # Get or create the EPGData record for the channel + epg_data, created = EPGData.objects.get_or_create( + channel=channel, + defaults={'channel_name': channel.channel_name} + ) + if not created and epg_data.channel_name != channel.channel_name: + epg_data.channel_name = channel.channel_name + epg_data.save(update_fields=['channel_name']) + + logger.info(f"Processing {len(programmes)} programme(s) for channel '{channel.channel_name}'.") + # For each programme, update or create a ProgramData record + with transaction.atomic(): + for prog in programmes: + obj, created = ProgramData.objects.update_or_create( + epg=epg_data, + start_time=prog['start_time'], + title=prog['title'], + defaults={ + 'end_time': prog['end_time'], + 'description': prog['description'], + 'sub_title': '' + } + ) + if created: + logger.info(f"Created ProgramData '{prog['title']}' for channel '{channel.channel_name}'.") + else: + logger.info(f"Updated ProgramData '{prog['title']}' for channel '{channel.channel_name}'.") except Exception as e: - # Log the error appropriately - print(f"Error fetching XMLTV from {source.name}: {e}") + logger.error(f"Error fetching XMLTV from {source.name}: {e}", exc_info=True) def fetch_schedules_direct(source): + logger.info(f"Fetching Schedules Direct data from source: {source.name}") try: - # need to add a setting for api url. - + # NOTE: You need to provide the correct api_url for Schedules Direct. api_url = '' headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {source.api_key}', } - - # Fetch subscriptions (channels) + logger.debug(f"Requesting subscriptions from Schedules Direct using URL: {api_url}") response = requests.get(api_url, headers=headers, timeout=30) response.raise_for_status() subscriptions = response.json() + logger.debug(f"Fetched subscriptions: {subscriptions}") - # Fetch schedules for each subscription for sub in subscriptions: channel_tvg_id = sub.get('stationID') - # Fetch schedules - # Need to add schedules direct url + logger.debug(f"Processing subscription for tvg_id: {channel_tvg_id}") schedules_url = f"/schedules/{channel_tvg_id}" + logger.debug(f"Requesting schedules from URL: {schedules_url}") sched_response = requests.get(schedules_url, headers=headers, timeout=30) sched_response.raise_for_status() schedules = sched_response.json() + logger.debug(f"Fetched schedules: {schedules}") - with transaction.atomic(): - try: - channel = Channel.objects.get(tvg_id=channel_tvg_id) - except Channel.DoesNotExist: - # skip programs for unknown channels - continue + try: + channel = Channel.objects.get(tvg_id=channel_tvg_id) + logger.debug(f"Found Channel: {channel}") + except Channel.DoesNotExist: + logger.warning(f"No channel found for tvg_id '{channel_tvg_id}'. Skipping subscription.") + continue - for sched in schedules.get('schedules', []): - title = sched.get('title', 'No Title') - desc = sched.get('description', '') - start_time = parse_schedules_direct_time(sched.get('startTime')) - end_time = parse_schedules_direct_time(sched.get('endTime')) - - Program.objects.update_or_create( - channel=channel, - title=title, - start_time=start_time, - end_time=end_time, - defaults={'description': desc} - ) + # Get or create the EPGData record for the channel + epg_data, created = EPGData.objects.get_or_create( + channel=channel, + defaults={'channel_name': channel.channel_name} + ) + if not created and epg_data.channel_name != channel.channel_name: + epg_data.channel_name = channel.channel_name + epg_data.save(update_fields=['channel_name']) + for sched in schedules.get('schedules', []): + title = sched.get('title', 'No Title') + desc = sched.get('description', '') + start_time = parse_schedules_direct_time(sched.get('startTime')) + end_time = parse_schedules_direct_time(sched.get('endTime')) + obj, created = ProgramData.objects.update_or_create( + epg=epg_data, + start_time=start_time, + title=title, + defaults={ + 'end_time': end_time, + 'description': desc, + 'sub_title': '' + } + ) + if created: + logger.info(f"Created ProgramData '{title}' for channel '{channel.channel_name}'.") + else: + logger.info(f"Updated ProgramData '{title}' for channel '{channel.channel_name}'.") except Exception as e: - # Log the error appropriately - print(f"Error fetching Schedules Direct data from {source.name}: {e}") + logger.error(f"Error fetching Schedules Direct data from {source.name}: {e}", exc_info=True) def parse_xmltv_time(time_str): - # XMLTV time format: '20250130120000 +0000' - dt = datetime.strptime(time_str[:14], '%Y%m%d%H%M%S') - tz_sign = time_str[15] - tz_hours = int(time_str[16:18]) - tz_minutes = int(time_str[18:20]) - if tz_sign == '+': - dt = dt - timedelta(hours=tz_hours, minutes=tz_minutes) - elif tz_sign == '-': - dt = dt + timedelta(hours=tz_hours, minutes=tz_minutes) - return timezone.make_aware(dt, timezone=timezone.utc) + try: + dt_obj = datetime.strptime(time_str[:14], '%Y%m%d%H%M%S') + tz_sign = time_str[15] + tz_hours = int(time_str[16:18]) + tz_minutes = int(time_str[18:20]) + if tz_sign == '+': + dt_obj = dt_obj - timedelta(hours=tz_hours, minutes=tz_minutes) + elif tz_sign == '-': + dt_obj = dt_obj + timedelta(hours=tz_hours, minutes=tz_minutes) + # Make the datetime aware with UTC using the imported dt_timezone + aware_dt = timezone.make_aware(dt_obj, timezone=dt_timezone.utc) + logger.debug(f"Parsed XMLTV time '{time_str}' to {aware_dt}") + return aware_dt + except Exception as e: + logger.error(f"Error parsing XMLTV time '{time_str}': {e}", exc_info=True) + raise def parse_schedules_direct_time(time_str): - # Schedules Direct time format: ISO 8601, e.g., '2025-01-30T12:00:00Z' - dt = datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%SZ') - return timezone.make_aware(dt, timezone=timezone.utc) + try: + dt_obj = datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%SZ') + aware_dt = timezone.make_aware(dt_obj, timezone=dt_timezone.utc) + logger.debug(f"Parsed Schedules Direct time '{time_str}' to {aware_dt}") + return aware_dt + except Exception as e: + logger.error(f"Error parsing Schedules Direct time '{time_str}': {e}", exc_info=True) + raise diff --git a/apps/epg/urls.py b/apps/epg/urls.py index 25f8f92a..cbf8bbec 100644 --- a/apps/epg/urls.py +++ b/apps/epg/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import EPGDashboardView +from .views import EPGDashboardView, epg_view app_name = 'epg_dashboard' urlpatterns = [ path('dashboard/', EPGDashboardView.as_view(), name='epg_dashboard'), + path('guide/', epg_view, name='epg_guide'), ] diff --git a/apps/epg/views.py b/apps/epg/views.py index bdfd0289..82ddb81f 100644 --- a/apps/epg/views.py +++ b/apps/epg/views.py @@ -2,8 +2,50 @@ from django.views import View from django.shortcuts import render from django.http import JsonResponse from rest_framework.parsers import JSONParser -from .models import EPGSource +from .models import EPGSource, ProgramData # Updated: import ProgramData instead of Program from .serializers import EPGSourceSerializer +from django.utils import timezone +from datetime import timedelta + + +def epg_view(request): + """ + Renders the TV guide using programmes from the next 12 hours, + grouped by channel (via EPGData). + """ + now = timezone.now() + end_time = now + timedelta(hours=12) + print(f"[EPG VIEW] Now: {now} | End Time: {end_time}") + + # Query ProgramData within the time range + programmes = ProgramData.objects.filter( + start_time__gte=now, + start_time__lte=end_time + ).order_by('start_time') + print(f"[EPG VIEW] Found {programmes.count()} programme(s) between now and end_time.") + + # Group programmes by channel (retrieved via the EPGData parent) + channels = {} + for prog in programmes: + # Assume that the EPGData instance (prog.epg) has a link to a Channel. + channel = prog.epg.channel if prog.epg and prog.epg.channel else None + if not channel: + continue + channels.setdefault(channel, []).append(prog) + + if not channels: + print("[EPG VIEW] No channels with programmes found.") + else: + for channel, progs in channels.items(): + print(f"[EPG VIEW] Channel: {channel} has {len(progs)} programme(s).") + + context = { + 'channels': channels, + 'now': now, + 'end_time': end_time, + } + return render(request, 'epg/tvguide.html', context) + class EPGDashboardView(View): def get(self, request, *args, **kwargs): diff --git a/apps/output/urls.py b/apps/output/urls.py index 7aaf9d4e..47d3e038 100644 --- a/apps/output/urls.py +++ b/apps/output/urls.py @@ -1,5 +1,5 @@ from django.urls import path, include -from .views import generate_m3u +from .views import generate_m3u, generate_epg from core.views import stream_view @@ -7,5 +7,6 @@ app_name = 'output' urlpatterns = [ path('m3u/', generate_m3u, name='generate_m3u'), + path('epg/', generate_epg, name='generate_epg'), path('stream//', stream_view, name='stream'), ] diff --git a/apps/output/views.py b/apps/output/views.py index 4f614734..2568a18f 100644 --- a/apps/output/views.py +++ b/apps/output/views.py @@ -2,6 +2,10 @@ from django.http import HttpResponse from django.urls import reverse from apps.channels.models import Channel +from datetime import timedelta +from apps.epg.models import ProgramData +from apps.channels.models import Channel +from django.utils import timezone def generate_m3u(request): """ @@ -28,3 +32,53 @@ def generate_m3u(request): response = HttpResponse(m3u_content, content_type="application/x-mpegURL") response['Content-Disposition'] = 'attachment; filename="channels.m3u"' return response + +def generate_epg(request): + """ + Dynamically generate an XMLTV (EPG) file using the new EPGData/ProgramData models. + Only channels that have EPG programmes (via EPGData) are included. + """ + now = timezone.now() + end_time = now + timedelta(hours=24) + + # Query ProgramData objects in the next 24 hours + programs = ProgramData.objects.select_related('epg__channel').filter( + start_time__gte=now, start_time__lte=end_time + ).order_by('start_time') + + # Group programmes by their channel (via EPGData) + channels_programs = {} + for prog in programs: + if prog.epg and prog.epg.channel: + channel = prog.epg.channel + channels_programs.setdefault(channel, []).append(prog) + + xml_lines = [] + xml_lines.append('') + xml_lines.append('') + + # Output channel definitions for channels that have programmes + for channel in channels_programs.keys(): + xml_lines.append(f' ') + xml_lines.append(f' {channel.channel_name}') + if channel.logo_url: + xml_lines.append(f' ') + xml_lines.append(' ') + + # Output programme entries + for channel, progs in channels_programs.items(): + for prog in progs: + start_str = prog.start_time.strftime("%Y%m%d%H%M%S %z") + stop_str = prog.end_time.strftime("%Y%m%d%H%M%S %z") + xml_lines.append(f' ') + xml_lines.append(f' {prog.title}') + xml_lines.append(f' {prog.description}') + xml_lines.append(' ') + + xml_lines.append('') + xml_content = "\n".join(xml_lines) + + response = HttpResponse(xml_content, content_type="application/xml") + response['Content-Disposition'] = 'attachment; filename="epg.xml"' + return response + diff --git a/core/urls.py b/core/urls.py new file mode 100644 index 00000000..d90dde1a --- /dev/null +++ b/core/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import settings_view + +urlpatterns = [ + path('', settings_view, name='settings'), +] diff --git a/core/views.py b/core/views.py index e2b3c1b1..76977ea4 100644 --- a/core/views.py +++ b/core/views.py @@ -6,6 +6,7 @@ import logging from django.conf import settings from django.http import StreamingHttpResponse, HttpResponseServerError from django.db.models import F +from django.shortcuts import render from apps.channels.models import Channel, Stream from core.models import StreamProfile @@ -14,6 +15,13 @@ from core.models import StreamProfile logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) logger = logging.getLogger(__name__) +def settings_view(request): + """ + Renders the settings page. + """ + return render(request, 'settings.html') + + def stream_view(request, stream_id): """ Streams the first available stream for the given channel. diff --git a/dispatcharr/urls.py b/dispatcharr/urls.py index b12f91cd..2b0c6997 100644 --- a/dispatcharr/urls.py +++ b/dispatcharr/urls.py @@ -34,7 +34,7 @@ urlpatterns = [ path('m3u/', include(('apps.m3u.urls', 'm3u'), namespace='m3u')), path('epg/', include(('apps.epg.urls', 'epg'), namespace='epg')), path('channels/', include(('apps.channels.urls', 'channels'), namespace='channels')), - #path('settings/', include(('apps.settings.urls', 'settings'), namespace='settings')), + path('settings/', include(('core.urls', 'settings'), namespace='settings')), #path('backup/', include(('apps.backup.urls', 'backup'), namespace='backup')), path('dashboard/', include(('apps.dashboard.urls', 'dashboard'), namespace='dashboard')), path('output/', include('apps.output.urls', namespace='output')), diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index b644ea3e..7fe2fed7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -34,7 +34,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" environment: - - POSTGRES_HOST=localhost + - POSTGRES_HOST=db - POSTGRES_DB=dispatcharr - POSTGRES_USER=dispatch - POSTGRES_PASSWORD=secret diff --git a/templates/channels/modals/add_stream.html b/templates/channels/modals/add_stream.html index 7d347f02..55ee6a98 100644 --- a/templates/channels/modals/add_stream.html +++ b/templates/channels/modals/add_stream.html @@ -11,7 +11,7 @@
- +
diff --git a/templates/epg/tvguide.html b/templates/epg/tvguide.html new file mode 100755 index 00000000..c9e2396c --- /dev/null +++ b/templates/epg/tvguide.html @@ -0,0 +1,34 @@ +{# templates/output/epg_tvguide.html #} +{% extends "base.html" %} +{% block title %}TV Guide{% endblock %} + +{% block content %} +

TV Guide

+

Showing programs from {{ now|date:"H:i" }} to {{ end_time|date:"H:i" }}

+ + {% for channel, programs in channels.items %} +

{{ channel.channel_name }}

+ + + + + + + + + + + {% for program in programs %} + + + + + + + {% endfor %} + +
Start TimeEnd TimeProgram TitleDescription
{{ program.start_time|date:"H:i" }}{{ program.end_time|date:"H:i" }}{{ program.title }}{{ program.description }}
+ {% empty %} +

No programs scheduled in this time frame.

+ {% endfor %} +{% endblock %} diff --git a/templates/settings.html b/templates/settings.html index ef0d87b7..0d3372d8 100755 --- a/templates/settings.html +++ b/templates/settings.html @@ -1,68 +1,334 @@ {% extends "base.html" %} -{% block title %}M3U Management - Dispatcharr{% endblock %} -{% block page_header %}M3U Management{% endblock %} +{% block title %}Settings - Dispatcharr{% endblock %} +{% block page_header %}Settings{% endblock %} {% block breadcrumb %} - - + + {% endblock %} + {% block content %}
-

M3U Accounts

- +

Settings

- - - - - - - - - - - - -
IDNameServer URLUploaded FileActiveActions
-
-
+ + - - +
+ + + + + + + + + + + + + + {% endblock %} + {% block extra_js %} + + + + + + + + + +