Handle followed (non-owned) playlists correctly during migration master
Spotify's GET /me/playlists returns playlists the account owns OR follows — treating a followed one as writable caused a 403 (can't add tracks to a playlist you don't own), and reusing it by name for the create-if-missing dedup could silently target the wrong playlist. CanonicalPlaylist now carries `owned`. A followed playlist is handled differently depending on the destination platform: - Same platform (e.g. Spotify -> Spotify): followed directly on the destination account using the same playlist ID — IDs are shared across accounts on one platform, so no matching is needed. If the destination has a stray same-named entry that's actually a different playlist (own ID mismatch, e.g. a duplicate wrongly created by the old buggy logic), it's removed first via the new followPlaylist/unfollowPlaylist adapter methods (Spotify: the unified PUT/DELETE /me/library with a playlist URI). Already correct (same ID) is left untouched. - Cross-platform: no reliable way to match a followed/curated playlist by name across platforms, so it becomes a dismiss-only review-queue note instead of a silent drop or a failed write. Also fixes two correctness bugs found while building this: - A reused existing (owned) destination playlist's current tracks are now fetched and deduped against before writing, instead of blindly appending every matched track — playlists don't dedup entries themselves, so this previously could have produced duplicate tracks on a rerun. - The "already fully migrated" match_cache check is now invalidated for a followed/same-platform playlist if its cached destination ID doesn't match the source's own ID (the only correct value there) — without this, a stale entry from before this fix permanently prevented ever reprocessing and cleaning up a wrongly-created duplicate. Also: rate-limit error messages and logs now show human-readable durations (e.g. "23.7h") instead of raw milliseconds, with unit rounding fixed so a value like 23.96h correctly promotes to "1.0d" instead of misleadingly displaying "24.0h". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>