feat(playlists): two-way (3-way) merge YouTube sync + mandatory preview (S7)
"Sync to YouTube" was a one-directional destructive push (local wins, deletes from YouTube every item not in the local list), so items added on YouTube were lost and a dirty pull was skipped entirely. Replace it with a real 3-way merge: each item is classified against the last-synced baseline as a local-add, a YouTube-add, or a genuine delete, so an add or delete on either side survives. - Persist the ordered baseline id set (new playlists.synced_video_ids, migration 0075); the fingerprint hash alone couldn't drive a merge. NULL baseline (fresh export / pre-S7 link) unions losslessly on the first sync and records one. - Pure merge_plan(baseline, local, youtube) -> add/remove per side + merged order (local order, YouTube-only additions appended). merge_playlist applies the local side directly and the YouTube side through the failure breaker; the baseline only advances on full success so a partial failure re-converges. - plan_merge/push-plan now report what each side gains and loses; the confirm dialog shows the two-way plan before applying (pushPlanMerge, HU+EN). - Tests: 10 new (pure classification incl. delete-vs-stale-copy; DB apply proving a YouTube-add and a Siftlode-add both survive on both sides, a genuine delete propagates without collateral loss, no-baseline never deletes, create path).
This commit is contained in:
@@ -31,12 +31,16 @@ export interface PlaylistDetail {
|
||||
items: Video[];
|
||||
}
|
||||
|
||||
// Two-way sync plan: what each side gains/loses under the 3-way merge (neither side wins
|
||||
// wholesale). Local edits are free; only the YouTube writes cost quota.
|
||||
export interface PushPlan {
|
||||
action: "create" | "update";
|
||||
to_insert: number;
|
||||
to_delete: number;
|
||||
add_yt: number; // local-adds to push out to YouTube
|
||||
add_local: number; // YouTube-adds to pull into Siftlode
|
||||
remove_yt: number; // genuine local deletes propagated to YouTube
|
||||
remove_local: number; // genuine YouTube deletes propagated to Siftlode
|
||||
to_reorder: number;
|
||||
yt_extra: number; // items on YouTube that the push would remove (divergence)
|
||||
rename: boolean;
|
||||
units_estimate: number;
|
||||
remaining_today: number;
|
||||
affordable: boolean;
|
||||
@@ -44,9 +48,11 @@ export interface PushPlan {
|
||||
|
||||
export interface PushResult {
|
||||
created: number;
|
||||
inserted: number;
|
||||
deleted: number;
|
||||
inserted: number; // added on YouTube
|
||||
deleted: number; // removed on YouTube
|
||||
reordered: number;
|
||||
pulled: number; // added into Siftlode from YouTube
|
||||
removed_local: number; // removed from Siftlode (deleted on YouTube)
|
||||
failures: string[];
|
||||
yt_playlist_id: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user