Synchronization Behavior
Understanding the exact comparison and failure rules is important before copying a large history.
Workflow
resolve range
→ persist session
→ fetch source and target snapshots
→ compare and build queue
→ enrich queued tracks from source
→ submit queued scrobbles to target
→ build result and delete completed state
All work is synchronous. Metadata lookups and submissions are performed one scrobble at a time in ascending timestamp order.
Range resolution
The requested range is inclusive at both ends.
- An explicit start is used unchanged.
- Without a start, the target's most recent scrobble timestamp is used.
- If the target is empty and start is omitted,
MissingStartTimestampErroris raised. - An explicit end is used unchanged.
- Without an end,
int(time.time())is used. - Negative bounds and start-after-end ranges raise
InvalidSyncRangeError.
Using the target's latest timestamp as the inferred start is safe because the event already present at that exact timestamp is removed during comparison, while other source events at the same timestamp can still be discovered.
Snapshot and identity rules
Both networks are queried for the full resolved range. Snapshot storage de-duplicates within each side using:
(artist, title, timestamp)
A source event is queued only when no target event has exactly the same three fields. Album, MusicBrainz ID, and duration do not participate. SQLite's default text equality is exact, so spelling and case differences are distinct.
Consequences:
- two plays of the same track at different timestamps are distinct;
- album differences alone are considered the same event;
- duplicate source rows with the same identity collapse into one snapshot row;
- a remote service that changes artist/title spelling can cause an apparent missing event.
Metadata enrichment
For every missing event, the engine asks the source network for track metadata by artist and title. It stores an optional MusicBrainz ID and duration in milliseconds. Missing metadata values are valid; malformed responses are not.
Duration is converted to seconds at submission time with round(milliseconds / 1000) and a minimum of one second. Album comes from the source snapshot.
Submission and retries
Each ready item is marked syncing before the target call and synced only after the call returns successfully.
Metadata and submission operations retry only NetworkConnectionError. Defaults are three total attempts and a five-second delay. Once retries are exhausted, the item is marked failed with its stage and error, processing stops, and the error is re-raised. Authentication errors and invalid responses are not retried.
Snapshot fetches and resume reconciliation calls are not covered by the retry service.
Progress callbacks
Callbacks receive SyncProgress for:
- every phase transition;
- source and target snapshot completion;
- initial and per-item metadata progress; and
- initial and per-item submission progress.
current_scrobble_completed=False identifies an item currently being worked on; True reports that the same item finished. A phase transition uses zero totals. Comparison itself has no item-level progress report.
Callbacks are invoked synchronously and exceptions propagate, leaving recoverable state when persistence has already occurred.
Result semantics
SyncResult counts the stored source snapshot, target snapshot, total queued items, synced items, and failed items. On a normal successful return all queued items have synchronized, and the completed session state is then deleted.
Direction and concurrency
The engine is one-way and does not merge histories automatically. A separate reverse run is required for bidirectional coverage, and each direction should be reviewed independently.
The CLI prevents a new run when its database contains unfinished work. The Python service does not impose this policy. It also does not provide an async API or explicit cross-process scheduling; applications should serialize operations that share a database or isolate them in separate database files.