`GetChannelGuideHandler` interpolated `request.AccessToken` (HTTP-request-
derived, from `?access_token=`) raw into the pre-built XMLTV cache fragments.
A token containing `&`, `<`, `>`, or `"` would emit invalid XML and malform
the entire guide. Escape it with `SecurityElement.Escape`, consistent with
how #340 escaped `{RequestBase}`.
The M3U path (`ChannelPlaylist.ToM3U`) also interpolates the token but M3U is
not XML, so escaping there is neither needed nor correct — left unchanged.
Regression test `Guide_xml_escapes_access_token` drives the real handler with
a token containing all four XML-special chars and asserts the output is
escaped (sibling to the #340 `Guide_xml_escapes_advertised_base_url` test).
Verified non-vacuous: it fails with the escape reverted.
fixes#376
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Independent review found that AdvertisedBaseUrl.TryParse accepts a path
prefix containing '&' (a legal URL-path char kept out of uri.Query), but
GetChannelGuideHandler substituted {RequestBase} raw into pre-built XML
written unescaped — so a configured base like https://host/a&b emitted a
bare '&', malforming the entire XMLTV guide (clients reject the document).
Escape the substituted base with SecurityElement.Escape before the raw
replace, mirroring how {AccessTokenUri} is already pre-escaped (&).
No-op for normal URLs (goldens unchanged); M3U output is untouched (M3U
isn't XML). Adds a regression test asserting '&' → '&' in the guide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* add paging to media server show library calls
* add paging to media server season and episode library calls
* formatting
* add paging to media server collection calls
* add paging to media server collection item calls
* update changelog