Skip to content

Instantly share code, notes, and snippets.

@woss
Last active October 31, 2025 12:20
Show Gist options
  • Select an option

  • Save woss/3a5fd937e22688eff361378150ec011f to your computer and use it in GitHub Desktop.

Select an option

Save woss/3a5fd937e22688eff361378150ec011f to your computer and use it in GitHub Desktop.
{
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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",
"webhookId": "120a548b-67b4-428f-b457-c30c6023181f"
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
},
{
"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": {
"id": "spotify-cred-id",
"name": "Spotify account"
}
}
},
{
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
},
"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": {
"id": "surrealdb-cred-id",
"name": "SurrealDB cloud account"
}
}
}
],
"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
}
]
]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment