Skip to content

feat(live-tv): support catch-up on M3U playlists - #216

Open
bilipp wants to merge 1 commit into
mainfrom
feature/m3u-m3u8-catch-up-archive
Open

feat(live-tv): support catch-up on M3U playlists#216
bilipp wants to merge 1 commit into
mainfrom
feature/m3u-m3u8-catch-up-archive

Conversation

@bilipp

@bilipp bilipp commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Catch-up / archive playback only worked for Xtream Codes playlists. An M3U playlist that declares catch-up per channel never marked any channel as having an archive, so the Guide never offered a past programme — while other players play the same archive from the same playlist. This makes M3U channels behave like Xtream tv_archive channels: the clock affordance appears, the archive depth comes from catchup-days, and selecting a past programme builds a playable URL.

Implementation

Parsing. M3UParser now reads the catchup attributes it already collected and discarded — catchup / catchup-type, catchup-days falling back to timeshift then tvg-rec, and catchup-source — and accepts unquoted values, which real playlists emit and the quoted-only scanner silently skipped. The attribute scan now stops at the display-name comma, so a channel named Sky F1=HD can no longer yield a bogus attribute. That fix also made the parser 15.9% faster than before this branch: the old scan walked the entire line. LumePerformanceTests/README.md records the measurement, including the module-placement confound that makes the naive before/after comparison misleading.

URL building. New M3UCatchupURL covers all five dialects — flussonic/flussonic-hls/fs (filename rewrite, with an explicit catchup-source winning over the convention), append, default/shift template expansion, and xc, which recovers credentials from the channel URL path because an M3U Playlist carries none and streamId is an FNV hash. Timestamps are raw UTC epochs and {duration} is seconds; the Xtream builder's wall-clock-and-minutes convention exists to keep XC panels from answering 400 and is reached only through xc. URLs are built from the raw directURL, deliberately bypassing PlaylistStreamFormat.applied(to:), which swaps .m3u8.ts and would 404 a Flussonic archive for anyone who set the playlist to MPEG-TS. Every dialect's result is validated to an absolute http(s) URL with a host.

Two decisions worth reviewing. First, buildability is decided once at import and persisted as tvArchive, so a channel only badges when a URL can actually be produced, and no view re-derives it — EPGChannelRow is a value snapshot precisely because SwiftData reads fault to SQLite on the main thread during scroll. Second, the capability rule was previously spelled three different ways across nine sites; it is now one shared predicate, so a badge and its tap can no longer disagree. Getting that ordering wrong would have shipped dead buttons on all four platforms, so the gate was converged before the importer began setting tvArchive.

A stored tvArchiveDuration of 0 means "declared, depth unknown" and renders a day-less badge reusing the existing Catch-up available string. No new localized strings are added. Xtream behaviour is unchanged throughout — the unknown-depth window applies only to M3U channels.

Testing

  • Acceptance criteria met
  • Tests pass (xcodebuild test -scheme Lume -destination 'platform=iOS Simulator,name=iPhone 17 Pro') — 1076/1076
  • SwiftLint clean (SwiftFormat 0/28, SwiftLint --strict silent)
  • Tests added — new M3UCatchupURLTests (per-dialect exact-URL assertions, relative/protocol-relative/garbage-source rejection, canBuild/build agreement), plus cases in M3UParserTests, M3USyncTests, PlayableMediaTests, EPGGridBuilderTests and ModelTests

Verified on a simulator against a playlist covering every dialect. All six catch-up cases badged with the right depth (flussonic+7d, tvg-rec flag+6d, default+source+3d, append+query+2d, unquoted days+4d), catchup="fs" with no day count rendered the glyph alone rather than "Catchup: 0d", and both controls — an unknown dialect and a channel with no attributes — correctly showed no affordance.

Platforms

  • iOS / iPadOS
  • tvOS
  • macOS
  • visionOS — not verified: the visionOS runtime is not installed on the build machine, so the gate could not run. No changed file contains a visionOS-specific branch, and the other three platforms build clean.

Related

Closes #203
Field report: #213 (closed as duplicate) — its four real-world spellings are all covered.
Builds on #25 / #88 (original Xtream-only catch-up) and #114 / #115 (catch-up from the EPG).

Known limitation, not addressed here: EPGGuideView hard-codes a 12-hour reach-back, so a channel badged for several days is still only scrollable back 12 h in the Guide. That is a pre-existing window-size issue and is being tracked separately.

Catch-up was Xtream-only by construction: the capability gate tested
`directURL == nil` as a de-facto "is Xtream" check, and the Xtream
timeshift path was the only URL builder. M3U playlists that declare an
archive per channel never surfaced one, even though other players play
the same archive from the same playlist.

M3UParser now reads the catchup attributes it already collected and
discarded (`catchup`/`catchup-type`, `catchup-days` falling back to
`timeshift` then `tvg-rec`, and `catchup-source`), and also accepts
unquoted values, which real playlists emit. The scan stops at the
display-name comma so a channel called `Sky F1=HD` cannot yield a bogus
attribute — which also made the parser measurably faster than before,
since the old scan walked the whole line.

M3UCatchupURL builds the archive URL for all five dialects: flussonic
(filename rewrite, with an explicit catchup-source winning over the
convention), append, default/shift template expansion, and xc, which
recovers credentials from the channel URL path because an m3u Playlist
carries none. Timestamps are raw UTC epochs and durations seconds; the
Xtream builder's wall-clock-and-minutes convention exists to satisfy XC
panels and is reached only through the xc dialect. URLs are built from
the raw directURL, bypassing the stream-format rewrite that would swap
.m3u8 for .ts and 404 a Flussonic archive.

Buildability is decided once at import and persisted as `tvArchive`, so
a channel only badges when a URL can actually be produced and the UI
never re-derives it during scroll. The capability rule was previously
spelled three different ways across nine sites; it is now one shared
predicate, so the badge and the tap can no longer disagree.

A stored duration of 0 means "declared, depth unknown" and renders the
day-less badge using the existing localized string — no new strings are
added in this change.

Closes #203
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: catch-up is Xtream-only — M3U playlists never expose an archive

1 participant