Skip to content

Add Apple Music as a first-class lyric source via MusicKit#191

Open
koto9x wants to merge 11 commits into
aviwad:mainfrom
koto9x:upstream-pr-a-applemusic
Open

Add Apple Music as a first-class lyric source via MusicKit#191
koto9x wants to merge 11 commits into
aviwad:mainfrom
koto9x:upstream-pr-a-applemusic

Conversation

@koto9x

@koto9x koto9x commented Jun 1, 2026

Copy link
Copy Markdown

Summary

Adds MusicKit-backed lyric fetching as the top-priority source when Music.app is the active player. For tracks in Apple Music's catalog, this returns Apple's official synced lyrics, which is dramatically more accurate than third-party sources for recent releases.

The motivating bug: aespa's LEMONADE album (released 2026-05) showed wrong-song or out-of-sync lyrics through the existing Spotify/LRClib/NetEase chain because none of those services had indexed the album yet — Apple Music had the correct TTML lyrics the entire time.

What's included

  • AppleMusicLyricProviderMusicDataRequest-backed; parses Apple's TTML lyric envelope into [LyricLine]
  • AppleMusicAuthManager — wraps MusicAuthorization; @Observable status accessible app-wide via .shared
  • AppleMusicAuthView — first-run consent sheet (routed through a Window scene, not .sheet — the latter is a silent no-op on MenuBarExtra label views on macOS)
  • TTMLParser — pure Swift TTML → [LyricLine] with XCTest fixtures (synced, plain/unsynced, malformed, Apple-realistic span-children)
  • ViewModel chain reorderallNetworkLyricProviders puts AM at index 0 when the AM player is active and a catalog (Adam) ID is known and auth is granted
  • SongObject schema v2 — adds optional appleMusicID: String? via lightweight Core Data migration; enables cross-player cache reuse when the same track was previously fetched under a different Spotify/Plexamp ID
  • Info.plistNSAppleMusicUsageDescription string
  • Auth UX — one-time prompt sheet on first AM track change; one-time "access denied" toast that opens System Settings

What's NOT included (intentional, follow-up PRs)

  • Sticky user-picked override / "Reset lyrics for this track" button (separate PR)
  • Album lyric prefetching (separate PR)
  • Karaoke-style word-level highlighting (out of scope for this PR)
  • Replacement of the existing chain — AM is purely additive (falls through to Spotify/LRClib/NetEase when AM returns nothing)

Dependency

Requires ejbills/mediaremote-adapter#4 to expose contentItemIdentifier + albumiTunesStoreAdamIdentifier in TrackInfo.Payload.

If that PR hasn't merged yet, the pbxproj in this PR temporarily pins koto9x/mediaremote-adapter@feat/expose-adam-ids (commit 6825821) which already has the new fields. Once ejbills/mediaremote-adapter#4 merges, a one-line revert of the repositoryURL in the pbxproj switches back to the canonical package.

Setup (one-time)

Enable MusicKit on the App ID in Apple Developer portal → Identifiers → your bundle ID → check MusicKit → Save. No code change needed beyond what's in this PR.

Test plan

  • Unit tests: TTMLParserTests — synced timestamps, plain (zero timestamps), malformed throws, Apple span-children fixture
  • Manual smoke: aespa LEMONADE — correct AM synced lyrics, timing matches Music.app's built-in display
  • Manual smoke: imported MP3 (no catalog ID) — chain falls to Spotify, no AM call fires
  • Manual smoke: revoke AM access in System Settings → Privacy → Media & Apple Music → graceful fallback + one-time denied toast
  • Schema migration test — SongObjectMigrationTests (existing CoreData store migrates cleanly)

koto9x and others added 11 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>
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