Skip to content

Optional: prefetch lyrics for every track on the currently-playing album#193

Open
koto9x wants to merge 14 commits into
aviwad:mainfrom
koto9x:upstream-pr-c-album-prefetch
Open

Optional: prefetch lyrics for every track on the currently-playing album#193
koto9x wants to merge 14 commits into
aviwad:mainfrom
koto9x:upstream-pr-c-album-prefetch

Conversation

@koto9x

@koto9x koto9x commented Jun 1, 2026

Copy link
Copy Markdown

Summary

When Apple Music is active and the user enables a new opt-in setting, LyricFever silently pre-fetches lyrics for every other track on the currently-playing album. The next time you skip a track or the album auto-advances, lyrics load instantly — no spinner.

Default: OFF. Existing users see no change in behavior unless they explicitly enable the setting.

What's included

  • AppleMusicPrefetcher (actor) — warmAlbum(albumID:) enumerates album tracks via MusicKit catalog API, diffs against CoreData to avoid re-fetching already-cached tracks, and fetches lyrics in a bounded TaskGroup (cap = 4 concurrent requests). All failures are silent; demand-fetch on next play recovers gracefully.
  • SongObject schema v3 — adds optional albumID: String? via lightweight Core Data migration. Used to group cached entries by album for the diff check.
  • Settings toggle — "Prefetch album lyrics in background" in the menubar dropdown (visible only when Apple Music is the active player). Stored in UserDefaults via ObservableUserDefault; false by default.
  • ViewModel gating — the warmAlbum fire-and-forget task is guarded by userDefaultStorage.prefetchAlbumLyrics; the demand-fetch path also now writes song.albumID.

Dependencies

Depends on PR #191 (Apple Music lyric source via MusicKit) — this PR uses AppleMusicLyricProvider, AppleMusicAuthManager, appleMusicPlayer.lastObservedAlbumCatalogID, and the appleMusicID CoreData field introduced in that PR.

If you're reviewing/merging these in order: land PR-A first, then this one.

What's NOT included

Test plan

  • Default: play Apple Music tracks → no background network traffic (prefetch toggle off)
  • Enable "Prefetch album lyrics in background" → play first track of aespa LEMONADE → observe console: AppleMusicPrefetcher.warmAlbum: N new tracks for album <ID>
  • Skip to next track immediately → lyrics already cached → no spinner
  • Disable toggle → skip track → spinner (demand-fetch)
  • Imported MP3 (no albumID) → no warmAlbum call
  • Toggle hidden when Spotify is the active player

Note on schema versioning

PR-A adds schema v2 (appleMusicID). This PR adds schema v3 (albumID). If both are merged, Xcode's automatic lightweight migration handles the two-step upgrade transparently for existing users.

koto9x and others added 14 commits June 2, 2026 08:35
Required usage description string for MusicKit authorization on macOS.
TDD: 3 TTML fixtures (synced/plain/malformed) + failing tests first,
then XMLDocument-based parser with HH:MM:SS.mmm timecode parsing.
All 3 TTMLParserTests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ns [], span fixture)

- TTMLParserError now Equatable; malformed test asserts .invalidXML specifically
- Empty TTML (no <p>) returns [] instead of throwing .missingBody (caller-chain
  falls through naturally); removed unused .missingBody case
