fix(iptv): download the Stalker channel list once per configuration change - #677
fix(iptv): download the Stalker channel list once per configuration change#677ReichiMD wants to merge 2 commits into
Conversation
…hange Three entry points ask for the Stalker channel list — the startup prefetch, the live TV snapshot load and the guide backfill load. Each opened its own portal session and pulled the full list; on a portal with ~21k channels that is 27.67 MB per entry point, measured at three downloads in 17 seconds. They now share one download through StalkerChannelListLoader: concurrent callers await the download that is already running, and a caller arriving within five minutes of a usable download reuses its result. Nothing is kept on disk — the playable URLs carry a play_token, so a list restored from disk would hand out expired ones. Two things decide when the shared list is dropped, and only these two: - The loader key, which is the configured portal set (id + URL + MAC). invalidateCache() deliberately no longer discards the download: it runs for every source change, and a playlist toggled or an EPG URL edited leaves the portals alone. Dropping the list there tore up a download another entry point was already running, so a single playlist toggle still cost two full 27.67 MB downloads. - A forced reload, which now means "not a list from before I asked" rather than "not the stored list". The repository takes that timestamp before it takes its load lock, so the second entry point reacting to one configuration change accepts the download that finished while it waited, while an explicit "Refresh IPTV" still reaches the portal. The download runs in its own scope: the startup prefetch gives up after 25 seconds, and its timeout must not cancel the download the live TV screen is waiting on. Tests cover the loader and, this time, its caller: the loader bug came back through invalidateCache() while the loader's own tests stayed green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MbtJc9Q9QK4SJAFveyp5m3
|
Thanks for this, reducing repeated downloads of such a large channel list is a useful improvement. The sharing approach looks good, but two caching issues still need fixing before merging:
The 13 loader tests passed locally, but an additional multi-portal recovery test exposed the first issue. Please add regression coverage for both partial failures and removing/disabling the last portal through the repository entry points. This is worth adding once those are covered. |
Review follow-up on ProdigyV21#677, both points from the same comment. Partial portal failures were cached as success. The freshness check asked whether the merged channel list was non-empty, and with portals A and B configured it was — A had filled it — so a load in which B failed counted as reusable and B was not asked again for up to five minutes, including after it recovered. The loader is now keyed per portal (id + URL + MAC) instead of per portal set, so every portal is downloaded, remembered and retried on its own and isReusable asks whether *this* portal answered: a session was opened and channels came back. Merging happens afterwards, in loadStalkerChannels, so a single portal's failure can no longer disappear into it. Per-portal caching rather than "remember nothing when any portal failed" because of what the second option costs the setup this is about: with one of two portals permanently down, dropping the combined result puts every entry point back to re-downloading the healthy portal's full list, which is the 3 x 29 MB per start this change exists to remove. Keyed per portal, the fix holds for the portals that work and a portal that failed still gets an immediate retry. The cleanup for "the last portal was removed" was unreachable. loadStalkerChannels(emptyList()) released the shared state correctly, but every caller returns before reaching it once no portal is enabled, so a removed portal's channel list and its session stayed in memory until the app closed. That branch is gone; the cleanup now runs in ensureCacheOwnership, which the snapshot load, the cache-only warmup and the cached-snapshot read all pass through before they can decide they have nothing to do. It releases only the portals that disappeared from the configuration, so an unrelated playlist change keeps the download it already has — dropping that is what used to cost a second full 27.67 MB list on every toggle. The same step prunes cachedStalkerApis. Regression coverage runs through the repository, not the loader alone: the loader's own tests were green while both of these were broken one level up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FDmLPyPaqS23awHuri7sX4
|
Thanks — both were real, and both are fixed on the same branch. 1. Partial portal failures I went with the first option you offered: the loader now caches per portal instead of per I preferred this over "remember nothing when any portal failed" because of what that costs the 2. Cleanup that is never reached You were right that Regression coverage, through the repository entry points New file
Counter-check, run on a throwaway state: with the per-portal Confirmed on device as well Both of your cases were also reproduced by hand on a real setup with two portals:
A packet capture of a two-portal cold start on the same build shows one
Full suite 952 passing, up from 948. detekt unchanged at 3276 findings, rule for rule. No created by Claude (Anthropic) on behalf of @ReichiMD |
The cause
The Stalker channel list is a single response — on the portal this was measured
against, ~29 MB for 21,295 channels. Three entry points ask for it: the startup
prefetch, the live TV snapshot load and the guide backfill load. Each opened its
own portal session and pulled the whole list, so one app start cost 3 × 29 MB
and three handshakes in 17 seconds.
Changing a source made it worse rather than better. Two entry points react to a
single configuration change, and each discarded the download the other had just
started — so one playlist toggle cost two complete lists.
What this changes
A new
StalkerChannelListLoader(~140 lines, memory only) lets all three entrypoints share one download:
its 25 s timeout does not cancel the download the live TV screen is waiting on;
stay dark for the whole window.
Exactly two things drop the shared list, and nothing else:
The loader key, which is the configured portal set (id, URL, MAC — hashed, so
those values never leave the function).
invalidateCache()deliberately nolonger drops the download: it runs on every source change, and a playlist toggled
or an EPG URL edited leaves the Stalker portals alone. Dropping it there is what
tore up the download the other entry point was already running.
A forced reload, which now means "nothing older than my request" instead of
"nothing remembered".
loadSnapshotreads that timestamp before it takes theload lock, so the second entry point reacting to the same configuration change
accepts the download that finished while it was queued — while
Settings → Refresh IPTV still reaches the portal.
The list is deliberately not persisted across app runs: its playable URLs
carry a
play_token, and a list restored from disk would hand out expired ones.M3U and Xtream sources never enter this path —
loadStalkerChannelsreturnsimmediately when no Stalker portal is enabled, and nothing else in
invalidateCache()changed.Result, measured on a real portal
Network capture, 80 seconds, one app start plus three source toggles, portal with
21,295 channels:
Live playback in the same capture stayed clean: no
create_linkcalls, everyportal response
200, the stream started over the usual302.Known limit, deliberately not part of this PR
A source change that has nothing to do with the Stalker portals — toggling an
M3U playlist, say — still costs one full channel list. This PR removes the
duplicate, not the reload itself: a configuration change asks for fresh data and
the loader honours that. Whether a Stalker list should be refreshed at all in
that case is a separate call, because the
play_tokeninside it ages, and otherportal clients reload on every start for exactly that reason. Happy to send a
follow-up if you want it.
Testing
testSideloadDebugUnitTest), 16 tests new,split across two levels on purpose:
StalkerChannelListLoaderTest(13) covers the loader itself — sharing, thefreshness window, a caller that gives up, a changed portal set, an empty
result, a failed download.
IptvRepositoryStalkerListInvalidationTest(3) covers its caller,because the first attempt at this fix passed every loader test and still
pulled the list twice on the device: the duplicate came from
invalidateCache()one level up.created by Claude (Anthropic) on behalf of @ReichiMD