🔴 SmartDJCache concurrent Dictionary crash (April 27+28 crash logs): - EXC_BAD_ACCESS + doesNotRecognizeSelector in bulkImport() - memoryCache dictionary mutated from main thread (loadBulkCache) and background Task.detached (bulkImport) simultaneously - Fix: NSLock serializes all memoryCache reads and writes 🔴 stopAVPlayer removeTimeObserver crash (April 30 crash log): - SIGABRT in -[AVPlayer removeTimeObserver:] from Previous button - timeObserver registered on old player, self.player swapped to crossfade's active player by finalizeCrossfade - Fix: remove observer from OLD player at both swap sites before assignment + reorder stopAVPlayer (observer before replaceCurrentItem) 🟡 Audit fixes (no crash logs, preventative): - KVO in radioSeekBack wrapped in DispatchQueue.main.async - Stale vis Task guarded by songId in all MainActor.run blocks - CHANGELOG.md with full findings documentation
2.5 KiB
2.5 KiB
Companion API
Python FastAPI server running on Raspberry Pi 5 inside Docker. Provides metadata editing, Smart DJ analysis, visualizer precomputation, lyrics, and library management.
Deployment
# On the Pi, from ~/docker/navidrome/
docker compose build music-companion && docker compose up -d music-companion
- Port: 8000
- Music dir inside container:
/music(mapped from/home/pi/navidrome/music) - Navidrome DB:
/navidrome_data/navidrome.db(read-only) - Companion data:
/app/data/(smart_dj.db, vis_cache, cover_art, tag backups)
Architecture
main.pyis the entire server (~3800 lines, single file)Dockerfile+docker-compose.ymlfor containerizationdiagnose.py— standalone diagnostic scriptpre_analyze.py— bulk pre-analysis for Smart DJ profiles
Critical Rules
Tag Safety
- ALL tag writes go through
apply_tags()(single track) orapply_tags_dict()(batch) - Both functions call
backup_tags()FIRST and abort withRuntimeErrorif backup fails - Files are NEVER modified without a backup safety net
enforce_tag_whitelist()has separate paths:- FLAC/OGG/Opus: checks against
NAVIDROME_TAGS(Vorbis Comment names) - MP3: checks against
ID3_FRAME_WHITELIST(ID3v2 frame IDs like TPE1, TALB) - AIFF: uses
mutagen.aiff.AIFFwith raw ID3 frames (easy=True returns None for AIFF)
- FLAC/OGG/Opus: checks against
- NEVER use
audio.delete()in restore — it destroys APIC (album art) and USLT (lyrics)
Backup System
- Dual-key backups:
_backup_key(full_path)+_backup_key_rel(relative_path) _find_backup()tries 4 strategies (current path, current rel, original path, original rel)- Survives file moves from
/bulk-fixrestructure save_batch_manifest()stores: batch_id, paths, tags_changed, affected_albums/artists, edit_type, is_reverted- Single-track edits (
PATCH /edit-metadata) also create manifests withedit_type: "single" - Double undo prevented:
is_revertedflag checked, HTTP 409 on repeat
Path Resolution
resolve_path()has 5 fallback strategies for finding files- Handles URL encoding, NFC unicode normalization, library folder prefixes
- Falls back to Companion songs table and fuzzy title matching
Common Pitfalls
MutagenFile(path, easy=True)returnsNonefor AIFF — always checkdatetime.utcnow().isoformat()produces timestamps WITHOUT timezone suffix — Swift must parse with fallback formatsffmpegsubprocess can hang on corrupt files — 120s timeout with process kill- Pi has limited CPU (4 cores) —
deploy.resources.limits.cpus: '2.0'in docker-compose