NavidromeApp/Widget/NowPlayingWidget.swift

185 lines
6.3 KiB
Swift
Raw Normal View History

2026-04-12 12:57:42 -07:00
import WidgetKit
import SwiftUI
//
// NowPlayingWidget.swift
// TARGET: Widget extension only.
//
// Defines the widget bundle, timeline provider, and timeline entry.
// The provider reads from WidgetSharedState (App Group UserDefaults)
// and generates entries with projected playback positions so the
// progress bar advances even between reloads.
//
// MARK: - Widget Bundle
@main
struct NavidromeWidgetBundle: WidgetBundle {
var body: some Widget {
NowPlayingWidget()
}
}
// MARK: - Timeline Entry
struct NowPlayingEntry: TimelineEntry {
let date: Date
// Song metadata
let songTitle: String
let artist: String
let album: String
// Playback state
let isPlaying: Bool
let currentTime: TimeInterval
let duration: TimeInterval
// Images (raw JPEG data decoded in the view)
let coverArtData: Data?
let blurredArtData: Data?
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
// Up Next queue
2026-04-12 12:57:42 -07:00
let queueNext: [WidgetQueueItem]
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
// v2: waveform, adaptive colors, crossfade
let waveformSamples: [Float] // 40 peaks 0.01.0
let accentColorHex: String? // dominant color e.g. "E74C3C"
let secondaryColorHex: String? // lighter variant for text
let crossfadeAt: TimeInterval // 0 = no crossfade data
2026-04-12 12:57:42 -07:00
// Whether any data has ever been written
let hasData: Bool
// MARK: - Computed
var progress: Double {
guard duration > 0 else { return 0 }
return min(max(currentTime / duration, 0), 1)
}
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
var currentTimeFormatted: String { formatTime(currentTime) }
2026-04-12 12:57:42 -07:00
var remainingTimeFormatted: String {
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
"-\(formatTime(max(duration - currentTime, 0)))"
}
/// Seconds until crossfade triggers. Nil if no crossfade data or already past.
var crossfadeCountdown: TimeInterval? {
guard crossfadeAt > 0, crossfadeAt > currentTime else { return nil }
let remaining = crossfadeAt - currentTime
return remaining < 120 ? remaining : nil // only show if < 2 min
}
var crossfadeCountdownFormatted: String? {
guard let cd = crossfadeCountdown else { return nil }
return "in \(formatTime(cd))"
2026-04-12 12:57:42 -07:00
}
private func formatTime(_ t: TimeInterval) -> String {
let total = Int(max(t, 0))
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
return String(format: "%d:%02d", total / 60, total % 60)
2026-04-12 12:57:42 -07:00
}
// MARK: - Placeholder
static let placeholder = NowPlayingEntry(
date: .now,
songTitle: "Not Playing",
artist: "NavidromePlayer",
album: "",
isPlaying: false,
currentTime: 0,
duration: 1,
coverArtData: nil,
blurredArtData: nil,
queueNext: [],
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
waveformSamples: [],
accentColorHex: nil,
secondaryColorHex: nil,
crossfadeAt: 0,
2026-04-12 12:57:42 -07:00
hasData: false
)
}
// MARK: - Timeline Provider
struct NowPlayingProvider: TimelineProvider {
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
func placeholder(in context: Context) -> NowPlayingEntry { .placeholder }
2026-04-12 12:57:42 -07:00
func getSnapshot(in context: Context, completion: @escaping (NowPlayingEntry) -> Void) {
completion(buildEntry(at: .now))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<NowPlayingEntry>) -> Void) {
let state = WidgetSharedState.shared
let now = Date()
if state.isPlaying && state.duration > 0 {
var entries: [NowPlayingEntry] = []
let elapsed = now.timeIntervalSince(state.lastUpdatedDate)
let baseTime = state.currentTime + elapsed
for i in 0..<10 {
let offset = Double(i) * 30.0
let entryDate = now.addingTimeInterval(offset)
let projectedTime = min(baseTime + offset, state.duration)
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
entries.append(makeEntry(at: entryDate, projectedTime: projectedTime, state: state))
2026-04-12 12:57:42 -07:00
}
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
completion(Timeline(entries: entries, policy: .after(now.addingTimeInterval(300))))
2026-04-12 12:57:42 -07:00
} else {
let entry = buildEntry(at: now)
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
completion(Timeline(entries: [entry], policy: .after(now.addingTimeInterval(15 * 60))))
2026-04-12 12:57:42 -07:00
}
}
private func buildEntry(at date: Date) -> NowPlayingEntry {
let state = WidgetSharedState.shared
var projectedTime = state.currentTime
if state.isPlaying && state.duration > 0 {
let elapsed = date.timeIntervalSince(state.lastUpdatedDate)
projectedTime = min(state.currentTime + elapsed, state.duration)
}
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
return makeEntry(at: date, projectedTime: projectedTime, state: state)
}
2026-04-12 12:57:42 -07:00
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
private func makeEntry(at date: Date, projectedTime: TimeInterval, state: WidgetSharedState) -> NowPlayingEntry {
NowPlayingEntry(
2026-04-12 12:57:42 -07:00
date: date,
songTitle: state.songTitle.isEmpty ? "Not Playing" : state.songTitle,
artist: state.artist.isEmpty ? "NavidromePlayer" : state.artist,
album: state.album,
isPlaying: state.isPlaying,
currentTime: projectedTime,
duration: state.duration,
coverArtData: state.coverArtData,
blurredArtData: state.blurredArtData,
queueNext: state.queueNext,
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
waveformSamples: state.waveformSamples,
accentColorHex: state.accentColorHex,
secondaryColorHex: state.secondaryColorHex,
crossfadeAt: state.crossfadeAt,
2026-04-12 12:57:42 -07:00
hasData: state.hasData
)
}
}
// MARK: - Widget Definition
struct NowPlayingWidget: Widget {
let kind = "NowPlayingWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: NowPlayingProvider()) { entry in
NowPlayingWidgetView(entry: entry)
.containerBackground(.clear, for: .widget)
}
.configurationDisplayName("Now Playing")
.description("Control playback and see what's playing.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled()
2026-04-12 12:57:42 -07:00
}
}