Skip to content

feat: optional pre-cache of upcoming queue tracks#866

Merged
eddyizm merged 4 commits into
eddyizm:developmentfrom
sinful1992:feat/queue-precache
Jul 17, 2026
Merged

feat: optional pre-cache of upcoming queue tracks#866
eddyizm merged 4 commits into
eddyizm:developmentfrom
sinful1992:feat/queue-precache

Conversation

@sinful1992

@sinful1992 sinful1992 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What

Split out of #861 as requested by @tvillega. This is the pre-cache feature half.

Adds an opt-in setting to fully cache the next N tracks of the play queue in the background, so skips start instantly and playback survives short signal drops (tunnels, elevators, spotty commutes).

This is the track-count-based prefetch users asked for in the #472 discussion (DSub2000 comparison), with the storage concern raised there addressed: pre-cached audio goes into the existing size-limited LRU streaming cache, not into downloads, so it can never grow storage unbounded — old entries are evicted automatically.

UI

Two new options in Settings, directly below the existing "Song preload buffer" option they complement:

  • Pre-cache upcoming tracks — list preference, default Off, choices 1–5 tracks.
  • Pre-cache on Wi-Fi only — switch, default On, pauses pre-caching on metered connections.

(The time-based preload buffer answers "how much of the current song to buffer"; this answers "how many of the next songs to have ready".)

How

  • QueuePreloader watches queue/timeline changes from BaseMediaService and writes upcoming tracks into the streaming cache via Media3's CacheWriter.
  • Uses the same cache keys the player reads with, so pre-cached bytes are found at playback time.
  • In-flight work is generation-tracked: queue changes or service teardown cancel superseded writes safely (no orphaned downloads).
  • Skips tracks already fully cached and does nothing while the setting is Off.

Notes

Testing

  • assembleDebug builds clean.
  • Ran on-device: with 3 tracks enabled, skipping to the next track starts instantly with no network; toggling airplane mode mid-queue keeps playing through the pre-cached tracks.

Summary by CodeRabbit

  • New Features
    • Added automatic pre-caching of upcoming (non-radio) HTTP/HTTPS tracks to improve playback smoothness.
    • Added settings to control how many tracks to pre-cache.
    • Added an option to enable pre-caching only on Wi‑Fi.
    • Pre-caching skips radio/“ir-” content and tracks already marked as downloaded.
  • Bug Fixes
    • Improved streaming cache consistency by aligning cache keying between playback and pre-caching.
    • Pre-caching now safely cancels and refreshes when playback state or network conditions change.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d0a41795-abee-43aa-b2b8-d09a551d4534

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds configurable pre-caching for upcoming queue tracks, stable streaming cache keys, streaming cache writer access, Wi-Fi-only controls, and media-service lifecycle hooks for starting and cancelling preload work.

Changes

Queue pre-caching

Layer / File(s) Summary
Streaming cache contract
app/src/main/java/com/cappielloantonio/tempo/util/StreamingCacheKeyFactory.kt, app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java
Streaming cache reads and writes use shared cache keys derived from explicit keys or stream URI parameters, with new APIs for accessing the streaming cache and writer factory.
Pre-caching settings
app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt, app/src/main/res/values/arrays.xml, app/src/main/res/values/strings.xml, app/src/main/res/xml/global_preferences.xml
Adds configurable track-count and Wi-Fi-only preferences with corresponding values and settings UI resources.
Queue preloader lifecycle
app/src/main/java/com/cappielloantonio/tempo/service/QueuePreloader.kt, app/src/main/java/com/cappielloantonio/tempo/service/BaseMediaService.kt
Collects eligible upcoming tracks, caches them on a background executor, removes incomplete resources, invalidates stale work, and runs or cancels preloading from media-service and network events.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • eddyizm/tempus#859 — The changes implement stable streaming cache keys and upcoming-queue pre-caching described by this issue.

Sequence Diagram(s)

