fix(api): update HDHR URLs to use port-aware URI builder

- Refactored HDHR API views to utilize `build_absolute_uri_with_port()` for generating URLs, ensuring correct handling of non-standard ports in lineup and discovery responses.
This commit is contained in:
SergeantPanda 2026-06-07 14:27:57 -05:00
parent b928de7d50
commit a8a3c70e55
3 changed files with 18 additions and 8 deletions

View file

@ -54,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- **HDHR lineup and discovery URLs now use the shared port-aware URI builder.** `lineup.json`, `discover.json`, and `device.xml` previously called Django's `request.build_absolute_uri()`, which drops non-standard ports behind reverse proxies and on dev installs. They now use `build_absolute_uri_with_port()` from `core/utils.py`, matching M3U, EPG, XC, and logo cache URLs.
- **EPG channel list did not refresh after a source finished parsing channels.** The `epg_refresh` WebSocket handler closed over a stale `epgs` snapshot from when the socket connected. When a newly created source was not found in that snapshot, the handler updated progress and returned early without calling `fetchEPGData()`, so the channel picker and EPG assignment UI stayed empty until a full page reload. The handler now reads the current store via `getState()` and still calls `fetchEPGData()` when `parsing_channels` completes.
- **DVR recordings no longer stop immediately when FFmpeg exits mid-stream.** If FFmpeg crashes, stalls, or loses the source before the scheduled end time, `run_recording` now restarts it in-process instead of going straight to HLS→MKV concat and marking the recording complete. Each restart reuses the same HLS working directory and continues segment numbering (`append_list`, `-start_number`) so the final MKV is a single continuous file. Retries are bounded by a per-outage time window matching live-proxy client tolerance (`STREAM_TIMEOUT` + `FAILOVER_GRACE_PERIOD`, default 80s); the window resets whenever new segments resume, so a long recording can survive multiple separate outages. Reconnect pacing between attempts follows the same `min(0.25 × attempt, 3s)` backoff used by the live-proxy `StreamManager`. Input demuxing also uses `-err_detect ignore_err` to tolerate minor TS corruption without aborting the process. If the outage window expires without recovery, the recording is saved as `interrupted` with `ffmpeg_outage_window_exhausted` rather than falsely reported as `completed`. (Closes #1170)
- **DVR HLS→MKV concat now tolerates timestamp splices and corrupt segments.** The finalize step (and startup recovery concat) previously used a bare `ffmpeg -f concat -c copy`, which could fail on truncated tail segments or PTS discontinuities from FFmpeg restarts mid-recording. Concat now uses a shared `_dvr_build_hls_concat_cmd` helper with `-fflags +genpts+igndts+discardcorrupt`, `-err_detect ignore_err`, and `-avoid_negative_ts make_zero`; the existing MP4-intermediate fallback path uses the same tolerant input flags.

View file

@ -19,6 +19,7 @@ from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from dispatcharr.utils import network_access_allowed
from core.utils import build_absolute_uri_with_port
# Configure logger
logger = logging.getLogger(__name__)
@ -75,7 +76,7 @@ class DiscoverAPIView(APIView):
uri_parts.append("output_profile")
uri_parts.append(str(output_profile_id))
base_url = request.build_absolute_uri(f'/{"/".join(uri_parts)}/').rstrip("/")
base_url = build_absolute_uri_with_port(request, f'/{"/".join(uri_parts)}/').rstrip("/")
device = HDHRDevice.objects.first()
from apps.m3u.utils import calculate_tuner_count
@ -166,6 +167,13 @@ class LineupAPIView(APIView):
resolved_output_profile_id = _resolve_hdhr_output_profile_id(output_profile_id)
_stream_url_prefix = build_absolute_uri_with_port(request, "/proxy/ts/stream/")
_output_profile_qs = (
f"?output_profile={resolved_output_profile_id}"
if resolved_output_profile_id is not None
else ""
)
lineup = []
for ch in channels:
formatted = format_channel_number(ch.effective_channel_number, empty=None)
@ -173,9 +181,7 @@ class LineupAPIView(APIView):
continue
formatted_channel_number = str(formatted)
stream_url = request.build_absolute_uri(f"/proxy/ts/stream/{ch.uuid}")
if resolved_output_profile_id is not None:
stream_url += f"?output_profile={resolved_output_profile_id}"
stream_url = f"{_stream_url_prefix}{ch.uuid}{_output_profile_qs}"
lineup.append(
{
@ -224,7 +230,7 @@ class HDHRDeviceXMLAPIView(APIView):
if blocked is not None:
return blocked
base_url = request.build_absolute_uri("/hdhr/").rstrip("/")
base_url = build_absolute_uri_with_port(request, "/hdhr/").rstrip("/")
xml_response = f"""<?xml version="1.0" encoding="utf-8"?>
<root>

View file

@ -15,6 +15,7 @@ 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
from core.utils import build_absolute_uri_with_port
@login_required
@ -46,7 +47,7 @@ class DiscoverAPIView(APIView):
description="Retrieve HDHomeRun device discovery information",
)
def get(self, request):
base_url = request.build_absolute_uri("/hdhr/").rstrip("/")
base_url = build_absolute_uri_with_port(request, "/hdhr/").rstrip("/")
device = HDHRDevice.objects.first()
if not device:
@ -92,6 +93,8 @@ class LineupAPIView(APIView):
.exclude(hidden_from_output=True)
.order_by("effective_channel_number")
)
_stream_url_prefix = build_absolute_uri_with_port(request, "/proxy/ts/stream/")
lineup = []
for ch in channels:
formatted = format_channel_number(ch.effective_channel_number, empty=None)
@ -102,7 +105,7 @@ class LineupAPIView(APIView):
{
"GuideNumber": formatted_channel_number,
"GuideName": ch.effective_name,
"URL": request.build_absolute_uri(f"/proxy/ts/stream/{ch.uuid}"),
"URL": f"{_stream_url_prefix}{ch.uuid}",
}
)
return JsonResponse(lineup, safe=False)
@ -133,7 +136,7 @@ class HDHRDeviceXMLAPIView(APIView):
description="Retrieve the HDHomeRun device XML configuration",
)
def get(self, request):
base_url = request.build_absolute_uri("/hdhr/").rstrip("/")
base_url = build_absolute_uri_with_port(request, "/hdhr/").rstrip("/")
xml_response = f"""<?xml version="1.0" encoding="utf-8"?>
<root>