INSERT OR REPLACE on existing tracks was deleting the entire row and
reinserting only 9 columns, nuking spotify_track_id, deezer_id, isrc,
bpm, musicbrainz IDs, and ~15 other enrichment columns every scan.
Now uses UPDATE for existing tracks (preserves all enrichment) and
INSERT only for new tracks. Also ensures file_path gets updated from
the media server on each scan, fixing stale paths for users whose
files were moved/reorganized.
- New sync_history DB table tracks last 100 syncs with full cached context
- Records history for all sync types: Spotify, Tidal, Deezer, YouTube,
Beatport, ListenBrainz, Mirrored playlists, and Download Missing flows
- Sync History button on sync page with modal showing entries, source
filter tabs, stats badges, and pagination
- Re-sync button: server syncs expand card inline with live progress bar,
matched/failed counts, and cancel button; download syncs open download modal
- Re-syncs update the original entry (moves to top) instead of creating duplicates
- Delete button (x) on each entry with smooth remove animation
- Fix mirrored playlist source detection (youtube_mirrored_ matched youtube_)
- Fix broken server import thumbnails with URL validation
- Switch user automations to 2-column grid layout (matches system automations)
- Add duplicate button on non-system cards with POST /api/automations/<id>/duplicate
- Add search/filter bar (text search + trigger/action dropdowns) shown at 6+ automations
- Add Inspiration section with 8 starter templates that pre-fill the builder
- Add folder-style automation grouping with group_name DB column, dropdown
popover for assignment, collapsible group sections, and builder group input
- personalized_playlists._get_active_source() now returns 'deezer' when
configured instead of always falling back to 'itunes'
- Add deezer_track_id to _build_track_dict() for discovery pool tracks
- Include album_deezer_id and artist_deezer_id in get_discovery_recent_albums()
response — fixes "No deezer album ID available" error when clicking cards
- Skip Spotify library section entirely when Spotify is not authenticated
Both NOT NULL and profile v2 migrations now include album_deezer_id and
artist_deezer_id columns, use safe shared-column data copy instead of
SELECT *, and add album_deezer_id to UNIQUE constraints.
Users can now choose between iTunes/Apple Music and Deezer as their free
metadata source in Settings. Spotify always takes priority when authenticated;
the fallback handles all lookups when it's not.
Core changes:
- DeezerClient: full metadata interface (search, albums, artists, tracks)
matching iTunesClient's API surface with identical dataclass return types
- SpotifyClient: configurable _fallback property switches between iTunes/Deezer
based on live config reads (no restart needed)
- MetadataService, web_server, watchlist_scanner, api/search, repair_worker,
seasonal_discovery, personalized_playlists: all direct iTunesClient imports
replaced with fallback-aware helpers
Database:
- deezer_artist_id on watchlist_artists and similar_artists tables
- deezer_track_id/album_id/artist_id on discovery_pool and discovery_cache
- Full CRUD for Deezer IDs: add, read, update, backfill, metadata enrichment
- Watchlist duplicate detection by artist name prevents re-adding across sources
- SimilarArtist dataclass and all query/insert methods handle Deezer columns
Bug fixes found during review:
- Similar artist backfill was writing Deezer IDs into iTunes columns
- Discover hero was storing resolved Deezer IDs in wrong column
- Status cache not invalidating on settings save (source name lag)
- Watchlist add allowing duplicates when switching metadata sources
New library_history table logs every completed download and every new
track imported from Plex/Jellyfin/Navidrome. A "History" button next
to "Recent Activity" on the dashboard opens a modal with Downloads and
Server Imports tabs, album art thumbnails, quality/source badges, and
pagination.
- Watchlist nullable migration now preserves profile_id column and composite
UNIQUE constraints when rebuilding the table
- Profile support migration always repairs missing profile_id columns on all
tables, even if the migration metadata key already exists (handles tables
rebuilt by other migrations)
- Confirm dialog z-index raised to 100000 to appear above profile picker
overlay (99999), fixing invisible delete confirmation
The migration to make spotify_artist_id nullable was using fragile string
matching against CREATE TABLE SQL, which silently failed for some databases.
Now uses PRAGMA table_info to reliably detect the NOT NULL flag.
Automatically mirrors every parsed playlist (Spotify, Tidal, YouTube, Beatport) to a local database so they're always accessible — even if a service subscription lapses or the browser closes.
- New "Mirrored" tab on the Sync page with source-branded cards showing discovery/download status
- Auto-mirrors on successful parse (upsert — re-parsing updates the existing mirror, no duplicates)
- Click any mirrored playlist to browse its full track list, then run it through the discovery pipeline
- Cards dynamically reflect live state: Discovering → Discovered → Downloading → Downloaded
- Download modal rehydrates after page refresh — click a "Downloading..." card to resume viewing progress
- All phase transitions (start, complete, cancel, error, modal close) keep card and backend state in sync
- Profile-scoped via profile_id, consistent with other features
Allow multiple users to share a single SoulSync instance with isolated personal data. Each profile gets its own watchlist, wishlist, discovery pool, similar artists, and bubble snapshots — while sharing the same music library, database, and service credentials.
- Netflix-style profile picker on startup when multiple profiles exist
- Optional PIN protection per profile; admin PIN required when >1 profiles
- Admin-only profile management (create, edit, rename, delete)
- Profile avatar images via URL with colored-initial fallback
- Zero-downtime SQLite migration — all existing data maps to auto-created
admin profile
- Single-user installs see no changes — profile system is invisible until
a second profile is created
- WebSocket count emitters scoped to profile rooms (watchlist/wishlist)
- Background scanners (watchlist, wishlist, discovery) iterate all profiles