ErsatzTV built every absolute M3U/XMLTV URL from the incoming request's
Scheme/Host/PathBase, so a client fetching via a host that downstream
consumers can't resolve (e.g. Dispatcharr over Docker DNS → Kodi) baked
that internal host into programme-image/stream URLs.
Add an optional advertised IPTV base URL, backed by the existing
ConfigElement key/value store (key `iptv.base_url`, no EF migration):
- Central pure Core helper `AdvertisedBaseUrl` (TryParse/Resolve):
validates absolute http(s), no credentials/query/fragment, preserves
port + path prefix, normalizes trailing slash. Blank/invalid falls
back to the request-derived values, so unset output is byte-identical.
- Resolved inside `GetChannelPlaylistHandler` (M3U guide/logo/stream) and
`GetChannelGuideHandler` (both XMLTV {RequestBase} sites) — controllers
stay thin, golden tests untouched.
- New `iptv` settings group: GET/PUT /api/v1/settings/iptv (blank clears,
malformed → 422) + a new IPTV section on the SPA Settings screen.
- Scoped to M3U + XMLTV; HDHomeRun deliberately out of scope. Distinct
from ETV_BASE_URL (which only sets ASP.NET PathBase).
Tests: AdvertisedBaseUrl unit tests (override/fallback/port/path/invalid),
handler override tests for both generators, settings controller + handler
tests, SPA client + screen tests. Docs: m3u-xmltv, decisions, domain-model,
regenerated OpenAPI v1.json + v1.d.ts + endpoint-index.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.7 KiB
C#
68 lines
2.7 KiB
C#
namespace ErsatzTV.Core.Iptv;
|
|
|
|
// Central resolution/validation for the optional advertised IPTV base URL (issue #340).
|
|
//
|
|
// ErsatzTV builds every absolute M3U/XMLTV URL from the incoming request's scheme + host + PathBase.
|
|
// When a downstream consumer fetches ErsatzTV via a host that other consumers can't resolve (e.g.
|
|
// Dispatcharr fetching over Docker DNS, then Kodi receiving those internal hostnames), the emitted
|
|
// URLs break. An operator can configure an advertised base URL to override those request-derived
|
|
// values consistently across the M3U (guide/logo/stream) and XMLTV ({RequestBase}) surfaces.
|
|
//
|
|
// When the configured value is blank or invalid, resolution falls back to the request-derived values
|
|
// so today's behavior is preserved byte-for-byte.
|
|
public static class AdvertisedBaseUrl
|
|
{
|
|
// Returns the effective (scheme, host, baseUrl) to use for absolute IPTV URLs. When the configured
|
|
// value is blank or invalid, returns the request-derived values unchanged.
|
|
public static (string Scheme, string Host, string BaseUrl) Resolve(
|
|
string configured,
|
|
string requestScheme,
|
|
string requestHost,
|
|
string requestBaseUrl) =>
|
|
TryParse(configured).Match(
|
|
Some: parsed => parsed,
|
|
None: () => (requestScheme, requestHost, requestBaseUrl));
|
|
|
|
// Validates + normalizes an advertised base URL. None => blank or invalid. A valid value is an
|
|
// absolute http(s) URL with no credentials, query, or fragment; an optional port and path prefix
|
|
// are preserved, and a trailing slash is normalized away (so a root "/" yields an empty base, and
|
|
// "/etv/" yields "/etv" — matching the PathBase convention the URL builders concatenate).
|
|
public static Option<(string Scheme, string Host, string BaseUrl)> TryParse(string configured)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(configured))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if (!Uri.TryCreate(configured.Trim(), UriKind.Absolute, out Uri uri))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(uri.UserInfo))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(uri.Query) || !string.IsNullOrEmpty(uri.Fragment))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(uri.Host))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
// Uri.Authority is host[:port] (omitting a redundant default port, wrapping IPv6 in brackets)
|
|
// and excludes any userinfo — exactly the "{host}" the URL builders expect.
|
|
string baseUrl = uri.AbsolutePath.TrimEnd('/');
|
|
return (uri.Scheme, uri.Authority, baseUrl);
|
|
}
|
|
}
|