This is a pretty big one! This release introduces a major internal refactor, replacing loosely structured identifiers with strongly typed models, modernizing service discovery, and simplifying the core architecture. While most of these changes are internal, they provide a stronger foundation for future development and improve the library's extensibility and maintainability.
The release also introduces SyncedPlaylist, a new API for bidirectional, conflict-free playlist synchronization across multiple services, alongside new examples, quality-of-life improvements, and bug fixes. Due to the architectural changes, this release includes several breaking changes for developers extending or integrating with plistsync.
Breaking Changes
- Refactored track identifier handling across the codebase by replacing service-specific
TrackIdsdictionaries with strongly typed, immutableTrackIDdataclasses for each service (e.g. Spotify, Plex). - Refactored playlist identifier handling across the codebase by replacing service-specific
PlaylistIdsdictionaries with strongly typed, immutablePlaylistIDdataclasses for each service (e.g. Spotify, Plex).- Removed obsolete playlist ID extraction utilities that are no longer required under the new identifier system.
- Updated all playlist operations and internal synchronization flows to use the new typed identifier model consistently.
This improves type safety, clarity, and extensibility for playlist identification and lookup operations. All playlist-related APIs and synchronization logic now operate on explicit identifier types instead of loosely structured dictionaries.
- Replaced the import-based
ServiceRegistrywith entry-point based service discovery viaServiceLoader(ServiceLoader.get(name)/ServiceLoader.all()). Services are now discovered through theplistsync.servicesentry-point group without importing all service modules, and services with missing optional dependencies are skipped gracefully. - Removed the
track_cls,library_cls,playlist_clsandplaylist_id_clsattributes fromServiceimplementations; registered classes are resolved through theRegistryinstead. OfflinePlaylistnow takes a typedPlaylistID(id=...) instead of anid_serialstring.
Added
- Added typed
Servicemarker class with a module-inferrednameproperty and registry-backed accessors (library(),tracks(),track_ids(),playlist_ids()), enabling single-handle access to all a service's core types. (#95) - Added generic
Registrybase class that auto-registers concrete implementations by service name at class-definition time.Track,Library,TrackID, andPlaylistIDare now registry roots, making service classes discoverable simply by importing their module. - Added
SerialIDbase class unifying the identifier contract (parse/serial) with a service-derived namespaceprefix()that is enforced on construction.TrackID.from_serial()andPlaylistID.from_serial()resolve a serial string to the correct service's identifier class viaServiceLoader. - Added a migration example to fully copy a playlist from an arbitrary service to another. (#60)
- Added
SyncedPlaylistclass for bidirectional playlist synchronization across an arbitrary number of services. This class encapsulates the logic for detecting differences, applying updates, and maintaining a consistent state across all linked services. (#100)- Conflict-free state replication via a CRDT (Fugue): each linked playlist acts as a replica, so changes made directly on a service (outside plistsync) merge cleanly without conflicts.
- Phased
sync()pipeline:refresh()pulls the current state from each service,merge()reconciles external additions/removals/reorders into the internal collection,enrich()cross-links tracks between services and backfills missing identifiers (e.g. ISRCs), andpush()writes the resolved state back to every linked playlist. - Track association is availability-aware: tracks that cannot be resolved on a given service are skipped for that service instead of being dropped from the internal collection.
- Added
SyncedPlaylistID, a UUID-based playlist identifier for synced playlists. - Added serialization for SyncedPlaylists,
save_toandload_frommethods (#104)
Fixed
- When syncing playlists to traktor, we now insert missing tracks into the library collection. This avoids those track to disappear from the playlist when launching Traktor after the sync. (#54)
- Adjusted spotify api layer for deprecations
Changes
- Introduced a OfflineTrack class mirroring the idea behind the OfflinePlaylist. This allows us to preserve track information even after deletion from the remote service, and to keep the Playlist class hierarchy cleanly separated between in-memory and service-synced implementations.