Commit graph

38 commits

Author SHA1 Message Date
3824f42ab5 quick fix 2026-04-12 20:18:29 -07:00
0a1a46df47 Widget glassmorphism redesign 2026-04-12 19:57:20 -07:00
Dallas Groot
b8f3544395 quick fix 2026-04-12 19:34:58 -07:00
Dallas Groot
b9844b23cd Performance audit, Now Playing widget, crossfade stability, cover art embedding, DJ profile bulk cache
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)
2026-04-12 19:24:22 -07:00
Dallas Groot
098e9b9363 added widget support 2026-04-12 12:57:42 -07:00
Dallas Groot
fc69d8a3cf CPU: Remove @Published from AudioPlayer time properties
Replace @Published var currentTime/duration with plain vars and drive
progress bars via TimelineView(.periodic) instead of SwiftUI
observation. This stops objectWillChange from firing 20x/second on
AudioPlayer, eliminating continuous body re-evaluation on
NowPlayingView, MiniPlayerBar, and MyMusicView regardless of
visualizer state.
2026-04-11 16:44:56 -07:00
Dallas Groot
85c85c2090 memory improvements 2026-04-11 15:37:14 -07:00
Dallas Groot
0730fa11f8 bug fixes 2026-04-11 15:09:06 -07:00
Dallas Groot
f3b9483b23 overhaul
AUDIT-036 — Slider/button fixes (direct Liquid Glass cause)
scheduleFlush() now runs Task { @MainActor } instead of bare Task. The
pendingSaves dictionary is now only ever read/written on the main
thread. Before this fix, a UserDefaults write could race with a slider
didSet, causing values to snap back or write the wrong value — which
is exactly why buttons were switching state unexpectedly.
AUDIT-034 — 60fps idle Canvas (direct Liquid Glass cause)
TimelineView now uses isRenderingActive ? settings.effectiveFPS : 2.0.
When paused or not visible, the Canvas drops from 60fps to 2fps. This
stops the continuous GPU wakeups that were fighting Liquid Glass
gesture tracking, which is why sliders needed multiple attempts.
AUDIT-001 — FFT real-time heap allocation
processFFT no longer allocates any heap memory. The Hann window is
computed once in init(). All four scratch buffers (fftWindow,
fftWindowed, fftRealp/fftImagp, fftMagnitudes) are pre-allocated and
reused every render callback — zero allocations on the real-time audio
thread.
AUDIT-002 — WatchOfflineStore data race
taskToSongId and pendingSongs now protected by a dedicated serial
storeQueue. URLSession delegate reads and main thread writes are
serialised.
AUDIT-019 — URLSession per AsyncCoverArt render
CompanionAPIService() no longer instantiated per render. Companion
cover art URLs now built directly from
CompanionSettings.shared.baseURL — no URLSession created.
AUDIT-020 — Synchronous disk read on main thread
CachedImageLoader now uses memoryOnlyImage (sync, no I/O) for the
first check, then cachedImageAsync (disk read on ioQueue) for the
second. Main thread never blocks on disk I/O.
AUDIT-033 — Lost star/unstar actions offline
Star/unstar now routes through OptimisticActionQueue — actions survive
Tailscale reconnection and are retried automatically.
AUDIT-035 — OptimisticActionQueue flush race
flush() Task is now @MainActor — pendingActions only ever touched on
main thread, no more race between rapid taps and in-flight flushes.
AUDIT-038 — O(n²) deduplication
deduplicateAlbums now O(n) using a frequency dictionary. For 843
albums: ~7.1M string comparisons/second during playback → ~1,700.
AUDIT-026, AUDIT-015 — Duplicate setResourceValue removed, cacheSize
now uses totalSize directly
2026-04-11 11:17:40 -07:00
Dallas Groot
92a5a54b8d quick fix 2026-04-11 08:36:32 -07:00
Dallas Groot
3ea57fa99b quick fix 2026-04-11 02:23:03 -07:00
Dallas Groot
b4d2a5ce92 Merge ; commit '07d125da3c29035a8bca6a992229655d2ff446a0' 2026-04-11 02:22:29 -07:00
Dallas Groot
a9acd65001 quick fix 2026-04-11 02:20:54 -07:00
Dallas Groot
07d125da3c Ignored IDs persisted in UserDefaults
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)
2026-04-11 02:13:06 -07:00
Dallas Groot
d3434f1911 quick fix 2026-04-11 01:59:36 -07:00
Dallas Groot
12dab4cc37 quick fix 2026-04-11 01:52:16 -07:00
Dallas Groot
db9d79f023 safeguards to id3 tagging and mismatch info 2026-04-11 01:36:13 -07:00
Dallas Groot
ffcddc86e2 quick fix 2026-04-11 01:14:38 -07:00
Dallas Groot
15ed38e13b Musicbrainz api add on
The MusicBrainz feature adds:
	•	Magnifying glass in the toolbar — searches by current title + artist
	•	A picker sheet showing up to 10 results with album, year, country,
label, track number
	•	Blue suggestion pills appear below each field that differs from the
MB match
	•	Tap a pill to accept it — auto-checks the field for saving
	•	Green magnifying glass once a match is selected, tap again to change
