Proyecto LCX Dispatcharr multicuenta
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled
Base Image Build / prepare (push) Has been cancelled
Build and Push Multi-Arch Docker Image / build-and-push (push) Has been cancelled
Frontend Tests / test (push) Has been cancelled
Base Image Build / docker (amd64, ubuntu-24.04) (push) Has been cancelled
Base Image Build / docker (arm64, ubuntu-24.04-arm) (push) Has been cancelled
Base Image Build / create-manifest (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from .models import HDHRDevice
|
||||
|
||||
@admin.register(HDHRDevice)
|
||||
class HDHRDeviceAdmin(admin.ModelAdmin):
|
||||
list_display = ('friendly_name', 'device_id', 'tuner_count')
|
||||
@@ -0,0 +1,19 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .api_views import HDHRDeviceViewSet, DiscoverAPIView, LineupAPIView, LineupStatusAPIView, HDHRDeviceXMLAPIView, hdhr_dashboard_view
|
||||
|
||||
app_name = 'hdhr'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'devices', HDHRDeviceViewSet, basename='hdhr-device')
|
||||
|
||||
urlpatterns = [
|
||||
path('dashboard/', hdhr_dashboard_view, name='hdhr_dashboard'),
|
||||
path('', hdhr_dashboard_view, name='hdhr_dashboard'),
|
||||
path('discover.json', DiscoverAPIView.as_view(), name='discover'),
|
||||
path('lineup.json', LineupAPIView.as_view(), name='lineup'),
|
||||
path('lineup_status.json', LineupStatusAPIView.as_view(), name='lineup_status'),
|
||||
path('device.xml', HDHRDeviceXMLAPIView.as_view(), name='device_xml'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
@@ -0,0 +1,182 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import AllowAny
|
||||
from apps.accounts.permissions import Authenticated, permission_classes_by_action
|
||||
from django.http import JsonResponse, HttpResponseForbidden, HttpResponse
|
||||
import logging
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db import models
|
||||
from apps.channels.models import Channel, ChannelProfile, Stream
|
||||
from .models import HDHRDevice
|
||||
from .serializers import HDHRDeviceSerializer
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
# Configure logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@login_required
|
||||
def hdhr_dashboard_view(request):
|
||||
"""Render the HDHR management page."""
|
||||
hdhr_devices = HDHRDevice.objects.all()
|
||||
return render(request, "hdhr/hdhr.html", {"hdhr_devices": hdhr_devices})
|
||||
|
||||
|
||||
# 🔹 1) HDHomeRun Device API
|
||||
class HDHRDeviceViewSet(viewsets.ModelViewSet):
|
||||
"""Handles CRUD operations for HDHomeRun devices"""
|
||||
|
||||
queryset = HDHRDevice.objects.all()
|
||||
serializer_class = HDHRDeviceSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
|
||||
# 🔹 2) Discover API
|
||||
class DiscoverAPIView(APIView):
|
||||
"""Returns device discovery information"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve HDHomeRun device discovery information",
|
||||
)
|
||||
def get(self, request, profile=None):
|
||||
uri_parts = ["hdhr"]
|
||||
if profile is not None:
|
||||
uri_parts.append(profile)
|
||||
|
||||
base_url = request.build_absolute_uri(f'/{"/".join(uri_parts)}/').rstrip("/")
|
||||
device = HDHRDevice.objects.first()
|
||||
|
||||
# Calculate tuner count using centralized function
|
||||
from apps.m3u.utils import calculate_tuner_count
|
||||
tuner_count = calculate_tuner_count(minimum=1, unlimited_default=10)
|
||||
|
||||
# Create a unique DeviceID for the HDHomeRun device based on profile ID or a default value
|
||||
device_ID = "12345678" # Default DeviceID
|
||||
friendly_name = "Dispatcharr HDHomeRun"
|
||||
if profile is not None:
|
||||
device_ID = f"dispatcharr-hdhr-{profile}"
|
||||
friendly_name = f"Dispatcharr HDHomeRun - {profile}"
|
||||
if not device:
|
||||
data = {
|
||||
"FriendlyName": friendly_name,
|
||||
"ModelNumber": "HDTC-2US",
|
||||
"FirmwareName": "hdhomerun3_atsc",
|
||||
"FirmwareVersion": "20200101",
|
||||
"DeviceID": device_ID,
|
||||
"DeviceAuth": "test_auth_token",
|
||||
"BaseURL": base_url,
|
||||
"LineupURL": f"{base_url}/lineup.json",
|
||||
"TunerCount": tuner_count,
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"FriendlyName": device.friendly_name,
|
||||
"ModelNumber": "HDTC-2US",
|
||||
"FirmwareName": "hdhomerun3_atsc",
|
||||
"FirmwareVersion": "20200101",
|
||||
"DeviceID": device.device_id,
|
||||
"DeviceAuth": "test_auth_token",
|
||||
"BaseURL": base_url,
|
||||
"LineupURL": f"{base_url}/lineup.json",
|
||||
"TunerCount": tuner_count,
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
# 🔹 3) Lineup API
|
||||
class LineupAPIView(APIView):
|
||||
"""Returns available channel lineup"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the available channel lineup",
|
||||
)
|
||||
def get(self, request, profile=None):
|
||||
if profile is not None:
|
||||
channel_profile = ChannelProfile.objects.get(name=profile)
|
||||
channels = Channel.objects.filter(
|
||||
channelprofilemembership__channel_profile=channel_profile,
|
||||
channelprofilemembership__enabled=True,
|
||||
).order_by("channel_number")
|
||||
else:
|
||||
channels = Channel.objects.all().order_by("channel_number")
|
||||
|
||||
lineup = []
|
||||
for ch in channels:
|
||||
# Format channel number as integer if it has no decimal component
|
||||
if ch.channel_number is not None:
|
||||
if ch.channel_number == int(ch.channel_number):
|
||||
formatted_channel_number = str(int(ch.channel_number))
|
||||
else:
|
||||
formatted_channel_number = str(ch.channel_number)
|
||||
else:
|
||||
formatted_channel_number = ""
|
||||
|
||||
lineup.append(
|
||||
{
|
||||
"GuideNumber": formatted_channel_number,
|
||||
"GuideName": ch.name,
|
||||
"URL": request.build_absolute_uri(f"/proxy/ts/stream/{ch.uuid}"),
|
||||
"Guide_ID": formatted_channel_number,
|
||||
"Station": formatted_channel_number,
|
||||
}
|
||||
)
|
||||
return JsonResponse(lineup, safe=False)
|
||||
|
||||
|
||||
# 🔹 4) Lineup Status API
|
||||
class LineupStatusAPIView(APIView):
|
||||
"""Returns the current status of the HDHR lineup"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the HDHomeRun lineup status",
|
||||
)
|
||||
def get(self, request, profile=None):
|
||||
data = {
|
||||
"ScanInProgress": 0,
|
||||
"ScanPossible": 0,
|
||||
"Source": "Cable",
|
||||
"SourceList": ["Cable"],
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
# 🔹 5) Device XML API
|
||||
class HDHRDeviceXMLAPIView(APIView):
|
||||
"""Returns HDHomeRun device configuration in XML"""
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the HDHomeRun device XML configuration",
|
||||
)
|
||||
def get(self, request):
|
||||
base_url = request.build_absolute_uri("/hdhr/").rstrip("/")
|
||||
|
||||
xml_response = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<DeviceID>12345678</DeviceID>
|
||||
<FriendlyName>Dispatcharr HDHomeRun</FriendlyName>
|
||||
<ModelNumber>HDTC-2US</ModelNumber>
|
||||
<FirmwareName>hdhomerun3_atsc</FirmwareName>
|
||||
<FirmwareVersion>20200101</FirmwareVersion>
|
||||
<DeviceAuth>test_auth_token</DeviceAuth>
|
||||
<BaseURL>{base_url}</BaseURL>
|
||||
<LineupURL>{base_url}/lineup.json</LineupURL>
|
||||
</root>"""
|
||||
|
||||
return HttpResponse(xml_response, content_type="application/xml")
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
from . import ssdp
|
||||
|
||||
class HdhrConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.hdhr'
|
||||
verbose_name = "HDHomeRun Emulation"
|
||||
def ready(self):
|
||||
# Start SSDP services when the app is ready
|
||||
ssdp.start_ssdp()
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-05 22:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HDHRDevice',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('friendly_name', models.CharField(default='Dispatcharr HDHomeRun', max_length=100)),
|
||||
('device_id', models.CharField(max_length=32, unique=True)),
|
||||
('tuner_count', models.PositiveIntegerField(default=3)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
class HDHRDevice(models.Model):
|
||||
friendly_name = models.CharField(max_length=100, default='Dispatcharr HDHomeRun')
|
||||
device_id = models.CharField(max_length=32, unique=True)
|
||||
tuner_count = models.PositiveIntegerField(default=3)
|
||||
|
||||
def __str__(self):
|
||||
return self.friendly_name
|
||||
@@ -0,0 +1,10 @@
|
||||
from rest_framework import serializers
|
||||
from .models import HDHRDevice
|
||||
|
||||
|
||||
class HDHRDeviceSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for HDHomeRun device information"""
|
||||
|
||||
class Meta:
|
||||
model = HDHRDevice
|
||||
fields = ['id', 'friendly_name', 'device_id', 'tuner_count']
|
||||
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import gevent # Add this import
|
||||
from django.conf import settings
|
||||
|
||||
# SSDP Multicast Address and Port
|
||||
SSDP_MULTICAST = "239.255.255.250"
|
||||
SSDP_PORT = 1900
|
||||
|
||||
DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1"
|
||||
SERVER_PORT = 8000
|
||||
|
||||
def get_host_ip():
|
||||
try:
|
||||
# This relies on "host.docker.internal" being mapped to the host’s gateway IP.
|
||||
return socket.gethostbyname("host.docker.internal")
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
|
||||
def ssdp_response(addr, host_ip):
|
||||
response = (
|
||||
f"HTTP/1.1 200 OK\r\n"
|
||||
f"CACHE-CONTROL: max-age=1800\r\n"
|
||||
f"EXT:\r\n"
|
||||
f"LOCATION: http://{host_ip}:{SERVER_PORT}/hdhr/device.xml\r\n"
|
||||
f"SERVER: Dispatcharr/1.0 UPnP/1.0 HDHomeRun/1.0\r\n"
|
||||
f"ST: {DEVICE_TYPE}\r\n"
|
||||
f"USN: uuid:device1-1::{DEVICE_TYPE}\r\n"
|
||||
f"\r\n"
|
||||
)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.sendto(response.encode("utf-8"), addr)
|
||||
sock.close()
|
||||
|
||||
def ssdp_listener(host_ip):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind((SSDP_MULTICAST, SSDP_PORT))
|
||||
while True:
|
||||
data, addr = sock.recvfrom(1024)
|
||||
if b"M-SEARCH" in data and DEVICE_TYPE.encode("utf-8") in data:
|
||||
print(f"Received M-SEARCH from {addr}")
|
||||
ssdp_response(addr, host_ip)
|
||||
|
||||
def ssdp_broadcaster(host_ip):
|
||||
notify = (
|
||||
f"NOTIFY * HTTP/1.1\r\n"
|
||||
f"HOST: {SSDP_MULTICAST}:{SSDP_PORT}\r\n"
|
||||
f"CACHE-CONTROL: max-age=1800\r\n"
|
||||
f"LOCATION: http://{host_ip}:{SERVER_PORT}/hdhr/device.xml\r\n"
|
||||
f"SERVER: Dispatcharr/1.0 UPnP/1.0 HDHomeRun/1.0\r\n"
|
||||
f"NT: {DEVICE_TYPE}\r\n"
|
||||
f"NTS: ssdp:alive\r\n"
|
||||
f"USN: uuid:device1-1::{DEVICE_TYPE}\r\n"
|
||||
f"\r\n"
|
||||
)
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
|
||||
while True:
|
||||
sock.sendto(notify.encode("utf-8"), (SSDP_MULTICAST, SSDP_PORT))
|
||||
gevent.sleep(30) # Replace time.sleep with gevent.sleep
|
||||
|
||||
def start_ssdp():
|
||||
host_ip = get_host_ip()
|
||||
threading.Thread(target=ssdp_listener, args=(host_ip,), daemon=True).start()
|
||||
threading.Thread(target=ssdp_broadcaster, args=(host_ip,), daemon=True).start()
|
||||
print(f"SSDP services started on {host_ip}.")
|
||||
@@ -0,0 +1,22 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .api_views import HDHRDeviceViewSet, DiscoverAPIView, LineupAPIView, LineupStatusAPIView, HDHRDeviceXMLAPIView, hdhr_dashboard_view
|
||||
|
||||
app_name = 'hdhr'
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'devices', HDHRDeviceViewSet, basename='hdhr-device')
|
||||
|
||||
urlpatterns = [
|
||||
path('dashboard/', hdhr_dashboard_view, name='hdhr_dashboard'),
|
||||
path('', hdhr_dashboard_view, name='hdhr_dashboard'),
|
||||
path('<str:profile>/discover.json', DiscoverAPIView.as_view(), name='discover_with_profile'),
|
||||
path('discover.json', DiscoverAPIView.as_view(), name='discover_no_profile'),
|
||||
path('<str:profile>/lineup.json', LineupAPIView.as_view(), name='lineup_with_profile'),
|
||||
path('lineup.json', LineupAPIView.as_view(), name='lineup_no_profile'),
|
||||
path('<str:profile>/lineup_status.json', LineupStatusAPIView.as_view(), name='lineup_status_with_profile'),
|
||||
path('lineup_status.json', LineupStatusAPIView.as_view(), name='lineup_status_no_profile'),
|
||||
path('device.xml', HDHRDeviceXMLAPIView.as_view(), name='device_xml'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
@@ -0,0 +1,138 @@
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from apps.accounts.permissions import Authenticated, permission_classes_by_action
|
||||
from django.http import JsonResponse, HttpResponseForbidden, HttpResponse
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from django.shortcuts import get_object_or_404
|
||||
from apps.channels.models import Channel
|
||||
from .models import HDHRDevice
|
||||
from .serializers import HDHRDeviceSerializer
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
||||
@login_required
|
||||
def hdhr_dashboard_view(request):
|
||||
"""Render the HDHR management page."""
|
||||
hdhr_devices = HDHRDevice.objects.all()
|
||||
return render(request, "hdhr/hdhr.html", {"hdhr_devices": hdhr_devices})
|
||||
|
||||
|
||||
# 🔹 1) HDHomeRun Device API
|
||||
class HDHRDeviceViewSet(viewsets.ModelViewSet):
|
||||
"""Handles CRUD operations for HDHomeRun devices"""
|
||||
|
||||
queryset = HDHRDevice.objects.all()
|
||||
serializer_class = HDHRDeviceSerializer
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
|
||||
# 🔹 2) Discover API
|
||||
class DiscoverAPIView(APIView):
|
||||
"""Returns device discovery information"""
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve HDHomeRun device discovery information",
|
||||
)
|
||||
def get(self, request):
|
||||
base_url = request.build_absolute_uri("/hdhr/").rstrip("/")
|
||||
device = HDHRDevice.objects.first()
|
||||
|
||||
if not device:
|
||||
data = {
|
||||
"FriendlyName": "Dispatcharr HDHomeRun",
|
||||
"ModelNumber": "HDTC-2US",
|
||||
"FirmwareName": "hdhomerun3_atsc",
|
||||
"FirmwareVersion": "20200101",
|
||||
"DeviceID": "12345678",
|
||||
"DeviceAuth": "test_auth_token",
|
||||
"BaseURL": base_url,
|
||||
"LineupURL": f"{base_url}/lineup.json",
|
||||
"TunerCount": 10,
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"FriendlyName": device.friendly_name,
|
||||
"ModelNumber": "HDTC-2US",
|
||||
"FirmwareName": "hdhomerun3_atsc",
|
||||
"FirmwareVersion": "20200101",
|
||||
"DeviceID": device.device_id,
|
||||
"DeviceAuth": "test_auth_token",
|
||||
"BaseURL": base_url,
|
||||
"LineupURL": f"{base_url}/lineup.json",
|
||||
"TunerCount": 10,
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
# 🔹 3) Lineup API
|
||||
class LineupAPIView(APIView):
|
||||
"""Returns available channel lineup"""
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the available channel lineup",
|
||||
)
|
||||
def get(self, request):
|
||||
channels = Channel.objects.all().order_by("channel_number")
|
||||
lineup = [
|
||||
{
|
||||
"GuideNumber": str(ch.channel_number),
|
||||
"GuideName": ch.name,
|
||||
"URL": request.build_absolute_uri(f"/proxy/ts/stream/{ch.uuid}"),
|
||||
}
|
||||
for ch in channels
|
||||
]
|
||||
return JsonResponse(lineup, safe=False)
|
||||
|
||||
|
||||
# 🔹 4) Lineup Status API
|
||||
class LineupStatusAPIView(APIView):
|
||||
"""Returns the current status of the HDHR lineup"""
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the HDHomeRun lineup status",
|
||||
)
|
||||
def get(self, request):
|
||||
data = {
|
||||
"ScanInProgress": 0,
|
||||
"ScanPossible": 0,
|
||||
"Source": "Cable",
|
||||
"SourceList": ["Cable"],
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
|
||||
# 🔹 5) Device XML API
|
||||
class HDHRDeviceXMLAPIView(APIView):
|
||||
"""Returns HDHomeRun device configuration in XML"""
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the HDHomeRun device XML configuration",
|
||||
)
|
||||
def get(self, request):
|
||||
base_url = request.build_absolute_uri("/hdhr/").rstrip("/")
|
||||
|
||||
xml_response = f"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<DeviceID>12345678</DeviceID>
|
||||
<FriendlyName>Dispatcharr HDHomeRun</FriendlyName>
|
||||
<ModelNumber>HDTC-2US</ModelNumber>
|
||||
<FirmwareName>hdhomerun3_atsc</FirmwareName>
|
||||
<FirmwareVersion>20200101</FirmwareVersion>
|
||||
<DeviceAuth>test_auth_token</DeviceAuth>
|
||||
<BaseURL>{base_url}</BaseURL>
|
||||
<LineupURL>{base_url}/lineup.json</LineupURL>
|
||||
</root>"""
|
||||
|
||||
return HttpResponse(xml_response, content_type="application/xml")
|
||||
Reference in New Issue
Block a user