sequenceDiagram
  participant BaseMediaService
  participant QueuePreloader
  participant Player
  participant DownloadUtil
  participant StreamingCache
  BaseMediaService->>QueuePreloader: preload current player timeline
  QueuePreloader->>Player: collect eligible upcoming HTTP tracks
  QueuePreloader->>DownloadUtil: create streaming cache writer
  DownloadUtil->>StreamingCache: cache track data
  BaseMediaService->>QueuePreloader: cancel during service teardown
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: optional pre-caching of upcoming queue tracks.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java (1)

129-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a more generic accessor name.

getStreamingCacheForPreload ties a general-purpose cache accessor to one specific caller's concept ("preload"). A neutral name (e.g. a public getStreamingCache) would be more reusable if other future consumers need direct Cache access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java` around
lines 129 - 145, The public cache accessor currently exposes preload-specific
naming. Rename getStreamingCacheForPreload to the neutral getStreamingCache,
update its callers accordingly, and preserve its existing synchronized behavior
and Cache return value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/cappielloantonio/tempo/service/QueuePreloader.kt`:
- Around line 88-90: Update the next-window calculation in the QueuePreloader
preload loop to pass the player’s current repeat mode, using player.repeatMode
instead of the hardcoded Player.REPEAT_MODE_OFF. Preserve the existing shuffle
behavior and termination checks so repeat-all wraps correctly and repeat-one
resolves to the current track.
- Around line 42-51: Update QueuePreloader.preload and the network capability
handling in BaseMediaService.onCapabilitiesChanged so an active preloader is
cancelled immediately when the connection transitions from Wi‑Fi to metered.
Track the prior network state, invoke QueuePreloader.cancel() on that
transition, and preserve the existing updateMediaItems behavior for other
capability changes.

---

Nitpick comments:
In `@app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java`:
- Around line 129-145: The public cache accessor currently exposes
preload-specific naming. Rename getStreamingCacheForPreload to the neutral
getStreamingCache, update its callers accordingly, and preserve its existing
synchronized behavior and Cache return value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 288d19ab-68cb-49a8-867b-a7b28ae4afdf

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe7074 and 3d1ec34.

📒 Files selected for processing (8)
  • app/src/main/java/com/cappielloantonio/tempo/service/BaseMediaService.kt
  • app/src/main/java/com/cappielloantonio/tempo/service/QueuePreloader.kt
  • app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java
  • app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt
  • app/src/main/java/com/cappielloantonio/tempo/util/StreamingCacheKeyFactory.kt
  • app/src/main/res/values/arrays.xml
  • app/src/main/res/values/strings.xml
  • app/src/main/res/xml/global_preferences.xml

sinful1992 added a commit to sinful1992/tempus that referenced this pull request Jul 12, 2026
Review findings from eddyizm#866: an in-flight precache kept downloading over
mobile data after Wi-Fi dropped, because only the start of a preload
checked the network. The service's network callback now re-invokes
preload() on Wi-Fi transitions, which cancels the active write on a
disallowed network and restarts it on an allowed one.

The upcoming-track walk also honours the player's repeat mode instead
of assuming repeat-off, so repeat-all precaches the wrap-around tracks.
@eddyizm
eddyizm changed the base branch from main to development July 12, 2026 14:36
@tvillega

tvillega commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

I am unable to replicate the feat, it seems to take into consideration the buffering of the current song.

I tried the following edge case:

  • Fresh install of the app + server credentials.
  • Configure pre-cache 1 song.
  • Enqueue 1 song of 45MiB
  • Enqueue 1 song of 57MiB
  • Go to the player and listen to 1 minute of the first song.
  • Set the phone on airplane mode.
  • Skip to the next track.

I checked the code and the WiFi check is only a short circuit, no further differentiation is made between networks. The streaming of heavy files is a common scenario, specially on WiFi, so the feat gets shadowed by the normal buffering of the app.

The queue list only has one icon on the right, perhaps adding a loading icon like DSub2000 could hint the user that the fetching is not completed or hint us about a blockage on the pre-fetch logic.

Update:

I mage the opposite edge case:

  • Fresh install of the app + server credentials.
  • Configure pre-cache 1 song.
  • Configure WiFi transcoding to opus 64kpbs
  • Enqueue 1 song of <20MiB
  • Enqueue 1 song of <20MiB
  • Go to player and listen to 1 minute of the first song
  • Set the phone on airplane mode.
  • Skip to the next track.

This failed as well, but the buffering of the current song was at 80%. I turned on the WiFi and skipped to the middle of the song until the song buffered completely, then turned it off again. The queue moved with no interruptions to the next song, which was pre-buffered on an 80%.

The pre-cache feat is being blocked by the current song buffer status, should it run on a thread instead to handle by its own how much songs ahead it can fetch?

@sinful1992

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough testing — you found a real bug, and your edge cases pinpointed it. The correlation with the current song's buffering turned out to be a side effect rather than the cause: the preloader already runs on its own background thread and never waits on the player. What was actually happening:

  • preload() cancelled and restarted the in-flight download on every player event (timeline changes, metadata refreshes, network callbacks), even when the upcoming tracks hadn't changed. For transcoded / unknown-length content, every one of those interruptions also wiped the partial cache entry, so progress kept resetting to zero. The precache only completed when it got one long uninterrupted window — which is exactly what waiting for the current song to finish buffering happened to provide. That's why forcing the current song to 100% "unblocked" it in your second test.
  • Your airplane-mode skip was doomed twice over: cutting the network made the writer throw, and the error handler deleted the partially cached next track at the exact moment the skip needed it. The skip itself then triggered another preload() whose cancel wiped the same track again. Only a track cached to 100% survived (a completed download stores its content length, which exempted it from the cleanup).

Fixed in 9793c30:

  • preload() is now a no-op while the in-flight task already targets the same upcoming tracks; it restarts only when the queue actually changes.
  • Fully cached tracks are skipped instead of re-downloaded.
  • The unknown-length partial cleanup moved from the failure path to the start of each write (a stale partial still can't be safely resumed into a fresh transcode). When the device goes offline mid-download, the partial is now kept — it's a valid prefix from a single run, so an offline skip plays what was cached instead of failing — and the player's existing cleanup policy removes it after use.
  • preload() also bails out when the streaming cache is disabled (size 0), where the player bypasses the cache and precaching was pure waste.

Your fresh-install airplane test should now behave as intended, with one caveat: if you cut the network before the next track finished caching, the skip plays the cached portion rather than the full song.

The per-row loading/progress icon like DSub2000 is a good idea for making the precache state visible — I'd rather keep it out of this PR's scope, but it would make a nice follow-up.

@tvillega

Copy link
Copy Markdown
Contributor

Will test this again under the same scenarios and I'll report back.

if you cut the network before the next track finished caching, the skip plays the cached portion rather than the full song.

Sounds like a reasonable default.

In the meantime, I'd like to bargain once more about

The per-row loading/progress icon like DSub2000 is a good idea for making the precache state visible — I'd rather keep it out of this PR's scope, but it would make a nice follow-up.

If I understand correctly the app is aware of two situations:

  • The next track is a cache hit (or not), check done to avoid re-downloading it.
  • There is WiFi, Mobile Data or no network (offline).

It could be possible to add a visual indicator on the player screen to inform that the next song is available on cache. Something as simple as toggling the visibility of a new element on the screen:

Example that adds a lightning icon.

This allows to have a visual feedback of the feat, and at the same time check the feat one skip at a time. If we skip beyond the pre-caching window the icon is hidden, because the app is aware if the next song is cached.

From the user side they would know the feature is active and working in a predictable manner. From the developer side, we could enhance that feature by using this feat as a starting point.

@SinTan1729

Copy link
Copy Markdown
Contributor

I also like the idea of an indicator, but I'd suggest that the indicator is added near/around the next button. Maybe an extra outline would do the job.

@tvillega

Copy link
Copy Markdown
Contributor

I checked the logs against logcat and the feat seems to be working as intended.

2026-07-14 02:52:57.352 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=0&format=raw&id=vzIgHD5jq8jVTi5xkqynGs
2026-07-14 02:52:58.916 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=0&format=raw&id=TXDJZb9J2jzAC8LVJ96iaD
2026-07-14 02:53:00.520 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=0&format=raw&id=nsx2fBwp8OAlUMVaUWTkz4
2026-07-14 02:53:02.330 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=0&format=raw&id=pn6uhQK0UrkXK4Q4T9oOUr
2026-07-14 02:53:35.244 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=0&format=raw&id=EQcLwwsEAdpF5BZbG5HTcH
2026-07-14 02:56:10.749 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=64&format=opus&id=Sr64lsBH04n6SHGSwdUndf
2026-07-14 02:56:41.821 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cached http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=64&format=opus&id=6WjrZTtEbc3YTRb6bB0LZb
2026-07-14 02:56:45.953 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cache aborted for http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=64&format=opus&id=s6ETpLVogNWY6yZ5M5yrw4: androidx.media3.datasource.HttpDataSource$HttpDataSourceException: java.net.SocketException: Software caused connection abort
2026-07-14 02:56:46.056 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cache aborted for http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=64&format=opus&id=agFqVnHNKGXAKiDZ2xsS6N: androidx.media3.datasource.HttpDataSource$HttpDataSourceException: java.net.ConnectException: Failed to connect to /10.0.0.1:4535
2026-07-14 02:56:46.103 21092-21359 QueuePreloader          com.eddyizm.degoogled.tempus.debug   D  Pre-cache aborted for http://10.0.0.1:4535/rest/stream?u=test&s=1b7da16b-be07-4063-9d37-aa7c4ff49444&t=bc25cb47daae2028d4f44b7738d4f82d&v=1.15.0&c=Tempus&maxBitRate=64&format=opus&id=ogdldR0KjuhsPHKzbyN2gI: androidx.media3.datasource.HttpDataSource$HttpDataSourceException: java.net.ConnectException: Failed to connect to /10.0.0.1:4535

At timestamp 02:56:45 I set the phone to airplane mode and it correctly triggered the subroutines to detect this, so the logic was indeed running and raised an exception which it logged and handled gracefully.

Said that, this feat will only work correctly on real-world scenarios where the connection is weak. This app has two network states: yes-wifi & no-wifi. If I force a change between them, the player will request for the raw song so it will become a cache miss and the feat won't get triggered.

Given that we can't reliably test this function, I would mark it on settings as experimental just like the hide bottom navigation bar one. It's not that the feat isn't working, it's just that other bugs shadow it.


I also like the idea of an indicator, but I'd suggest that the indicator is added near/around the next button. Maybe an extra outline would do the job.

I thought of it as well, but the impact on the UI would be really high as the bottom part of the player is too clean. I could be proven wrong if apps like Spotify or Apple Music have clever ways of showcasing something.

if (uris.isEmpty()) return

executor.execute {
for (uri in uris) {

@tvillega tvillega Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the array of uris is relative to the current song, the first element must correspond to the next song.

This could trigger an intent if the song is fully cached to handle further UI logic somewhere else. I'm not exactly sure if this runs async or not, the success of the intent would depend of that.

Edit: Actually, broadcasting the uri itself would allow to make the check outside.

@sinful1992

Copy link
Copy Markdown
Contributor Author

Experimental label added in 9d578a6 — the setting now reads "Pre-cache upcoming tracks [Experimental]", same convention as the hide-bottom-navbar toggle. Agreed on the reasoning: the feature behaved correctly in your logs, but the binary wifi/no-wifi handling can shadow it, so setting expectations is fair until that's sorted.

On the indicator — I'd still keep it a follow-up PR, but here's the direction I'd take when we build it, because I think you're each right about a different half of it.

@SinTan1729's instinct to put the signal near the action is right in principle: the state ("this skip will be instant") is about the next button. But decorating exo_next itself has two problems. An outline or ring on a transport control is the same visual language the player already uses for toggle state (shuffle, repeat), so it would read as "next is switched on" rather than "next is ready". And chrome that appears/disappears on a transport button mid-song tends to be perceived as a glitch, not information — plus, as @tvillega says, that row is the most contested real estate in the app.

@tvillega's standalone icon is the right shape, and I'd anchor it somewhere specific: the media-quality sector at the top of the controller (player_media_quality_sector, where the bitrate and format labels already live). That strip is already the player's "technical state" zone — a small icon appearing next to 320 kbps · flac reads naturally as stream status, stays out of the clean lower half, and can fade in/out there without pulling the eye the way it would near the controls.

Two details I'd consider non-negotiable whenever it lands:

  1. Don't reuse the download/pin glyph. Tempus has real persistent downloads; the pre-cache is ephemeral. The same icon would tell users the track is "saved" when it isn't. A bolt (or similar "instant" glyph) keeps the two concepts separate, and the setting's summary text can teach its meaning.
  2. Give it a contentDescription ("Next track ready for instant playback") so the state is also announced by TalkBack — an outline around a button can't carry that.

Stating the tradeoff openly: the quality strip is less discoverable than a ring around the next button. I think that's the correct trade — this is passive reassurance (and a handy testing aid while the feature is experimental), not something the user must notice to benefit from.

@tvillega

Copy link
Copy Markdown
Contributor

Fair enough. The feat is ready for merge.

@eddyizm

eddyizm commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Due to some other prs, this one had some merge conflicts that need a update.

Adds a setting to pre-cache the next N upcoming tracks of the play
queue into the existing streaming cache, so playback continues
smoothly through short connectivity drops. Off by default; optional
Wi-Fi-only restriction. Cached data lands in the size-limited LRU
streaming cache, not in downloads, so it cannot grow storage unbounded.

Preload work is cancelled and superseded safely across queue changes
and service teardown.
Review findings from eddyizm#866: an in-flight precache kept downloading over
mobile data after Wi-Fi dropped, because only the start of a preload
checked the network. The service's network callback now re-invokes
preload() on Wi-Fi transitions, which cancels the active write on a
disallowed network and restarts it on an allowed one.

The upcoming-track walk also honours the player's repeat mode instead
of assuming repeat-off, so repeat-all precaches the wrap-around tracks.
…e preloader

The precache could be defeated by its own lifecycle management, which
matched the reported symptom of 'never works until the current song is
fully buffered':

- preload() cancelled and restarted the in-flight CacheWriter on every
  player event (timeline updates, metadata refreshes, network callbacks),
  even when the upcoming-track target was unchanged. For unknown-length
  (transcoded / chunked) content every restart also wiped the partial
  cache, so progress kept resetting to zero and the precache only ever
  completed if it got one long uninterrupted window - e.g. after the
  current song finished buffering and the player went quiet.

- Losing connectivity made the writer throw, and the catch handler wiped
  the partially cached next track at exactly the moment an offline skip
  needed it. The skip itself made it worse: the media item transition
  triggered another preload(), whose cancel wiped the track being
  transitioned into. A track only survived if it had been cached to 100%
  (EOF stores the content length, which exempts it from the wipe).

Changes:
- preload() is now a no-op when the in-flight task already targets the
  same track list; restarts happen only when the upcoming queue actually
  changes.
- Fully cached tracks are skipped instead of re-written.
- The unknown-length partial cleanup moved from the failure path to the
  start of each write (a leftover partial still can't be resumed with a
  range request into a fresh transcode). On failure the partial is now
  kept when the device has no network - it is a valid single-run prefix
  that lets an imminent offline skip keep playing - and cleaned up as
  before while online.
- preload() bails out when the streaming cache is disabled (size 0), in
  which case the player bypasses the cache and precaching is pure waste.
@sinful1992
sinful1992 force-pushed the feat/queue-precache branch from 9d578a6 to 256734e Compare July 17, 2026 09:30
@sinful1992

Copy link
Copy Markdown
Contributor Author

Rebased onto current development — the conflict was the stale pre-#865 cache-key commit this branch was stacked on (now squash-merged as abadca4, so it's dropped from the branch). No code changes; the PR is conflict-free again.

@eddyizm
eddyizm merged commit 720ac35 into eddyizm:development Jul 17, 2026
1 check passed
skajmer added a commit to skajmer/tempus-polish-translation that referenced this pull request Jul 17, 2026
skajmer added a commit to skajmer/tempus-polish-translation that referenced this pull request Jul 17, 2026
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.

4 participants