2026-04-11 01:08:24 -07:00
Dallas Groot
d37dc8fb44 quick fixes 2026-04-11 00:59:53 -07:00
Dallas Groot
f761f65e87 edit-metadata endpoint
iOS — 6 fixes across 5 files:
Models.swift — Song, Album, AlbumWithSongs all now have albumArtist:
String?. CompanionSong.toSong() passes albumArtist.
CompanionAlbum.toAlbum() passes albumArtist to the new field.
TrackEditorView.swift — Album Artist field now initialises from
song.albumArtist ?? song.artist instead of just song.artist.
fetchCompanionDetails no longer requires companion: prefix — it
fetches for all songs using the album name, decodes properly using
CompanionLibraryResponse, and matches by relative_path.
BatchAlbumEditorSheet.swift — initialises from album.albumArtist ??
album.artist.
MultiAlbumEditorSheet.swift — pre-fills from first.albumArtist ??
first.artist.
AlbumDetailView.swift — buildAlbumWithSongs now passes albumArtist
from the companion songs.
Companion — 2 fixes:
apply_tags — now does full FLAC cleanup (removes all Picard legacy
variants) before writing, same as apply_tags_dict already did.
edit-metadata endpoint — no longer calls restructure_file. File
renaming only happens via /bulk-fix. This was the root cause of the
500 errors on compilation tracks with disc numbers.
2026-04-11 00:50:37 -07:00
Dallas Groot
55820fdb38 my music view fix 2026-04-10 20:44:23 -07:00
Dallas Groot
a4103c8250 bug fixes and ready to upload to testflight 2026-04-10 19:05:45 -07:00
Dallas Groot
2bdac607b4 bug fixes
Songs Tab (SearchView.swift)
Default state now loads all songs alphabetically from the library via
getAlbumList2 → per-album song fetch, cached under "all_songs_sorted"
so subsequent opens are instant. The Download All banner shows song
count + already-downloaded count and queues only non-downloaded songs.
Every row uses .contextMenu (the long-press menu) with Play Now, Play
Next, Add to Queue, Download/Remove, Send to Watch, and Add to
Playlist — same pattern as Favourites. Watch and download badges
appear on each row. Searching ≥2 chars runs the server search and
shows artists/albums/songs in sections, then clears back to the full
list when the field is empty.
Keyboard Done Button
A single keyboardDoneButton() View extension in AsyncCoverArt.swift
calls UIApplication.shared.sendAction(resignFirstResponder:...)
globally — no @FocusState needed. Applied to: LoginView (all 4
fields), CompanionSettingsView (host/port), TrackEditorView
(checkField helper covers all tag fields), BatchAlbumEditorSheet
(editField helper), RadioView (name/URL), PlaylistsView (name fields),
MyMusicView (search), SearchView (via @FocusState + toolbar directly).
ShazamKit MTAudioProcessingTap
Primary path: MTAudioProcessingTap installed on AVPlayerItem.audioMix
— works for HLS, radio, and any AVPlayer stream without touching the
microphone. The prepare callback captures the source format and builds
an AVAudioConverter to 16kHz mono. The C-style shazamTapProcess free
function (required by the API) calls
MTAudioProcessingTapGetSourceAudio then dispatches to a serial
analysisQueue — the render thread is never blocked. convertAndMatch
wraps the raw AudioBufferList in an AVAudioPCMBuffer, converts it, and
feeds SHSession.matchStreamingBuffer. Fallback to microphone
(AVAudioEngine) is kept for the local engine path where no
AVPlayerItem exists. NSMicrophoneUsageDescription is only needed if
the mic fallback is ever hit.
2026-04-10 16:55:09 -07:00
Dallas Groot
99e4f3bd3a updates to companion from ios to server
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.
2026-04-10 11:14:41 -07:00
Dallas Groot
7b94844e38 renamed label
•	Smart DJ & Visualizer row label → Crossfade & Visualizer Analyzer
(and removed the misplaced style value from that row)
	•	Visualizer Appearance row now shows the active style (e.g. “Wave”)
as the trailing label, which is where it belongs
	•	Navigation title inside the subview updated to match
2026-04-10 07:41:31 -07:00
Dallas Groot
989412fda5 improvong the settings tab 2026-04-05 08:23:07 -07:00
Dallas Groot
98bd66a9e5 radio fixes 2026-04-04 18:25:41 -07:00
Dallas Groot
7b123b02f4 Auto-commit before update (2026-04-04 18:25) 2026-04-04 18:25:31 -07:00
Dallas Groot
b4e64b545f radio view fix 2026-04-04 18:06:08 -07:00
Dallas Groot
7094ffab16 Update from NavidromePlayer.zip (2026-04-04 16:12) 2026-04-04 16:12:28 -07:00
Dallas Groot
c3bce4a997 Update from NavidromePlayer.zip (2026-04-04 06:58) 2026-04-04 06:58:58 -07:00
Dallas Groot
16097f5ff2 Watch overhaul other changes 2026-04-03 19:20:55 -07:00
Dallas Groot
e0b2575e45 Auto-commit before update (2026-04-03 19:20) 2026-04-03 19:20:29 -07:00
Dallas Groot
f39eaff281 3-Dot Menu Grid Layout — Replaced List-based sheet with a grid of button pairs: Instant Mix | Add to Playlist, Go to Album | Go to Artist, Download/Remove | Send to Watch, Get Info | Edit Tags. 2026-04-03 15:48:37 -07:00
Dallas Groot
34f9cb4232 Auto-commit before update (2026-04-03 15:47) 2026-04-03 15:47:48 -07:00
Dallas Groot
b1b7d4b695 Favourites tab, artist cover change, ArtistCoverStore 2026-03-31 11:35:42 -07:00
Dallas Groot
d8041c0019 NavidromePlayer: iOS + watchOS Navidrome/Subsonic music player
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
2026-03-28 20:49:47 +00:00