{
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6,
              "triggerAtMinute": 31
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -1472,
        1184
      ],
      "id": "634b5228-5b1a-4871-a8ac-f919327bfaf3",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const { track, track_id} = $input.item.json\nreturn { track, track_id}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3248,
        1968
      ],
      "id": "9155754e-6f79-4209-9441-591bc87866e4",
      "name": "clean payload"
    },
    {
      "parameters": {
        "url": "=https://api.spotify.com/v1/users/{{ $json.id.id }}/playlists",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "spotifyOAuth2Api",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "next,items(id,snapshot_id,tracks.total)"
            },
            {
              "name": "limit",
              "value": "50"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "pagination": {
            "pagination": {
              "paginationMode": "responseContainsNextURL",
              "nextURL": "={{ $response.body.next }}",
              "paginationCompleteWhen": "other",
              "completeExpression": "={{$response.body.next === null}}"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        64,
        1840
      ],
      "id": "52db77f5-eb95-4ba0-b4d2-4d38434787ce",
      "name": "my playlists",
      "alwaysOutputData": false,
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=select id, snapshot_id from playlist:{{ $json.id }};",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        624,
        1872
      ],
      "id": "f2976ec0-37e8-45dc-9b95-a9025802c9d7",
      "name": "synced playlists",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "combine",
        "fieldsToMatchString": "snapshot_id",
        "joinMode": "keepNonMatches",
        "outputDataFrom": "input1",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        816,
        1632
      ],
      "id": "44ea9012-7930-4523-8cc5-1caa4e9e6968",
      "name": "missing playlists",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=select id, snapshot_id from playlist:{{ $json.id }} where snapshot_id != \"{{ $json.snapshot_id }}\";",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        976,
        1952
      ],
      "id": "bb40bbd3-bce1-427a-be88-ab80b74e7fb0",
      "name": "query playlist with last snapshot",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const isObjectEmpty = (objectName) => {\n  return Object.keys(objectName).length === 0\n}\nlet changedPlaylist = []\nfor (const item of $input.all()) {\n  if(!isObjectEmpty(item.json)){\n    changedPlaylist.push({\n      id:item.json.id.id,\n      snapshot_id: item.json.snapshot_id\n    })\n  }\n}\n\n\nreturn changedPlaylist;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1312,
        2096
      ],
      "id": "181f4f5c-142a-41e4-a54f-d28929dbefd8",
      "name": "playlists that need sync",
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "url": "=https://api.spotify.com/v1/me",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "spotifyOAuth2Api",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1008,
        1184
      ],
      "id": "b82fcdef-63c8-4865-b018-297224e7390f",
      "name": "get me",
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "user",
        "id": "={{ $json.id }}",
        "data": "={{ $json }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        -784,
        1184
      ],
      "id": "8945a030-4c39-4957-aeb0-59cb2fcbedf3",
      "name": "upsert me",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "fieldToSplitOut": "items",
        "options": {}
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        256,
        2080
      ],
      "id": "cef37ccb-5f76-4850-8621-4a86f7f0cfb3",
      "name": "combine all calls"
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=select count() from playlist_track where in = playlist:{{ $json.id }} group all;",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        624,
        2080
      ],
      "id": "18bf2cee-b7c3-4f7b-af2b-ca4ef77734cf",
      "name": "query playlist_tracks",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "numberInputs": 3
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1056,
        2336
      ],
      "id": "f9add17f-2fc9-4b35-9b9e-afbf21e29edc",
      "name": "missing track for playlists"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "count"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        880,
        2112
      ],
      "id": "82a78312-4dcd-478e-b082-005bc49bd46a",
      "name": "Aggregate db count"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "tracks.total",
              "renameField": true,
              "outputFieldName": "spotify_totals"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        624,
        2272
      ],
      "id": "eb650346-5aae-429e-877b-9739231af9ad",
      "name": "Aggregate spotify data"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "id",
              "renameField": true,
              "outputFieldName": "playlist_ids"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        624,
        2464
      ],
      "id": "67a03a6f-3646-4c26-aca6-f35ea90a22f9",
      "name": "Aggregate spotify ids"
    },
    {
      "parameters": {
        "url": "=https://api.spotify.com/v1/playlists/{{ $json.id }}/tracks?offset=0&limit=100",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "spotifyOAuth2Api",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          },
          "pagination": {
            "pagination": {
              "paginationMode": "responseContainsNextURL",
              "nextURL": "={{ $response.body.next }}",
              "paginationCompleteWhen": "other",
              "completeExpression": "={{$response.body.next === null}}",
              "requestInterval": 100
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1984,
        2272
      ],
      "id": "a8773504-3663-475c-b57d-06f7caf9c526",
      "name": "get all tracks for playlist",
      "alwaysOutputData": false,
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "let data = {\n  count: [0],\n  spotify_totals: [0],\n  playlist_ids: ['']\n}\n\nfor (const input of $input.all()) {\n  if (input.json.count) {\n    data.count = input.json.count\n  }\n  if (input.json.spotify_totals) {\n    data.spotify_totals = input.json.spotify_totals\n  }\n  if (input.json.playlist_ids) {\n    data.playlist_ids = input.json.playlist_ids\n  }\n}\n\nlet retData = []\n\nfor (let index = 0; index < data.playlist_ids.length; index++) {\n  const playlist_id = data.playlist_ids[index];\n  const dbCount = data.count[index];\n  const spotifyTotal = data.spotify_totals[index];\n  if(dbCount !== spotifyTotal) {\n    retData.push({\n      id:playlist_id,\n      dbCount,\n      spotifyTotal,\n      diff: spotifyTotal - dbCount\n    })\n  }\n}\n\nreturn retData;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1264,
        2320
      ],
      "id": "3d7e9a5f-a1ec-41f1-b756-b4507537b220",
      "name": "filter out synced playlists"
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "album",
        "id": "={{ $json.album_id }}",
        "data": "={{ $json.album }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        3472,
        2160
      ],
      "id": "c07771d1-c811-4812-a1aa-931316c1ae5e",
      "name": "Upsert album",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1712,
        2064
      ],
      "id": "2b9a3f57-4678-4c2c-8215-7b74a87c2631",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "amount": 0.5
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        3936,
        2352
      ],
      "id": "3c92d837-8a21-45b2-9448-3c8dcd4042af",
      "name": "Wait"
    },
    {
      "parameters": {
        "sortFieldsUi": {
          "sortField": [
            {
              "fieldName": "diff"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.sort",
      "typeVersion": 1,
      "position": [
        1488,
        2320
      ],
      "id": "de0e99c6-8989-434b-9848-07bf6213051a",
      "name": "Sort by diff ASC"
    },
    {
      "parameters": {
        "numberInputs": 7
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        3696,
        1984
      ],
      "id": "8988cf7d-9c5f-43e0-91be-8bebecef09d4",
      "name": "Merge"
    },
    {
      "parameters": {
        "resource": "relationship",
        "fromRecordId": "=album:{{ $json.album_id }}",
        "relationshipType": "album_track",
        "toRecordId": "=track:{{ $json.track_id }}",
        "options": {},
        "connectionPooling": {
          "retryAttempts": 0
        }
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        3472,
        2352
      ],
      "id": "df731b6a-370c-4dd7-9624-3aaf22e7d914",
      "name": "album_track",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=DELETE from playlist_track where in=playlist:{{ $('Loop Over Items').item.json.id }};",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        2432,
        1984
      ],
      "id": "0c2fbd25-2e83-4b53-9406-c3c4e0f09252",
      "name": "delete all playlist_track items for playlist",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=RELATE playlist:{{ $json.playlist_id }}->playlist_track->track:{{ $json.track_id }} SET added_at = d'{{ $json.added_at }}';",
        "options": {},
        "connectionPooling": {
          "retryAttempts": 0
        }
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        3472,
        1776
      ],
      "id": "b7eee354-6cc7-473b-b338-63557952ec4d",
      "name": "playlist_track",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "track",
        "id": "={{ $json.track_id }}",
        "data": "={{ $json.track }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        3472,
        1968
      ],
      "id": "b8e1bf62-cf8a-4345-bba1-6a503ed82712",
      "name": "Upsert track from playlist",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "playlist",
        "id": "={{ $json.playlist_id }}",
        "data": "={{ $json.playlist }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        608,
        -256
      ],
      "id": "2a197992-94cb-4036-aa40-c3b25a80846f",
      "name": "Upsert playlist",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "playlist",
        "operation": "getUserPlaylists",
        "returnAll": true
      },
      "type": "n8n-nodes-base.spotify",
      "typeVersion": 1,
      "position": [
        64,
        208
      ],
      "id": "9cfaaf92-e1e6-474c-8b7a-3cca9f8c847d",
      "name": "Get a user's playlists",
      "executeOnce": false,
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "relationship",
        "fromRecordId": "=user:`{{ $('upsert me').item.json.id.id }}`",
        "relationshipType": "has_playlist",
        "toRecordId": "=playlist:{{ $json.playlist_id }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        608,
        -64
      ],
      "id": "c60344bd-0c9e-4532-90e2-b21a791164bc",
      "name": "has_playlist",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "resource": "relationship",
        "fromRecordId": "=user:`{{ $json.owner_id }}`",
        "relationshipType": "playlist_owner",
        "toRecordId": "=playlist:{{ $json.playlist_id }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        608,
        128
      ],
      "id": "c2e3801c-c1fc-4f90-bee0-9f7e8eb455ce",
      "name": "playlist_owner",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "user",
        "id": "={{ $json.owner_id }}",
        "data": "={{ $json.owner }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        608,
        320
      ],
      "id": "ff96e6a3-c5cc-489a-bf83-914a65aac95f",
      "name": "upsert_user as owner",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const payload = []\nfor (const item of $input.all()) {\n  const {owner, tracks, ...playlistRest} = item.json\n  const {id, ...playlist} = playlistRest\n  const {id: owner_id, ...ownerRest} = owner\n  payload.push({owner:ownerRest, tracks,playlist, playlist_id:id, owner_id })\n}\n\nreturn payload;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        288,
        208
      ],
      "id": "9170ea5c-c9fd-4cf9-921c-5c0f66b2674e",
      "name": "remap keys and restructure"
    },
    {
      "parameters": {
        "resource": "library",
        "returnAll": true
      },
      "type": "n8n-nodes-base.spotify",
      "typeVersion": 1,
      "position": [
        1184,
        1248
      ],
      "id": "fb4e5977-077b-49d0-bffc-909953195702",
      "name": "Get liked tracks",
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "track",
        "id": "={{ $json.track_id }}",
        "data": "={{ $json.track }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        1856,
        864
      ],
      "id": "dde0f6b3-ddce-4ded-b30c-1fb29e39cec6",
      "name": "Upsert track",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=RELATE {{ $('upsert me').item.json.id.tb }}:{{ $('upsert me').item.json.id.id }}->likes_track->track:{{ $json.track_id }} SET added_at = d'{{ $json.added_at }}';",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        1856,
        640
      ],
      "id": "bd64308f-1bc8-45d0-8d47-04cf350470a0",
      "name": "me likes",
      "notesInFlow": false,
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        960,
        672
      ],
      "id": "1574cb4e-bf5d-4df5-beb6-aa5c0d1c0469",
      "name": "Liked songs in sync"
    },
    {
      "parameters": {
        "jsCode": "return [{run:1}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        960,
        1072
      ],
      "id": "8a7d18e0-c985-43c0-b3e9-2d83d37c7a91",
      "name": "dummy single run"
    },
    {
      "parameters": {
        "jsCode": "return {message: \"Sync success\"}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2080,
        928
      ],
      "id": "5d094c3c-370d-472a-851f-e5f14987fb2d",
      "name": "success"
    },
    {
      "parameters": {
        "jsCode": "return {message: \"Sync error\"}"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2080,
        1120
      ],
      "id": "2138387c-dd76-40a4-b7d1-91600e1b7a63",
      "name": "error"
    },
    {
      "parameters": {
        "jsCode": "const retArray = []\n\nfor (const item of $input.all()) {\n  const {id, album, ...restTrack} = item.json.track\n  const {id: album_id, ...restAlbum} = album\n  \n  retArray.push({\n    track_id:id, \n    album_id, \n    album:restAlbum, \n    track: restTrack,\n    added_at:item.json.added_at\n  })\n}\n\nreturn retArray;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1360,
        1248
      ],
      "id": "53ae34b0-28e0-4dde-bfa0-6da213635748",
      "name": "modify output"
    },
    {
      "parameters": {
        "mode": "combine",
        "advanced": true,
        "mergeByFields": {
          "values": [
            {
              "field1": "out.id",
              "field2": "track_id"
            }
          ]
        },
        "joinMode": "keepNonMatches",
        "outputDataFrom": "input2",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1552,
        928
      ],
      "id": "0a67c772-e42a-4f26-9536-589e45aa2b21",
      "name": "match only missing tracks"
    },
    {
      "parameters": {
        "url": "https://api.spotify.com/v1/me/tracks",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "spotifyOAuth2Api",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "fields",
              "value": "=total"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        272,
        1088
      ],
      "id": "ffcab92b-ff2b-47f7-8e37-ed62f2427497",
      "name": "get tracks total",
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "relationship",
        "fromRecordId": "=album:{{ $json.album_id }}",
        "relationshipType": "album_track",
        "toRecordId": "=track:{{ $json.track_id }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        1856,
        1264
      ],
      "id": "9de5abc5-202b-49a1-8c4f-18571b930dbb",
      "name": "album_track1",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        64,
        880
      ],
      "id": "7a31f56e-12de-46f2-a622-930e05ad96f5",
      "name": "passthrough"
    },
    {
      "parameters": {
        "mode": "combine",
        "advanced": true,
        "mergeByFields": {
          "values": [
            {
              "field1": "dbTotal",
              "field2": "total"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        512,
        880
      ],
      "id": "378265c5-6ecd-4a65-857f-0fc442696e4f",
      "name": "merge local & spotify",
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "content": "# Sync liked tracks",
        "height": 848,
        "width": 2304,
        "color": 6
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -16,
        624
      ],
      "id": "1cc7511a-555b-48fb-a260-f38294f3639e",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "# Sync playlists",
        "height": 848,
        "width": 912,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -16,
        -320
      ],
      "id": "404958a8-c58c-44cc-af79-1254ffab9d3f",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "# Sync playlists tracks",
        "height": 1232,
        "width": 4288,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -16,
        1568
      ],
      "id": "38e75dbd-ce7f-491e-8b64-85b560ce2100",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "3478646e-57f4-49e0-a241-29aefe83332e",
              "leftValue": "={{$json}}",
              "rightValue": "",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2192,
        2128
      ],
      "id": "ab52f96a-99ab-4228-8a81-2c424ec62d86",
      "name": "with result"
    },
    {
      "parameters": {
        "mode": "chooseBranch",
        "useDataOfInput": 2
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        2672,
        2176
      ],
      "id": "359246a9-338f-4dd7-90f9-e47814158560",
      "name": "fwd input 2"
    },
    {
      "parameters": {
        "jsCode": "function slugify(str) {\n  return str\n    .toLowerCase()\n    .trim()\n    .replace(/[\\s\\W-]+/g, '_') // Replace spaces and non-word chars with -\n    .replace(/^-+|-+$/g, '');  // Remove leading/trailing -\n}\n\nfunction idFromLocal(uri) {\n  const onlyValue = uri.replace(\"spotify:local:\", \"\");\n  // const parts = onlyValue.split(\":\");\n  // const artist = parts[0];\n  // const album = parts[2];\n  // const track = parts[1];\n  // const duration = parts[3];\n  return slugify(decodeURIComponent(onlyValue));\n}\n\nconst url = $input.first().json.href;\nconst match = url.match(/\\/playlists\\/([^/]+)(?:\\/|$)/);\nconst playlist_id = match ? match[1] : null;\nlet allItems = [];\n\n// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n  for (const i of item.json.items) {\n    if (i.track) {\n      const {\n        id: origId,\n        album: { id: album_id, ...restAlbum },\n        ...restItem\n      } = i.track;\n      let id = origId;\n      if (!origId) {\n        // need to create a meaningful id\n        id = idFromLocal(restItem.uri);\n      }\n\n      const _i = Object.assign(\n        {},\n        i,\n        { track: restItem },\n        {\n          album: restAlbum,\n          album_id,\n          playlist_id,\n          track_id: id,\n        }\n      );\n      allItems.push(_i);\n    } else {\n      console.log(\"Missing track\", item);\n    }\n  }\n}\n\nreturn allItems;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2848,
        2048
      ],
      "id": "db2ebc1f-8dc5-4b89-b62a-d57754606f41",
      "name": "restructure payload"
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "album",
        "id": "={{ $json.album_id }}",
        "data": "={{ $json.album }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        1856,
        1072
      ],
      "id": "f33f059a-fdd5-4a11-9b73-786bc75fa47f",
      "name": "Upsert album from liked tracks",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=select count() as dbTotal from likes_track where in = {{$json.id.tb}}:{{ $json.id.id }} group all;",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        288,
        752
      ],
      "id": "cd6aead8-ea11-4560-b00b-a97e2445d56a",
      "name": "get liked tracks count",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "query",
        "query": "=select * from likes_track where in = {{$('upsert me').item.json.id.tb}}:{{ $('upsert me').item.json.id.id }};",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        1184,
        944
      ],
      "id": "09b652da-9ebc-41da-a2b3-728c2b5ef41d",
      "name": "get all liked tracks",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1968,
        1856
      ],
      "id": "901b412c-9fb7-45cf-88a5-267fb7086277",
      "name": "done iterating"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "cc28f07a-1fff-41e4-92ec-69e99084fe96",
              "leftValue": "={{ $json }}",
              "rightValue": "",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              }
            },
            {
              "id": "e418c917-e9cb-4493-b074-74f426f7f760",
              "leftValue": "={{ $json.total }}",
              "rightValue": "={{ $json.dbTotal }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {
          "ignoreCase": false
        }
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        736,
        880
      ],
      "id": "7c0d65c7-6006-4734-8d30-1f869bbadffe",
      "name": "in sync"
    },
    {
      "parameters": {
        "operation": "upsertRecord",
        "table": "artist",
        "id": "={{ $json.item.artistId }}",
        "data": "={{ $json.item }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        448,
        -720
      ],
      "id": "164964c6-e01c-4623-a9f8-c1a7bdf12b83",
      "name": "Upsert artists",
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "myData",
        "returnAll": true
      },
      "type": "n8n-nodes-base.spotify",
      "typeVersion": 1,
      "position": [
        32,
        -720
      ],
      "id": "707e2586-f9d3-4847-beee-56d2083cef32",
      "name": "Get your followed artists",
      "credentials": {
        "spotifyOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "relationship",
        "fromRecordId": "={{$('upsert me').item.json.id.tb}}:{{$('upsert me').item.json.id.id}}",
        "relationshipType": "follows",
        "toRecordId": "={{ $json.id.tb }}:{{ $json.id.id }}",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        688,
        -720
      ],
      "id": "82396689-12d8-4282-a7be-d79a2d19ca62",
      "name": "Create a relationship",
      "notesInFlow": false,
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nconst ret = []\nfor (const item of $input.all()) {\n  ret.push({item: {...item.json, artistId: item.json.id}})\n}\n\nreturn ret;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        -720
      ],
      "id": "3b3139b3-0d72-4c42-a6d7-cd6e03174fea",
      "name": "add artistId"
    },
    {
      "parameters": {
        "content": "# Sync following artists",
        "height": 432,
        "width": 1072,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -16,
        -848
      ],
      "id": "a9ac44c1-f319-4a81-b141-4b564ef6fabb",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "resource": "query",
        "query": "DEFINE TABLE IF NOT EXISTS album TYPE ANY SCHEMALESS PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS album_id_idx ON album FIELDS id UNIQUE; DEFINE INDEX IF NOT EXISTS album_uri_idx ON album FIELDS uri UNIQUE;  DEFINE TABLE IF NOT EXISTS artist TYPE ANY SCHEMALESS PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS artist_id_idx ON artist FIELDS id UNIQUE; DEFINE INDEX IF NOT EXISTS artist_uri_idx ON artist FIELDS uri UNIQUE;  DEFINE TABLE IF NOT EXISTS playlist TYPE ANY SCHEMALESS PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS snapshot_id ON playlist FIELDS snapshot_id; DEFINE INDEX IF NOT EXISTS playlist_uri_idx ON playlist FIELDS uri UNIQUE;  DEFINE TABLE IF NOT EXISTS track TYPE NORMAL SCHEMALESS PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS track_index ON track FIELDS id UNIQUE; DEFINE INDEX IF NOT EXISTS track_uri_idx ON track FIELDS uri UNIQUE;  DEFINE TABLE IF NOT EXISTS user TYPE NORMAL SCHEMALESS PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS user_idx ON user FIELDS id UNIQUE; DEFINE INDEX IF NOT EXISTS user_uri_idx ON user FIELDS uri UNIQUE;  DEFINE TABLE IF NOT EXISTS album_track TYPE RELATION IN album OUT track SCHEMAFULL PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS album_track ON album_track FIELDS in, out UNIQUE;  DEFINE TABLE IF NOT EXISTS follows TYPE RELATION IN user OUT artist SCHEMAFULL PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS user_follow_artist ON follows FIELDS in, out UNIQUE;  DEFINE TABLE IF NOT EXISTS has_playlist TYPE RELATION IN user OUT playlist SCHEMALESS PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS user_playlist ON has_playlist FIELDS in, out UNIQUE;  DEFINE TABLE IF NOT EXISTS likes_track TYPE RELATION IN user OUT track SCHEMALESS PERMISSIONS NONE; DEFINE FIELD IF NOT EXISTS added_at ON likes_track TYPE datetime PERMISSIONS FULL; DEFINE INDEX IF NOT EXISTS user_likes_track ON likes_track FIELDS in, out UNIQUE;  DEFINE TABLE IF NOT EXISTS playlist_owner TYPE RELATION IN user OUT playlist SCHEMAFULL PERMISSIONS NONE; DEFINE INDEX IF NOT EXISTS playlist_owner_idx ON playlist_owner FIELDS in, out UNIQUE;  DEFINE TABLE IF NOT EXISTS playlist_track TYPE RELATION IN playlist OUT track SCHEMALESS PERMISSIONS NONE; DEFINE FIELD IF NOT EXISTS added_at ON playlist_track TYPE datetime PERMISSIONS FULL; DEFINE INDEX IF NOT EXISTS playlist_track_id_idx ON playlist_track FIELDS id UNIQUE;",
        "options": {},
        "connectionPooling": {}
      },
      "type": "n8n-nodes-surrealdb.surrealDb",
      "typeVersion": 1,
      "position": [
        -1216,
        1184
      ],
      "id": "78bc580e-7544-4f91-981b-5c9b36295d30",
      "name": "setup database",
      "alwaysOutputData": true,
      "credentials": {
        "surrealDbApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "setup database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "clean payload": {
      "main": [
        [
          {
            "node": "Upsert track from playlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "my playlists": {
      "main": [
        [
          {
            "node": "combine all calls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "synced playlists": {
      "main": [
        [
          {
            "node": "missing playlists",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "missing playlists": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "query playlist with last snapshot": {
      "main": [
        [
          {
            "node": "playlists that need sync",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "playlists that need sync": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get me": {
      "main": [
        [
          {
            "node": "upsert me",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "upsert me": {
      "main": [
        [
          {
            "node": "my playlists",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get a user's playlists",
            "type": "main",
            "index": 0
          },
          {
            "node": "passthrough",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get your followed artists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "combine all calls": {
      "main": [
        [
          {
            "node": "synced playlists",
            "type": "main",
            "index": 0
          },
          {
            "node": "missing playlists",
            "type": "main",
            "index": 0
          },
          {
            "node": "query playlist with last snapshot",
            "type": "main",
            "index": 0
          },
          {
            "node": "query playlist_tracks",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate spotify data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate spotify ids",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "query playlist_tracks": {
      "main": [
        [
          {
            "node": "Aggregate db count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "missing track for playlists": {
      "main": [
        [
          {
            "node": "filter out synced playlists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate db count": {
      "main": [
        [
          {
            "node": "missing track for playlists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate spotify data": {
      "main": [
        [
          {
            "node": "missing track for playlists",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Aggregate spotify ids": {
      "main": [
        [
          {
            "node": "missing track for playlists",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "get all tracks for playlist": {
      "main": [
        [
          {
            "node": "with result",
            "type": "main",
            "index": 0
          },
          {
            "node": "fwd input 2",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "filter out synced playlists": {
      "main": [
        [
          {
            "node": "Sort by diff ASC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert album": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "done iterating",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "get all tracks for playlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort by diff ASC": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "album_track": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 6
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "delete all playlist_track items for playlist": {
      "main": [
        [
          {
            "node": "fwd input 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "playlist_track": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Upsert track from playlist": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get a user's playlists": {
      "main": [
        [
          {
            "node": "remap keys and restructure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "remap keys and restructure": {
      "main": [
        [
          {
            "node": "Upsert playlist",
            "type": "main",
            "index": 0
          },
          {
            "node": "has_playlist",
            "type": "main",
            "index": 0
          },
          {
            "node": "playlist_owner",
            "type": "main",
            "index": 0
          },
          {
            "node": "upsert_user as owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get liked tracks": {
      "main": [
        [
          {
            "node": "modify output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert track": {
      "main": [
        [
          {
            "node": "success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "me likes": {
      "main": [
        [
          {
            "node": "success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "dummy single run": {
      "main": [
        [
          {
            "node": "Get liked tracks",
            "type": "main",
            "index": 0
          },
          {
            "node": "get all liked tracks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "modify output": {
      "main": [
        [
          {
            "node": "match only missing tracks",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "match only missing tracks": {
      "main": [
        [
          {
            "node": "me likes",
            "type": "main",
            "index": 0
          },
          {
            "node": "Upsert track",
            "type": "main",
            "index": 0
          },
          {
            "node": "Upsert album from liked tracks",
            "type": "main",
            "index": 0
          },
          {
            "node": "album_track1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get tracks total": {
      "main": [
        [
          {
            "node": "merge local & spotify",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "album_track1": {
      "main": [
        [
          {
            "node": "success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "passthrough": {
      "main": [
        [
          {
            "node": "get liked tracks count",
            "type": "main",
            "index": 0
          },
          {
            "node": "get tracks total",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "merge local & spotify": {
      "main": [
        [
          {
            "node": "in sync",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "with result": {
      "main": [
        [
          {
            "node": "delete all playlist_track items for playlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "fwd input 2": {
      "main": [
        [
          {
            "node": "restructure payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "restructure payload": {
      "main": [
        [
          {
            "node": "album_track",
            "type": "main",
            "index": 0
          },
          {
            "node": "Upsert album",
            "type": "main",
            "index": 0
          },
          {
            "node": "clean payload",
            "type": "main",
            "index": 0
          },
          {
            "node": "playlist_track",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert album from liked tracks": {
      "main": [
        [
          {
            "node": "success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get liked tracks count": {
      "main": [
        [
          {
            "node": "merge local & spotify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get all liked tracks": {
      "main": [
        [
          {
            "node": "match only missing tracks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "in sync": {
      "main": [
        [
          {
            "node": "Liked songs in sync",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "dummy single run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert artists": {
      "main": [
        [
          {
            "node": "Create a relationship",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get your followed artists": {
      "main": [
        [
          {
            "node": "add artistId",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "add artistId": {
      "main": [
        [
          {
            "node": "Upsert artists",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "setup database": {
      "main": [
        [
          {
            "node": "get me",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}