Skip to content

Offline-first lyrics and cover art for downloaded songs - #2778

Open
loucass wants to merge 4 commits into
maxrave-dev:devfrom
loucass:fix/offline-first-media
Open

loucass wants to merge 4 commits into
maxrave-dev:devfrom
loucass:fix/offline-first-media

Conversation

@loucass

@loucass loucass commented Oct 30, 2026 •

Copy link
Copy Markdown

Summary

Downloaded songs now behave truly offline-first: saved lyrics appear instantly, and downloaded songs keep their cover art on disk instead of relying on Coil's 24-hour cache.


The Problem

What I experienced: I download songs to listen offline. When opening a downloaded track with no internet, the lyrics and cover art took a long time to appear — sometimes they didn't show at all until a network request timed out. At first I thought the app simply wasn't downloading lyrics. After inspecting the code, I found the root cause.

Why it happened:

Symptom Root Cause
Lyrics delay / missing offline The app always started by fetching from the selected provider (SimpMusic, Genius, etc.). Only after that failed would it fall back to the DB. On a plane or subway, the request hangs until timeout before the fallback triggers.
Cover art missing offline Coil caches images for 24 hours, but that cache is shared and evictable. A downloaded song's cover is not pinned anywhere; clearing the thumbnail cache or memory pressure could evict it. There was no "this song is downloaded → its cover must survive" guarantee.

The Fix

1. Lyrics: Show saved first, refresh later

File: SharedViewModel.getLyricsFromFormat

  • Before provider fetch, call showSavedLyricsFirst(song, duration) which:
    • Reads lyricsCanvasRepository.getSavedLyrics(videoId) from the local DB
    • Renders immediately with LyricsProvider.OFFLINE (UI already displays "Offline" badge)
    • Also restores saved AI translations if enabled
  • Then continues to the selected provider in the same coroutine; when it returns, it replaces the saved copy.
  • The saved-first copy is not written back to the DB and not uploaded to the community lyrics database. A new saveToDatabase flag on updateLyrics gates both.
  • Provider fallback chain is untouched — if the provider fails, it still ends at the saved lyrics, so offline always works.

2. Cover Art: Pin covers for downloaded songs

New files:

  • ArtworkStore (common interface) + NoOpArtworkStore (Desktop)
  • AndroidArtworkStore (Android implementation, filesDir/artwork/)
  • ArtworkFiles.kt — filename logic ({videoId}.jpg for ytimg, SHA-256 of canonical URL otherwise)
  • LocalArtworkInterceptor.kt — Coil interceptor, registered before NetworkFirstInterceptor

Behaviour:

  • When a download transitions into PREPARING/DOWNLOADING, observeDownloadedArtwork() launches saveArtworkFor(videoId) off the collector (so conflated state flow doesn't drop transitions).
  • MainActivity.onStart calls ensureDownloadedArtwork() to backfill covers for downloads that predate the feature and to finish in-progress ones.
  • retainOnly(urls) keeps covers for both downloaded and downloading songs, then deletes orphaned files.
  • Removing a download calls deleteArtworkIfUnused(videoId, thumbnails) — builds a keep-set from all downloaded/downloading songs and only deletes if the file isn't shared (album art survives removing one track).
  • "Clear downloaded cache" calls retainOnly(emptyList()) → all pinned covers removed.
  • Non-downloaded songs never get a pinned file → they continue through NetworkFirstInterceptor with the existing 24-hour rule.
  • TurntableWidget now uses context.imageLoader (singleton) so pinned covers resolve there too.

Testing


Notes

  • Artwork pinning is Android-only; Desktop binds NoOpArtworkStore.
  • Pinned images re-encoded as JPEG 90; transparency lost (album art is normally opaque).
  • If the app is killed mid-save, the next foreground backfill retries.

Linked issue

Closes #2777
Closes #2602
Closes #2829

Checklist

  • This PR addresses an accepted issue (linked above)
  • I wrote or personally reviewed every line of this change
  • [] I tested it on Android and/or Desktop (say which, and how)
    i will download the debug apk and test it

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

Labels

None yet

Projects

None yet

1 participant