diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 79aadf7d6..55157fe10 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -131,7 +131,7 @@ jobs: build: name: Build & push image (amd64) runs-on: ubuntu-latest - needs: test + needs: [test, migrations] if: github.event_name != 'pull_request' steps: - name: Checkout diff --git a/docs/handoffs/m3u-tvg-logo-fix.md b/docs/handoffs/m3u-tvg-logo-fix.md new file mode 100644 index 000000000..0e7aaf35f --- /dev/null +++ b/docs/handoffs/m3u-tvg-logo-fix.md @@ -0,0 +1,79 @@ +# Handoff — ersatztv#1: M3U `tvg-logo` (and channel-logo) URLs hardcoding localhost + +**How to use:** start a fresh session and say something like +*"Read `docs/handoffs/m3u-tvg-logo-fix.md` and execute ersatztv#1"* (optionally *"with ultracode"* / +*"use a workflow"*). Read **ersatztv#1** in full (Gitea, `timothy/ersatztv`) before acting. + +This is the first **feature/bug** task after the engineering-foundations batch (all 8 done: CPM, vuln +deps, analyzers, arch tests, golden tests, migration checks, IPTV E2E, contributors guide). #1 is +`priority: high` (Jellyfin channel logos break after every guide refresh) and was deliberately +**unblocked + protected** by that work. + +## ⚠️ Investigation first — the issue's stated root cause is partly a RED HERRING + +A quick trace (do not skip — it'll save you from the wrong rabbit hole): + +- `ChannelLogoGenerator.GenerateChannelLogoUrl()` (`ErsatzTV.Core/Images/ChannelLogoGenerator.cs:81`) + **does** hardcode `http://localhost:{StreamingPort}…`. **But its only callers are in + `ErsatzTV.Core/FFmpeg/WatermarkSelector.cs`** (4×) — i.e. it's the URL **ffmpeg** fetches to draw the + channel logo as a *watermark*, and ffmpeg runs **inside the ErsatzTV container**, where + `localhost:8409` correctly reaches ErsatzTV. So this `localhost` is **not** the M3U bug — leave it + (or confirm before touching). +- The **M3U** `tvg-logo` is built in `ChannelPlaylist.ToM3U()` + (`ErsatzTV.Core/Iptv/ChannelPlaylist.cs:65,70`) from `{_scheme}://{_host}{_baseUrl}` — i.e. the + **request** host. That's already "correct" given a correct request host. +- The **XMLTV** guide (`GetChannelGuideHandler`) and artwork (`RefreshChannelDataHandler:~1114`) use a + **`{RequestBase}` placeholder** substituted at request time with `{Scheme}://{Host}{BaseUrl}` — also + request-host based. +- `Startup` **does** configure `ForwardedHeaders.All` (`Startup.cs:126`) + `app.UseForwardedHeaders()` + (`:534`), and `ETV_BASE_URL` → `UsePathBase` (`:505`). + +**So: where does `localhost` actually reach Jellyfin?** Most likely one of: +1. **Request flow / proxy**: Jellyfin (or a reverse proxy) fetches the M3U via a host that resolves to + `localhost` from ErsatzTV's perspective, **or** `UseForwardedHeaders()` runs too late / the proxy + isn't sending `X-Forwarded-Host`, so `Request.Host` ends up `localhost:8409`. Check middleware + **ordering** (forwarded-headers must run before anything reading `Request.Host`) and what host + ErsatzTV actually sees. +2. **A cached value**: `channels.xml` / per-channel fragments are generated by `RefreshChannelList`/ + `RefreshChannelData` (background, **no** request) — confirm every host-bearing field there is a + `{RequestBase}` placeholder, not a baked absolute URL. A baked `localhost` there would leak into the + guide regardless of the request. +3. **Genuine misconfiguration** (the issue flags this): the Jellyfin-side M3U URL points at `localhost`. + +**Reproduce first.** You can run the real image on bumblebee (Docker host, `ssh timothy@192.168.1.99`, +registry `192.168.1.95:3000/timothy/ersatztv:latest`) and curl `/iptv/channels.m3u` + +`/iptv/xmltv.xml` with different `Host`/`X-Forwarded-*` headers to see exactly when `localhost` +appears. (This is how #16's E2E was validated.) + +## Likely fix shapes (decide after repro) +- If it's forwarded-headers ordering/config → fix middleware order / `KnownProxies`/`KnownNetworks`, + or honor an explicit configured public base URL. +- If a background-generated cache bakes a host → switch it to the `{RequestBase}` placeholder pattern + (copy `GetChannelGuideHandler.cs:51` / `RefreshChannelDataHandler.cs:~1114`). +- If purely Jellyfin/proxy config → document it (and this may belong in server-management, not here). + +## Regression net (already in place — use it) +- **M3U golden tests**: `ErsatzTV.Core.Tests/Iptv/ChannelPlaylistGoldenTests.cs`. If your fix changes + `ToM3U` output, the goldens **will** fail — review the diff, then regenerate intentionally with + `ETV_UPDATE_GOLDENS=1` (or the `[Explicit] Regenerate_goldens` test) and commit. (ersatztv#11) +- **Container E2E**: the smoke step asserts `/iptv/channels.m3u`→`#EXTM3U` and `/iptv/xmltv.xml`→`