- New ttml-synced-spans.ttml fixture mirrors Apple's word-level <span> children
  with ttm:agent attributes; new test verifies stringValue assembles line text
  correctly with no XML leakage (no parser changes needed — XMLElement.stringValue
  concatenates child text nodes + Apple's inter-span whitespace)
- Renamed local startMS -> startTimeMS; documented ms-vs-seconds convention
- Expanded parseTimecode switch cases to multi-line
- XCTUnwrap in fixture helper (no more force-unwraps)
- Doc comment: "static block" -> "unsynced block"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lightweight CoreData migration. Adds optional appleMusicID (String?)
to SongObject so fetched lyrics can be keyed by Apple Music catalog ID
(Adam ID) in addition to the existing Spotify trackID. This enables
cross-player cache reuse and catalog-ID-based lookup for the AM provider.

New fields: appleMusicID (optional String)
- Pin koto9x/mediaremote-adapter to 6825821 (feat/expose-adam-ids branch)
  which adds contentItemIdentifier + albumiTunesStoreAdamIdentifier to
  TrackInfo.Payload, plus the missing CFSTR definitions that fix the linker.
- Add lastObservedCatalogID / lastObservedAlbumCatalogID vars to
  AppleMusicPlayer, populated from the MediaRemote payload in ViewModel's
  onTrackInfoReceived callback.
- Fix MediaController() call site: bundleIdentifier param was removed in
  adapter commit b8ce5d1 ("remove broken bundle ID filtering").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nd cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ext)

A sheet attached to a MenuBarExtra label view is a silent no-op on macOS —
the label has no NSWindow host. Route the auth trigger through an
openWindow(id:) call to a dedicated Window scene, matching the existing
onboarding/search/update window pattern.

The auth trigger + denied alert are pulled into an AppleMusicAuthModifier
to keep the MenuBarExtra label's modifier chain (and thereby the
@SceneBuilder body) inside SwiftUI's type-check budget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds optional albumID: String? to SongObject (lightweight migration).
Used by AppleMusicPrefetcher to group cached lyrics by album and
avoid re-fetching tracks already warmed for the current album.

Depends on: schema v2 (appleMusicID, from Apple Music PR)
On every Apple Music track change (when authorized + albumID known),
fire-and-forget warmAlbum enumerates album tracks via MusicKit catalog
API, diffs against CoreData, and prefetches lyrics for uncached tracks
with a bounded TaskGroup (cap=4). All failures are silent; demand-fetch
on next play recovers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gating

- UserDefaultStorage: add prefetchAlbumLyrics: Bool (@AppStorage, default false)
- MenubarWindowView: show 'Prefetch album lyrics in background' Toggle when
  Apple Music is the active player; hidden otherwise
- ViewModel: gate warmAlbum trigger on userDefaultStorage.prefetchAlbumLyrics;
  also write song.albumID when demand-fetching succeeds (mirrors prefetcher behavior)
- AppleMusicPrefetcher: drop sourceProvider/userPicked writes (those fields live
  in the separate sticky-picks PR; this PR's schema only adds albumID)
koto9x added a commit to koto9x/LyricFever that referenced this pull request Jul 4, 2026
Upstream PRs:
- A: koto9x:upstream-pr-a-applemusic → aviwad#191
  Title: Add Apple Music as a first-class lyric source via MusicKit
  Commits: TTMLParser, AppleMusicAuthManager, AppleMusicAuthView,
           schema v2 (appleMusicID only), catalog ID capture (Task 6),
           AppleMusicLyricProvider, ViewModel chain reorder,
           auth sheet trigger + Window scene fix

- B: koto9x:upstream-pr-b-sticky-picks → aviwad#192
  Title: Make SearchWindow manual picks sticky + add 'Reset lyrics' menu item
  Commits: schema v2 (userPicked+sourceProvider only),
           ViewModel sticky read path, SearchWindow apply,
           resetLyricsForCurrentTrack + menubar button

- C: koto9x:upstream-pr-c-album-prefetch → aviwad#193
  Title: Optional: prefetch lyrics for every track on the currently-playing album
  Commits: schema v3 (albumID), AppleMusicPrefetcher.warmAlbum,
           prefetchAlbumLyrics toggle (DEFAULT OFF), ViewModel gating

Adapter dependency: ejbills/mediaremote-adapter#4 (koto9x/mediaremote-adapter
feat/expose-adam-ids temporarily pinned in PR-A pbxproj until upstream merges).

Stays in fork (not upstreamed): Lyrics9x provider, 3-tier romanization+translation,
Plexamp player + smart routing, MediaRemote adapter callback fork-specific bits,
Tailscale ATS exception, warmQueueWindow, sandbox fix for Plexamp,
Kotopia Inc team/bundle ID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant