Metadata/art/colors delayed — currentSong, artwork, widget colors, and lyrics only updated after finalizeCrossfade. Now a songHandoff callback fires at the crossfade midpoint (50%) — updates everything mid-fade so the UI transitions with the audio. Scrobble of the outgoing song also fires here (before currentSong changes).
Visualizer stale data — Old song’s vis frames stayed in the buffer during the first half of crossfade. Now visualizerHandoff zeros offlineVisBuffer + _audioLevels before loading the new song’s data. Clean slate → simulation fills the gap → real vis takes over.
The needsNextTrack callback (post-finalize) now only handles player swap, observer re-registration, and queue persistence — no redundant metadata/scrobble work.
PERFORMANCE AUDIT
- Removed 16 dead SubsonicClient methods (~117 lines)
- Added NSCache memory tier to LibraryCache, AlbumCoverStore,
ArtistCoverStore, RadioCoverStore
- Replaced weak polynomial hash with FNV-1a 64-bit in ImageCache
- Split PlaybackStateStore into save() (full queue) and savePosition()
(time only)
- Reused single SubsonicClient in OfflineManager instead of
per-download allocation
- Added periodic ImageCache disk trim every 50 writes
- Changed AudioPreFetcher to fuzzy offline match
(isSongAvailableOffline)
- Removed dead code: hasCompanionLibrary, downloadedSongIds, isActive,
CachedImageLoader.task
- Fixed thread safety: inline JSONEncoder/JSONDecoder in LibraryCache
(no shared instances)
WIDGET EXTENSION (new target: NavidromeWidget)
- v2 glassmorphism design: blurred album art background + frosted
glass panel
- Waveform scrubber: 40-bar Canvas with tap-to-seek (20 segments via
SeekToIntent)
- Color-adaptive theming: CIAreaAverage dominant color extraction with
HSB contrast adjustment
- Transport controls: previous/play-pause/next with interactive
AppIntents
- Up Next footer with crossfade countdown from Smart DJ profiles
- Large widget: 3-item queue list with numbered rows
- Small/Medium/Large sizes matching design mockups
- App Group communication via WidgetSharedState (UserDefaults)
- Darwin notification observer for widget→app commands
- Foreground command pickup for suspended app recovery
- Idempotency guards on all widget commands
CROSSFADE & PLAYBACK FIXES
- Fixed dual audio on single-song queue: guard nextSong.id ==
currentSong?.id in prepareNextForCrossfade
- Fixed crossfade play path never calling pushWidgetState (returned
before reaching it)
- Fixed crossfade needsNextTrack callback missing queue persistence +
widget push
- Fixed toggleShuffle queue not persisted after PlaybackStateStore
split
- Added nowPlayingSyncTimer restart on foreground (Lock Screen seek
bar drift)
- Added AVPlayer currentTime/duration sync in resumeVisTimers before
vis timer restart
(fixes waveform distortion after background — confirmed by Apple
Forums + SoundCloud engineering)
COVER ART PIPELINE
- Fixed pushWidgetState cover art size mismatch (300→600 to match
fetchAndSetArtwork)
- Added custom cover art key differentiation ("custom_" prefix forces
re-blur)
- Changed server art lookup from memoryOnlyImage to cachedImage
(memory+disk fallback)
- Added POST /library/cover-art-by-path endpoint (was missing — iOS
fallback hit 404)
- Added navidrome_id fallback on existing cover art endpoint
- Added embed_cover_art_in_file/embed_cover_art_in_directory: mutagen
writes cover art
directly into FLAC/MP3/M4A/OGG metadata tags so Navidrome serves
updated art
- All three upload paths (by-id, by-path, upload-tracks) now embed +
trigger_scan
COMPANION API FIXES
- Fixed _create_task recursion (was calling itself instead of
asyncio.create_task)
- Fixed navidrome_db NameError on /library/conflicts endpoint
- Reduced WebSocket connect/disconnect logging (only first-client and
all-disconnected)
SMART DJ PROFILE PREFETCH
- New endpoint: GET /smart-dj/profiles/export (bulk JSON, gzip
automatic)
- SmartDJCache.loadBulkCache() reads single file on launch (instant)
- SmartDJCache.bulkImport() writes all profiles in one atomic file
- CompanionAPIService.fetchAllProfiles() fetches entire profile set in
one request
- Wired into NavidromePlayerApp.task after server connect
- SmartCrossfadeManager unchanged — already reads from SmartDJCache
first
WEBSOCKET NOISE REDUCTION
- iOS: silent reconnect retries, only log milestones (#1, #5, every
20th)
- iOS: log "reconnected after N attempts" on success, silent initial
connect
- Python: only log first client connect and all-clients-disconnected
Files: 13 modified, 8 new (including companion-api/main.py)
Companion (main.py):
• NAVIDROME_TAGS whitelist — the single source of truth for what tags
survive
• enforce_tag_whitelist() — whitelist enforcer, replaces blacklist
approach
• All 5 write points updated: apply_tags, apply_tags_dict,
upload-track, upload-tracks, restructure_all
• preserve_composer and preserve_lyrics flags on both upload
endpoints (default False)
• /library/clean-tags now uses whitelist enforcer
iOS:
• UploadMetadata — preserveComposer and preserveLyrics fields
• buildMultipartBody — sends both flags as form fields
• BatchUploadView — two toggles, both off by default, wired end-to-end
• MultiAlbumEditorSheet — full rewrite matching
BatchAlbumEditorSheet: MusicBrainz search, swipe to exclude/include
tracks, Reset button, cover art widget with red glow
Active tab — all unignored issues, swipe left → “Ignore” (grey)
• Ignored tab — all ignored issues shown dimmed, swipe left →
“Restore” (pink) to bring them back
• Fix buttons hidden on ignored issues
• Ignored IDs persisted in @AppStorage so they survive app restarts
• Tab labels show live counts: Active (1) | Ignored (31)
Models.swift — CompanionSong, CompanionAlbum, CompanionArtist,
CompanionLibraryResponse. Each has a toSong()/toAlbum() converter.
Song.id = navidrome_id (streaming still works), Song.coverArt =
"companion:{id}" (routes cover art to Companion), Album.id =
"companion:{name}|{artist}" (detected by AlbumDetailView).
CompanionAPIService.swift — fetchAllSongs, fetchAlbumSongs,
fetchAllAlbums, fetchAllArtists, searchLibrary are all nonisolated
where needed. coverArtURL/artistPhotoURL are nonisolated so views can
call them synchronously. Push handler now fires
companionCoverArtUpdated and companionArtistPhotoUpdated events and
clears ImageCache immediately on receipt.
LibraryCache.swift — cacheCompanionAlbums/loadCompanionAlbums,
cacheCompanionAlbumSongs/loadCompanionAlbumSongs for local persistence
of companion data between launches.
SyncEngine.swift — syncCompanion() runs after every Navidrome sync
(bootstrap and delta) when Companion is enabled. It fetches albums
from /library/albums, converts them to Album objects, and overwrites
"all_albums" cache — so MyMusicView’s album grid immediately shows
Companion-sorted data. companionLibraryChanged notification triggers
reactive UI updates.
AsyncCoverArt.swift — Detects "companion:{id}" prefix and routes to
CompanionAPIService().coverArtURL(companionId:) instead of Navidrome.
All existing non-companion cover art paths are unchanged.
AlbumDetailView.swift — Detects albumId.hasPrefix("companion:"),
parses "companion:{name}|{artist}", calls
/library/songs?album=...&album_artist=... on Companion, and builds a
synthetic AlbumWithSongs locally. Caches results so the next tap is
instant. Falls back to the existing Navidrome path for all
non-companion album IDs.
Features:
- Dual-AVPlayer Smart DJ crossfade with LUFS normalization
- Mitsuha-style FFT visualizer (real-time + offline pre-computed)
- Companion API integration (Smart DJ, tag editing, vis frames)
- Offline-first SyncEngine with delta sync and album detail pre-caching
- Audio pre-fetcher for gapless queue playback
- Optimistic action queue (star/unstar with background retry)
- ShazamKit recognition with MusicKit preview playback
- Radio streaming with HLS/PLS/M3U support and buffer seek
- Watch app with Crown Sequencer and Ultra speaker support
- Batch metadata editing with album_artist fix for split albums
- Cache-first UI pattern across all views
- NWPathMonitor offline detection with reactive song greying