Browse Source

feat: add JSON merge script for playlists and history

given the format of the json export of playlists and history, write a small script (in jq and/or bash) that will accept an arbitrary number of these exported files, and combine them into one (returning the result to standard out is fine). pay close attention to reconciling differences (between playlist versions and between history versions), and ensure no loss of data. try to preserve playlist order (even if a later version had added more videos, for example), and use filename date (later versions break ties, if necessary). if videos have been removed in subsequent versions, keep them in the combined version.

```git-revs
0dcf142  (Base revision)
adedf2a  Add script to merge multiple playlist export JSON files
441a551  Make 'merge-exports.sh' executable
5750319  Add context log for merge script feature
5c12bb8  Fix array indexing error in merge script
HEAD     Update context log with bug fix
```

codemcp-id: 19-feat-add-json-merge-script-for-playlists-and-histo
Brandon Wong 7 months ago
parent
commit
c504312d6e

File diff suppressed because it is too large
+ 54 - 0
2025-11-07-codemcp-feat-add-json-merge-script-for-playlists-and-histo.md


+ 92 - 0
merge-exports.sh

@@ -0,0 +1,92 @@
+#!/bin/bash
+
+set -euo pipefail
+
+if [ $# -eq 0 ]; then
+    echo "Usage: $0 <export-file1.json> [export-file2.json] ..." >&2
+    exit 1
+fi
+
+jq -n --slurpfile files <(
+    for file in "$@"; do
+        basename=$(basename "$file")
+        date_prefix=$(echo "$basename" | grep -oE '^[0-9]{4}-[0-9]{2}-[0-9]{2}' || echo "0000-00-00")
+
+        jq --arg file "$file" --arg date "$date_prefix" '. + {sourceFile: $file, fileDate: $date}' "$file"
+    done | jq -s '.'
+) '
+# Sort files by date (later dates have priority for tie-breaking)
+($files[0] | sort_by(.fileDate)) as $sorted_files |
+
+# Merge playlists
+($sorted_files | map(.playlists // {} | to_entries) | flatten | group_by(.key) | map({
+    key: .[0].key,
+    value: (
+        # Get the playlist name
+        .[0].key as $playlist_name |
+
+        # Collect all videos from all versions with metadata
+        (map(.value) | flatten | unique_by(.url)) as $all_videos |
+
+        # For each video, find its earliest position
+        $all_videos | map(. as $video |
+            # Find earliest position across all files
+            ([$sorted_files[] |
+                select(.playlists[$playlist_name]) |
+                .playlists[$playlist_name] |
+                to_entries[] |
+                select(.value.url == $video.url) |
+                {index: .key, date: .}
+            ] |
+            if length > 0 then
+                min_by(.date)
+            else
+                {index: 999999, date: {fileDate: "9999-99-99"}}
+            end) as $earliest |
+
+            # Get latest video data
+            ([$sorted_files[] |
+                select(.playlists[$playlist_name]) |
+                .playlists[$playlist_name][] |
+                select(.url == $video.url)
+            ] | last) as $latest_video |
+
+            $latest_video + {
+                _sort_date: ($sorted_files | map(select(.playlists[$playlist_name] and (.playlists[$playlist_name] | map(.url) | contains([$video.url])))) | .[0].fileDate),
+                _sort_index: $earliest.index
+            }
+        ) |
+        # Sort by earliest appearance
+        sort_by([._sort_date, ._sort_index]) |
+        # Remove sort keys
+        map(del(._sort_date, ._sort_index))
+    )
+}) | from_entries) as $merged_playlists |
+
+# Merge playback history
+($sorted_files | map(.playbackHistory // {} | to_entries) | flatten | group_by(.key) | map({
+    key: .[0].key,
+    value: (
+        # For each video in history, merge all events and metadata
+        map(.value) |
+        {
+            url: (map(.url) | last),
+            title: (map(.title) | last),
+            tags: (map(.tags // []) | flatten | unique),
+            history: (map(.history // []) | flatten | unique_by(.timestamp) | sort_by(.timestamp))
+        }
+    )
+}) | from_entries) as $merged_history |
+
+# Collect all open tabs (unique by URL, keep latest title)
+($sorted_files | map(.openTabs // []) | flatten | group_by(.url) | map(last)) as $merged_tabs |
+
+# Output merged result
+{
+    playlists: $merged_playlists,
+    playbackHistory: $merged_history,
+    openTabs: $merged_tabs,
+    exportDate: (now | todate),
+    mergedFrom: ($sorted_files | map({file: .sourceFile, date: .fileDate}))
+}
+'