From c0a38e8c8da4211d616f2d81382296ca8d26ee79 Mon Sep 17 00:00:00 2001 From: Ben W Date: Thu, 8 Jan 2026 15:15:02 +0000 Subject: [PATCH] clean up supabase, MAM automation continues. --- credential_stubs/BRiFacyi0A60Y7ZZ.json | 16 - credential_stubs/dsnKfvOBMkgU21Lt.json | 16 - tags.json | 24 + workflows/3l7tJfcRoA1T1o6g.json | 2 +- workflows/ERCWB3oSYbgsUiqL.json | 98 ++-- workflows/NChjOd3ILEmt0FdyAt8qA.json | 185 +++++++ workflows/kRZyX9H2uDHHncpE.json | 681 ++++++++++++++----------- workflows/kn1gehxiWbkRfDFFAKx0x.json | 208 ++++++++ workflows/xXUnt2hL2FKxzOhBnkd3Z.json | 128 ++++- 9 files changed, 974 insertions(+), 384 deletions(-) delete mode 100644 credential_stubs/BRiFacyi0A60Y7ZZ.json delete mode 100644 credential_stubs/dsnKfvOBMkgU21Lt.json create mode 100644 workflows/NChjOd3ILEmt0FdyAt8qA.json create mode 100644 workflows/kn1gehxiWbkRfDFFAKx0x.json diff --git a/credential_stubs/BRiFacyi0A60Y7ZZ.json b/credential_stubs/BRiFacyi0A60Y7ZZ.json deleted file mode 100644 index 36dd591..0000000 --- a/credential_stubs/BRiFacyi0A60Y7ZZ.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "BRiFacyi0A60Y7ZZ", - "name": "mam_id", - "type": "httpBasicAuth", - "data": { - "user": "", - "password": "" - }, - "ownedBy": { - "type": "personal", - "projectId": "FeLO36wNUAcn61Wj", - "projectName": "Ben W ", - "personalEmail": "admin@ben.io" - }, - "isGlobal": false -} \ No newline at end of file diff --git a/credential_stubs/dsnKfvOBMkgU21Lt.json b/credential_stubs/dsnKfvOBMkgU21Lt.json deleted file mode 100644 index 6311a8a..0000000 --- a/credential_stubs/dsnKfvOBMkgU21Lt.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "dsnKfvOBMkgU21Lt", - "name": "supabase postgres account", - "type": "postgres", - "data": { - "host": "", - "password": "" - }, - "ownedBy": { - "type": "personal", - "projectId": "FeLO36wNUAcn61Wj", - "projectName": "Ben W ", - "personalEmail": "admin@ben.io" - }, - "isGlobal": false -} \ No newline at end of file diff --git a/tags.json b/tags.json index 11cb464..1d9e68e 100644 --- a/tags.json +++ b/tags.json @@ -61,6 +61,30 @@ { "workflowId": "J3uKCCbSuQ1fdJkC", "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "WkAdUd9jXTtPagGO", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "6S41oPplwN1S9Lz0", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "NChjOd3ILEmt0FdyAt8qA", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "kRZyX9H2uDHHncpE", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "xXUnt2hL2FKxzOhBnkd3Z", + "tagId": "FydpKYmttDwoZVAA" + }, + { + "workflowId": "xXUnt2hL2FKxzOhBnkd3Z", + "tagId": "ct0Rtzpu15B497av" } ] } \ No newline at end of file diff --git a/workflows/3l7tJfcRoA1T1o6g.json b/workflows/3l7tJfcRoA1T1o6g.json index 0e9bcf1..d089bb4 100644 --- a/workflows/3l7tJfcRoA1T1o6g.json +++ b/workflows/3l7tJfcRoA1T1o6g.json @@ -601,6 +601,6 @@ "projectName": "Ben W ", "personalEmail": "admin@ben.io" }, - "parentFolderId": "of8yoeyjjIAhYdnQ", + "parentFolderId": "eWW72giJDI4fxlWw", "isArchived": false } \ No newline at end of file diff --git a/workflows/ERCWB3oSYbgsUiqL.json b/workflows/ERCWB3oSYbgsUiqL.json index 6a73f3a..5351f66 100644 --- a/workflows/ERCWB3oSYbgsUiqL.json +++ b/workflows/ERCWB3oSYbgsUiqL.json @@ -22,37 +22,6 @@ 400 ] }, - { - "parameters": { - "operation": "getAll", - "tableId": "npm_tokens", - "limit": 1, - "matchType": "allFilters", - "filters": { - "conditions": [ - { - "keyName": "service_name", - "condition": "eq", - "keyValue": "npm_dfw" - } - ] - } - }, - "id": "get-npm-token", - "name": "Get NPM Token", - "type": "n8n-nodes-base.supabase", - "typeVersion": 1, - "position": [ - 448, - 400 - ], - "credentials": { - "supabaseApi": { - "id": "lWyf2ikOGHTTwnSU", - "name": "Supabase account" - } - } - }, { "parameters": { "jsCode": "// Process SSL certificates and check expiration\nconst certificates = $input.all();\nconst today = new Date();\nconst WARNING_DAYS = 7;\n\nconsole.log('=== NPM SSL Certificate Monitor ===');\nconsole.log('Total certificates:', certificates.length);\n\nconst certificateStatus = [];\nlet expiringCerts = [];\nlet skippedCount = 0;\n\nfor (const item of certificates) {\n const cert = item.json;\n \n // Skip if no expiration date or invalid\n if (!cert.expires_on || cert.expires_on === 0) {\n console.log('Skipping certificate (no expiration):', cert.nice_name || cert.id);\n skippedCount++;\n continue;\n }\n \n const domainNames = cert.domain_names?.join(', ') || cert.nice_name || `Certificate ${cert.id}`;\n \n // Create date and validate it\n const expiryDate = new Date(cert.expires_on * 1000);\n if (isNaN(expiryDate.getTime())) {\n console.log('Skipping certificate (invalid date):', domainNames);\n skippedCount++;\n continue;\n }\n \n const daysRemaining = Math.ceil((expiryDate - today) / (1000 * 60 * 60 * 24));\n const isExpiring = daysRemaining <= WARNING_DAYS;\n \n let status = 'OK';\n if (daysRemaining < 0) {\n status = 'EXPIRED';\n } else if (isExpiring) {\n status = 'EXPIRING SOON';\n }\n \n const certInfo = {\n domain: domainNames,\n expiryDate: expiryDate.toISOString().split('T')[0],\n daysRemaining: daysRemaining,\n status: status,\n isExpiring: isExpiring\n };\n \n certificateStatus.push(certInfo);\n \n if (isExpiring) {\n expiringCerts.push(certInfo);\n console.log(`🔴 ${domainNames}: ${daysRemaining} days (expires ${certInfo.expiryDate})`);\n } else {\n console.log(`✅ ${domainNames}: ${daysRemaining} days`);\n }\n}\n\nconsole.log('\\n=== Summary ===');\nconsole.log('Valid certificates:', certificateStatus.length);\nconsole.log('Skipped (invalid/missing expiration):', skippedCount);\nconsole.log('Expiring within', WARNING_DAYS, 'days:', expiringCerts.length);\n\nreturn [{\n json: {\n totalCerts: certificateStatus.length,\n expiringCount: expiringCerts.length,\n hasExpiringCerts: expiringCerts.length > 0,\n allCertificates: certificateStatus,\n expiringCertificates: expiringCerts\n }\n}];" @@ -169,6 +138,45 @@ 640, 400 ] + }, + { + "parameters": { + "operation": "select", + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "table": { + "__rl": true, + "value": "npm_tokens", + "mode": "list", + "cachedResultName": "npm_tokens" + }, + "where": { + "values": [ + { + "column": "service_name", + "value": "npm_dfw" + } + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 448, + 400 + ], + "id": "18c7d30b-bd5e-450c-9800-4f883c641b1c", + "name": "Select rows from a table", + "credentials": { + "postgres": { + "id": "Ik8CFyap8ic2Md3M", + "name": "n8n-infra" + } + } } ], "connections": { @@ -176,18 +184,7 @@ "main": [ [ { - "node": "Get NPM Token", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get NPM Token": { - "main": [ - [ - { - "node": "Fetch Certificates", + "node": "Select rows from a table", "type": "main", "index": 0 } @@ -209,7 +206,7 @@ "main": [ [ { - "node": "Get NPM Token", + "node": "Select rows from a table", "type": "main", "index": 0 } @@ -248,6 +245,17 @@ } ] ] + }, + "Select rows from a table": { + "main": [ + [ + { + "node": "Fetch Certificates", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": { @@ -258,7 +266,7 @@ "executionOrder": "v1" }, "triggerCount": 1, - "versionId": "708d8124-5275-445c-92da-43b0f43e9826", + "versionId": "67bbccd4-8c5e-4cc5-8969-9460ed529b23", "owner": { "type": "personal", "projectId": "FeLO36wNUAcn61Wj", diff --git a/workflows/NChjOd3ILEmt0FdyAt8qA.json b/workflows/NChjOd3ILEmt0FdyAt8qA.json new file mode 100644 index 0000000..6855258 --- /dev/null +++ b/workflows/NChjOd3ILEmt0FdyAt8qA.json @@ -0,0 +1,185 @@ +{ + "id": "NChjOd3ILEmt0FdyAt8qA", + "name": "Transmission: Get Session", + "nodes": [ + { + "parameters": { + "method": "POST", + "url": "=http://{{ $json.server_id }}:9091/transmission/rpc", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendBody": true, + "specifyBody": "json", + "jsonBody": "={{ { method: 'session-get' } }}", + "options": { + "response": { + "response": { + "fullResponse": true, + "neverError": true + } + } + } + }, + "id": "4c14375e-3d69-46eb-b263-d333a775dc02", + "name": "Request Transmission Session", + "position": [ + -176, + 16 + ], + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "credentials": { + "httpBasicAuth": { + "id": "iymUPilnVhfL3h5D", + "name": "transmission" + } + } + }, + { + "parameters": { + "language": "pythonNative", + "pythonCode": "# Access the first item's JSON data\n# In n8n Python nodes, '_items' is the global list of input items\nfirst_item = _items[0]['json']\n\n# Get headers safely\nheaders = first_item.get('headers', {})\n\n# Extract session ID (check both lowercase and capitalized keys)\nsession_id = headers.get('x-transmission-session-id') or headers.get('X-Transmission-Session-Id')\n\nif not session_id:\n raise Exception('Transmission session header missing')\n\n# Return the formatted list with 'json' key\nreturn [{'json': {'session_id': session_id}}]" + }, + "id": "7880edb1-b575-4f9f-b744-987d22cd18a6", + "name": "Extract Session Header", + "position": [ + 48, + 16 + ], + "type": "n8n-nodes-base.code", + "typeVersion": 2 + }, + { + "parameters": { + "workflowInputs": { + "values": [ + { + "name": "server_id" + } + ] + } + }, + "type": "n8n-nodes-base.executeWorkflowTrigger", + "typeVersion": 1.1, + "position": [ + -624, + 112 + ], + "id": "f39637d4-bd93-470a-a704-7947450edfb5", + "name": "When Executed by Another Workflow" + }, + { + "parameters": { + "errorMessage": "Invalid server_name, must be a seed server." + }, + "type": "n8n-nodes-base.stopAndError", + "typeVersion": 1, + "position": [ + -176, + 208 + ], + "id": "1299a1c0-5147-4da1-8fab-1eb0fb696e68", + "name": "Stop and Error" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": false, + "leftValue": "", + "typeValidation": "strict", + "version": 3 + }, + "conditions": [ + { + "id": "d1351268-ab96-4a81-905a-336fd784da32", + "leftValue": "={{ $json.server_id }}", + "rightValue": "seed", + "operator": { + "type": "string", + "operation": "startsWith" + } + }, + { + "id": "c3d2d00a-ce5c-49cc-b468-4088623a773b", + "leftValue": "={{ $json.server_id }}", + "rightValue": "ben.io", + "operator": { + "type": "string", + "operation": "endsWith" + } + } + ], + "combinator": "and" + }, + "options": { + "ignoreCase": true + } + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.3, + "position": [ + -400, + 112 + ], + "id": "c31c8c1a-bba5-47b2-a7b8-e30b53ccb38d", + "name": "server name check" + } + ], + "connections": { + "Request Transmission Session": { + "main": [ + [ + { + "node": "Extract Session Header", + "type": "main", + "index": 0 + } + ] + ] + }, + "When Executed by Another Workflow": { + "main": [ + [ + { + "node": "server name check", + "type": "main", + "index": 0 + } + ] + ] + }, + "server name check": { + "main": [ + [ + { + "node": "Request Transmission Session", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Stop and Error", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "triggerCount": 0, + "versionId": "b1a4dc71-5217-4bc9-8f23-ec7b92a7d23a", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "6tDyZCwqELStb6Ik", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/kRZyX9H2uDHHncpE.json b/workflows/kRZyX9H2uDHHncpE.json index ab7b906..d1b12d4 100644 --- a/workflows/kRZyX9H2uDHHncpE.json +++ b/workflows/kRZyX9H2uDHHncpE.json @@ -24,13 +24,13 @@ "parameters": [ { "name": "X-Transmission-Session-Id", - "value": "={{ $json.session_id }}" + "value": "={{ $('Merge Session to Files').item.json.headers[\"X-Transmission-Session-Id\"] }}" } ] }, "sendBody": true, "specifyBody": "json", - "jsonBody": "={{ $json.transmission_body }}", + "jsonBody": "={\n \"method\": \"torrent-add\",\n \"arguments\": {\n \"metainfo\": \"{{ $('Extract from File').item.json.data }}\",\n \"download-dir\": \"/home/seed_/.transmission/downloads/audio\",\n \"paused\": true\n }\n}", "options": { "response": { "response": { @@ -42,7 +42,7 @@ "id": "http-add-torrent", "name": "Add Torrent to Transmission", "position": [ - 2192, + 1792, 128 ], "type": "n8n-nodes-base.httpRequest", @@ -62,106 +62,12 @@ "id": "code-extract-torrent-id", "name": "Extract Torrent ID", "position": [ - 2416, + 2016, 128 ], "type": "n8n-nodes-base.code", "typeVersion": 2 }, - { - "parameters": { - "amount": 30 - }, - "id": "wait-60s", - "name": "Wait 60 Seconds", - "position": [ - 2688, - 288 - ], - "type": "n8n-nodes-base.wait", - "typeVersion": 1.1, - "webhookId": "ae0f8400-3629-4965-989e-11e14a60a260" - }, - { - "parameters": { - "method": "POST", - "url": "http://seed-1.dfw.ben.io:9091/transmission/rpc", - "authentication": "genericCredentialType", - "genericAuthType": "httpBasicAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "X-Transmission-Session-Id", - "value": "={{ $json.session_id || '' }}" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ { method: 'torrent-get', arguments: { ids: [$json.torrent_id], fields: ['id','name','status','percentDone','files','downloadDir', 'hashString'] } } }}", - "options": {} - }, - "id": "http-check-status", - "name": "Check Torrent Status", - "position": [ - 2688, - 64 - ], - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "credentials": { - "httpBasicAuth": { - "id": "iymUPilnVhfL3h5D", - "name": "transmission" - } - }, - "onError": "continueRegularOutput" - }, - { - "parameters": { - "jsCode": "// Check if torrent download is complete\n// Transmission status codes: 0=stopped, 4=downloading, 6=seeding\nconst item = $input.first();\nconst response = item.json;\n\nif (item.error || !response.arguments || !response.arguments.torrents) {\n return [{\n json: {\n complete: false,\n status: 'error',\n error: item.error?.message || 'Failed to get torrent status',\n retry: true\n }\n }];\n}\n\nconst torrent = response.arguments.torrents[0];\n\nif (!torrent) {\n return [{\n json: {\n complete: false,\n status: 'error',\n error: 'Torrent not found in Transmission',\n retry: false\n }\n }];\n}\n\nconst isComplete = torrent.status === 6 || (torrent.status === 0 && torrent.percentDone === 1);\n\n// Get original input to preserve history_id\nconst waitNode = $('Wait 60 Seconds').last();\nconst originalInput = waitNode ? (waitNode.json.original_input || {}) : {};\nconst historyId = originalInput.history_id;\n\nif (isComplete) {\n const downloadDir = torrent.downloadDir || '/home/seed_/.transmission/downloads/audio';\n \n // Find largest file (likely the main audiobook) to handle multi-file/directory torrents\n let largestFile = null;\n let maxBytes = -1;\n \n if (torrent.files && torrent.files.length > 0) {\n for (const file of torrent.files) {\n if (file.length > maxBytes) {\n maxBytes = file.length;\n largestFile = file;\n }\n }\n }\n \n // Use largest file path if found, otherwise fallback to torrent name (single file)\n let relativePath = torrent.name;\n if (largestFile) {\n relativePath = largestFile.name;\n }\n \n const filePath = `${downloadDir}/${relativePath}`;\n \n return [{\n json: {\n complete: true,\n status: 'complete',\n file_path: filePath,\n filename: relativePath.split('/').pop(), // Just the filename part\n torrent_id: torrent.id,\n torrent_name: torrent.name,\n torrent_hash: torrent.hashString,\n percent_done: torrent.percentDone,\n transmission_status: torrent.status,\n history_id: historyId,\n debug_original_keys: Object.keys(originalInput),\n debug_wait_node_exists: !!waitNode\n }\n }];\n} else {\n return [{\n json: {\n complete: false,\n status: 'downloading',\n percent_done: torrent.percentDone,\n transmission_status: torrent.status,\n retry: true\n }\n }];\n}" - }, - "id": "code-check-completion", - "name": "Check If Complete", - "position": [ - 2912, - 64 - ], - "type": "n8n-nodes-base.code", - "typeVersion": 2 - }, - { - "parameters": { - "conditions": { - "conditions": [ - { - "id": "condition-1", - "leftValue": "={{ $json.complete }}", - "operator": { - "operation": "equals", - "type": "boolean" - }, - "rightValue": true - } - ], - "options": { - "caseSensitive": true, - "leftValue": "", - "typeValidation": "strict" - } - }, - "options": {} - }, - "id": "if-complete", - "name": "Is Download Complete?", - "position": [ - 3136, - 64 - ], - "type": "n8n-nodes-base.if", - "typeVersion": 2 - }, { "parameters": { "respondWith": "allIncomingItems", @@ -170,120 +76,21 @@ "id": "respond-complete", "name": "Respond Complete", "position": [ - 3360, - 16 + 2464, + 128 ], "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1 }, { "parameters": { - "assignments": { - "assignments": [ - { - "id": "keep-context", - "name": "torrent_id", - "type": "string", - "value": "={{ $('Extract Torrent ID').item.json.torrent_id }}" - }, - { - "id": "keep-hash", - "name": "torrent_hash", - "type": "string", - "value": "={{ $('Extract Torrent ID').item.json.torrent_hash }}" - }, - { - "id": "keep-original", - "name": "original_input", - "type": "object", - "value": "={{ $('Extract Torrent ID').item.json.original_input }}" - }, - { - "id": "keep-session", - "name": "session_id", - "type": "string", - "value": "={{ $json.session_id || '' }}" - } - ] - }, - "options": {} + "language": "pythonNative", + "pythonCode": "# 1. Access the input item\nitem = _items[0]['json']\n\n# 2. Extract the data we need\n# UPDATE: The input key is now 'dl_link', not 'download_link'\ndownload_link = item.get('dl_link')\nsession_id = item.get('session_id')\n\n# 3. Create the Transmission Payload\npayload = {\n \"method\": \"torrent-add\",\n \"arguments\": {\n \"filename\": download_link,\n \"download-dir\": \"/home/seed_/.transmission/downloads/audio\",\n \"paused\": True \n }\n}\n\n# 4. Return the formatted data\nreturn [{\n \"json\": {\n # UPDATE: Map 'title' correctly for any old nodes that need it\n \"title\": item.get('meta_title'),\n \n # NEW: Pass through all metadata for the Postgres Logger\n \"meta_title\": item.get('meta_title'),\n \"meta_series\": item.get('meta_series'),\n \"meta_book_number\": item.get('meta_book_number'),\n \"meta_author\": item.get('meta_author'),\n \"mam_id\": item.get('mam_id'),\n \"dl_link\": download_link,\n \n # Transmission Data\n \"transmission_body\": payload,\n \"headers\": {\n \"X-Transmission-Session-Id\": session_id\n }\n }\n}]" }, - "id": "loop-back", - "name": "Loop Back to Wait", - "position": [ - 3360, - 240 - ], - "type": "n8n-nodes-base.set", - "typeVersion": 3.4 - }, - { - "parameters": { - "method": "POST", - "url": "http://seed-1.dfw.ben.io:9091/transmission/rpc", - "authentication": "genericCredentialType", - "genericAuthType": "httpBasicAuth", - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ { method: 'session-get' } }}", - "options": { - "response": { - "response": { - "fullResponse": true, - "neverError": true - } - } - } - }, - "id": "http-request-session", - "name": "Request Transmission Session", + "type": "n8n-nodes-base.code", + "typeVersion": 2, "position": [ 896, - 192 - ], - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "credentials": { - "httpBasicAuth": { - "id": "iymUPilnVhfL3h5D", - "name": "transmission" - } - } - }, - { - "parameters": { - "jsCode": "const item = $input.first();\nconst data = item.json || {};\nconst headers = data.headers || {};\nconst sessionId = headers['x-transmission-session-id'] || headers['X-Transmission-Session-Id'];\n\nif (!sessionId) {\n throw new Error('Transmission session header missing');\n}\n\nreturn [{ json: { session_id: sessionId } }];" - }, - "id": "code-session-header", - "name": "Extract Session Header", - "position": [ - 1168, - 192 - ], - "type": "n8n-nodes-base.code", - "typeVersion": 2 - }, - { - "parameters": { - "jsCode": "// Loop over all items (books)\nreturn items.map(item => {\n // Get the Base64 file string safely\n const binaryData = item.binary.data.data;\n \n // Create the exact JSON body Transmission expects\n const payload = {\n method: \"torrent-add\",\n arguments: {\n metainfo: binaryData,\n \"download-dir\": \"/home/seed_/.transmission/downloads/audio\", // Using your seedbox path\n paused: true\n }\n };\n\n // Return it as a normal JSON field called 'transmission_body'\n return {\n json: {\n ...item.json,\n transmission_body: payload\n }\n };\n});" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1936, - 128 - ], - "id": "0b011613-50e9-4612-ac6e-1f6ac0e39ebb", - "name": "Code in JavaScript" - }, - { - "parameters": { - "jsCode": "// 1. Get the Session ID from the \"Extract\" node\n// (We use .first() because there is only one session ID)\nconst sessionNode = $('Extract Session Header').first();\n// Robustly find the ID whether it's in headers or root\nconst sessionId = sessionNode.json.headers ? sessionNode.json.headers['x-transmission-session-id'] : sessionNode.json.session_id;\n\n// 2. Get all Files from the \"Start\" node\nconst files = $('Start').all();\n\n// 3. Map the ID onto every file while PRESERVING the binary data\nreturn files.map(file => {\n return {\n json: {\n ...file.json,\n session_id: sessionId\n },\n binary: file.binary // <--- This is the critical part we were losing!\n };\n});" - }, - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1712, 128 ], "id": "d6d9f145-6b10-452f-aa08-2dc669c3b4d6", @@ -298,11 +105,321 @@ "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [ - 1488, + 672, 128 ], "id": "dde23141-d708-48e7-942f-08aa1d749189", "name": "Merge Session Context" + }, + { + "parameters": { + "url": "={{ $json.transmission_body.arguments.filename }}", + "authentication": "genericCredentialType", + "genericAuthType": "httpHeaderAuth", + "options": { + "response": { + "response": { + "fullResponse": true, + "responseFormat": "file" + } + } + } + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + 1120, + 128 + ], + "id": "acb7d649-a853-44e7-84e8-f82398f3008a", + "name": "MAM Get File HTTP Request", + "credentials": { + "httpBasicAuth": { + "id": "iymUPilnVhfL3h5D", + "name": "transmission" + }, + "httpHeaderAuth": { + "id": "G8eA8XeS9P5axwJd", + "name": "mam cookie auth header" + } + } + }, + { + "parameters": { + "operation": "binaryToPropery", + "options": {} + }, + "type": "n8n-nodes-base.extractFromFile", + "typeVersion": 1.1, + "position": [ + 1568, + 128 + ], + "id": "acb1b56b-ac9a-41ec-ad60-7ce983b8cdd7", + "name": "Extract from File" + }, + { + "parameters": { + "language": "pythonNative", + "pythonCode": "results = []\n\nfor item in _items:\n # 1. Get the binary data from the previous download node\n # n8n stores the file in the 'binary' key. The default property name is usually 'data'.\n # We need the 'data' field inside that, which contains the Base64 string.\n binary_ref = item.get('binary', {}).get('data', {})\n b64_string = binary_ref.get('data')\n\n if b64_string:\n # 2. Create the exact JSON body Transmission expects for file uploads\n # We use \"metainfo\" for the actual file content (Base64)\n payload = {\n \"method\": \"torrent-add\",\n \"arguments\": {\n \"metainfo\": b64_string, \n \"download-dir\": \"/home/seed_/.transmission/downloads/audio\",\n \"paused\": False\n }\n }\n\n # 3. Preserve any existing JSON (like session_id) if it exists, or start fresh\n new_json = item.get('json', {}).copy()\n new_json['transmission_body'] = payload\n \n results.append({\n \"json\": new_json,\n # Pass the binary through just in case, though we don't strictly need it anymore\n \"binary\": item.get('binary', {})\n })\n\nreturn results" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1344, + 128 + ], + "id": "0b011613-50e9-4612-ac6e-1f6ac0e39ebb", + "name": "Prepare Transmission Payload" + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "value": "NChjOd3ILEmt0FdyAt8qA", + "mode": "list", + "cachedResultUrl": "/workflow/NChjOd3ILEmt0FdyAt8qA", + "cachedResultName": "Transmission: Get Session" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "server_id": "seed-1.dfw.ben.io" + }, + "matchingColumns": [ + "server_id" + ], + "schema": [ + { + "id": "server_id", + "displayName": "server_id", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.3, + "position": [ + 448, + 200 + ], + "id": "175e270a-b1d9-41d0-981a-1f83258443b1", + "name": "Transmission: Get Session" + }, + { + "parameters": { + "schema": { + "__rl": true, + "mode": "list", + "value": "public" + }, + "table": { + "__rl": true, + "value": "download_history", + "mode": "list", + "cachedResultName": "download_history" + }, + "columns": { + "mappingMode": "defineBelow", + "value": { + "mam_id": "={{ $json.original_input.mam_id }}", + "torrent_url": "={{ $('Extract Torrent ID').item.json.original_input.dl_link }}", + "book_id": "={{ null }}", + "torrent_hash": "={{ $json.torrent_hash }}", + "filename": "={{ $('Extract Torrent ID').item.json.torrent_name }}", + "status": "={{ $json.status }}", + "meta_title": "={{ $json.original_input.meta_title }}", + "meta_series": "={{ $json.original_input.meta_series }}", + "meta_book_number": "={{ $json.original_input.meta_book_number }}", + "meta_author": "={{ $json.original_input.meta_author }}" + }, + "matchingColumns": [ + "id" + ], + "schema": [ + { + "id": "id", + "displayName": "id", + "required": false, + "defaultMatch": true, + "display": true, + "type": "string", + "canBeUsedToMatch": true, + "removed": false + }, + { + "id": "book_id", + "displayName": "book_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "torrent_url", + "displayName": "torrent_url", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "torrent_hash", + "displayName": "torrent_hash", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "filename", + "displayName": "filename", + "required": true, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "transmission_path", + "displayName": "transmission_path", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "gateway_path", + "displayName": "gateway_path", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "status", + "displayName": "status", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "downloaded_at", + "displayName": "downloaded_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": true + }, + { + "id": "created_at", + "displayName": "created_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": true + }, + { + "id": "updated_at", + "displayName": "updated_at", + "required": false, + "defaultMatch": false, + "display": true, + "type": "dateTime", + "canBeUsedToMatch": true + }, + { + "id": "meta_title", + "displayName": "meta_title", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "meta_series", + "displayName": "meta_series", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "meta_book_number", + "displayName": "meta_book_number", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "meta_author", + "displayName": "meta_author", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + }, + { + "id": "mam_id", + "displayName": "mam_id", + "required": false, + "defaultMatch": false, + "display": true, + "type": "number", + "canBeUsedToMatch": true + }, + { + "id": "error_log", + "displayName": "error_log", + "required": false, + "defaultMatch": false, + "display": true, + "type": "string", + "canBeUsedToMatch": true + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + }, + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 2240, + 128 + ], + "id": "5a79163a-a211-475c-b53e-79d086a4c9a7", + "name": "Insert rows in a table", + "credentials": { + "postgres": { + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media" + } + } } ], "connections": { @@ -317,80 +434,13 @@ ] ] }, - "Check If Complete": { - "main": [ - [ - { - "index": 0, - "node": "Is Download Complete?", - "type": "main" - } - ] - ] - }, - "Check Torrent Status": { - "main": [ - [ - { - "index": 0, - "node": "Check If Complete", - "type": "main" - } - ] - ] - }, - "Extract Session Header": { - "main": [ - [ - { - "node": "Merge Session Context", - "type": "main", - "index": 1 - } - ] - ] - }, "Extract Torrent ID": { - "main": [ - [] - ] - }, - "Is Download Complete?": { "main": [ [ { - "index": 0, - "node": "Respond Complete", - "type": "main" - } - ], - [ - { - "index": 0, - "node": "Loop Back to Wait", - "type": "main" - } - ] - ] - }, - "Loop Back to Wait": { - "main": [ - [ - { - "index": 0, - "node": "Wait 60 Seconds", - "type": "main" - } - ] - ] - }, - "Request Transmission Session": { - "main": [ - [ - { - "index": 0, - "node": "Extract Session Header", - "type": "main" + "node": "Insert rows in a table", + "type": "main", + "index": 0 } ] ] @@ -399,7 +449,7 @@ "main": [ [ { - "node": "Request Transmission Session", + "node": "Transmission: Get Session", "type": "main", "index": 0 }, @@ -411,33 +461,11 @@ ] ] }, - "Wait 60 Seconds": { - "main": [ - [ - { - "index": 0, - "node": "Check Torrent Status", - "type": "main" - } - ] - ] - }, - "Code in JavaScript": { - "main": [ - [ - { - "node": "Add Torrent to Transmission", - "type": "main", - "index": 0 - } - ] - ] - }, "Merge Session to Files": { "main": [ [ { - "node": "Code in JavaScript", + "node": "MAM Get File HTTP Request", "type": "main", "index": 0 } @@ -454,11 +482,66 @@ } ] ] + }, + "MAM Get File HTTP Request": { + "main": [ + [ + { + "node": "Prepare Transmission Payload", + "type": "main", + "index": 0 + } + ] + ] + }, + "Extract from File": { + "main": [ + [ + { + "node": "Add Torrent to Transmission", + "type": "main", + "index": 0 + } + ] + ] + }, + "Prepare Transmission Payload": { + "main": [ + [ + { + "node": "Extract from File", + "type": "main", + "index": 0 + } + ] + ] + }, + "Transmission: Get Session": { + "main": [ + [ + { + "node": "Merge Session Context", + "type": "main", + "index": 1 + } + ] + ] + }, + "Insert rows in a table": { + "main": [ + [ + { + "node": "Respond Complete", + "type": "main", + "index": 0 + } + ] + ] } }, "settings": {}, "triggerCount": 0, - "versionId": "ef553d09-89f5-4f19-9003-0f2040bf82ba", + "versionId": "509f1eb5-35d3-4d2a-829d-573deddc24a7", "owner": { "type": "personal", "projectId": "FeLO36wNUAcn61Wj", diff --git a/workflows/kn1gehxiWbkRfDFFAKx0x.json b/workflows/kn1gehxiWbkRfDFFAKx0x.json new file mode 100644 index 0000000..6bc2eaf --- /dev/null +++ b/workflows/kn1gehxiWbkRfDFFAKx0x.json @@ -0,0 +1,208 @@ +{ + "id": "kn1gehxiWbkRfDFFAKx0x", + "name": "MAM Check for Completed", + "nodes": [ + { + "parameters": { + "rule": { + "interval": [ + { + "field": "hours" + } + ] + } + }, + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.3, + "position": [ + -512, + -16 + ], + "id": "e621dd75-f5c0-48e6-816c-15f22fb500ba", + "name": "Schedule Trigger" + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "value": "NChjOd3ILEmt0FdyAt8qA", + "mode": "list", + "cachedResultUrl": "/workflow/NChjOd3ILEmt0FdyAt8qA", + "cachedResultName": "Transmission: Get Session" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "server_id": "seed-1.dfw.ben.io" + }, + "matchingColumns": [ + "server_id" + ], + "schema": [ + { + "id": "server_id", + "displayName": "server_id", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string", + "removed": false + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.3, + "position": [ + -304, + -16 + ], + "id": "10e82b7b-fdc4-4d95-a5e7-4df97bb700f7", + "name": "Call 'Transmission: Get Session'" + }, + { + "parameters": { + "method": "POST", + "url": "http://seed-1.dfw.ben.io:9091/transmission/rpc", + "authentication": "genericCredentialType", + "genericAuthType": "httpBasicAuth", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "X-Transmission-Session-Id", + "value": "={{ $json.session_id }}" + } + ] + }, + "sendBody": true, + "specifyBody": "json", + "jsonBody": "{\n \"method\": \"torrent-get\",\n \"arguments\": {\n \"fields\": [\n \"id\",\n \"name\",\n \"hashString\",\n \"percentDone\",\n \"isFinished\",\n \"downloadDir\",\n \"addedDate\" \n ]\n }\n}", + "options": {} + }, + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.3, + "position": [ + -96, + -16 + ], + "id": "c49a3e03-7fab-43d7-a430-4cbaa089ca2c", + "name": "HTTP Request", + "credentials": { + "httpBasicAuth": { + "id": "iymUPilnVhfL3h5D", + "name": "transmission" + } + } + }, + { + "parameters": { + "language": "pythonNative", + "pythonCode": "import time\n\n# Output list\nresults = []\n\n# Define your time window (2 hours in seconds to match your node name)\n# 2 * 60 * 60 = 7200\nTIME_WINDOW = 2 * 60 * 60\ncurrent_time = time.time()\n\n# 1. Get the list of torrents from the HTTP response\ninput_data = _items[0]['json']\ntorrents = input_data.get('arguments', {}).get('torrents', [])\n\nfor torrent in torrents:\n # 2. Check if download is 100% complete (1.0)\n is_downloaded = torrent.get('percentDone') == 1\n \n # 3. Filter Logic (Check if added recently)\n added_date = torrent.get('addedDate', 0)\n age_in_seconds = current_time - added_date\n \n # Optional: Uncomment this if you ALSO only want completed items\n # is_complete = torrent.get('percentDone') == 1\n\n if age_in_seconds <= TIME_WINDOW: # and is_complete:\n # 4. Return the FULL object\n # We pass the entire 'torrent' dictionary directly\n results.append({\n \"json\": torrent\n })\n\nreturn results" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 112, + -16 + ], + "id": "18ad2b6e-824b-4fac-8924-ddbf35f45579", + "name": "Filter for last 2 hours" + }, + { + "parameters": { + "operation": "select", + "schema": { + "__rl": true, + "value": "public", + "mode": "list", + "cachedResultName": "public" + }, + "table": { + "__rl": true, + "value": "smb_general_books", + "mode": "list", + "cachedResultName": "smb_general_books" + }, + "where": { + "values": [ + {} + ] + }, + "options": {} + }, + "type": "n8n-nodes-base.postgres", + "typeVersion": 2.6, + "position": [ + 384, + -112 + ], + "id": "0fa625e2-3e86-4abe-a837-f876d2d08f10", + "name": "Select rows from a table", + "credentials": { + "postgres": { + "id": "9grzZwW7Br6SzdV8", + "name": "n8n-media" + } + } + } + ], + "connections": { + "Schedule Trigger": { + "main": [ + [ + { + "node": "Call 'Transmission: Get Session'", + "type": "main", + "index": 0 + } + ] + ] + }, + "Call 'Transmission: Get Session'": { + "main": [ + [ + { + "node": "HTTP Request", + "type": "main", + "index": 0 + } + ] + ] + }, + "HTTP Request": { + "main": [ + [ + { + "node": "Filter for last 2 hours", + "type": "main", + "index": 0 + } + ] + ] + }, + "Filter for last 2 hours": { + "main": [ + [] + ] + } + }, + "settings": { + "executionOrder": "v1", + "availableInMCP": false + }, + "triggerCount": 0, + "versionId": "26ae4972-43fe-4776-b2ed-af9f7c88aa42", + "owner": { + "type": "personal", + "projectId": "FeLO36wNUAcn61Wj", + "projectName": "Ben W ", + "personalEmail": "admin@ben.io" + }, + "parentFolderId": "6tDyZCwqELStb6Ik", + "isArchived": false +} \ No newline at end of file diff --git a/workflows/xXUnt2hL2FKxzOhBnkd3Z.json b/workflows/xXUnt2hL2FKxzOhBnkd3Z.json index 676cc17..73e7acb 100644 --- a/workflows/xXUnt2hL2FKxzOhBnkd3Z.json +++ b/workflows/xXUnt2hL2FKxzOhBnkd3Z.json @@ -40,7 +40,7 @@ "genericAuthType": "httpHeaderAuth", "sendBody": true, "specifyBody": "json", - "jsonBody": "={\n \"tor\": {\n \"text\": \"{{ $json.series_name }} m4b\",\n \"srchIn\": [\n \"title\",\n \"author\",\n \"filenames\",\n \"fileTypes\"\n ],\n \"main_cat\": [\n \"13\"\n ],\n \"searchType\": \"all\",\n \"sortType\": \"seedersDesc\"\n }\n}", + "jsonBody": "={\n \"dlLink\": \"1\",\n \"tor\": {\n \"text\": \" {{ $json.series_name }} m4b\",\n \"srchIn\": [\n \"title\",\n \"author\",\n \"filenames\",\n \"fileTypes\"\n ],\n \"main_cat\": [\n \"13\"\n ],\n \"searchType\": \"all\",\n \"sortType\": \"seedersDesc\"\n }\n}", "options": {} }, "type": "n8n-nodes-base.httpRequest", @@ -91,7 +91,8 @@ 0 ], "id": "aef43627-3228-42ab-a2fe-9f4b50d1a855", - "name": "Basic LLM Chain" + "name": "Basic LLM Chain", + "onError": "continueErrorOutput" }, { "parameters": { @@ -113,12 +114,15 @@ "parameters": { "model": { "__rl": true, - "value": "gemini_cli/gemini-2.5-flash-lite", + "value": "gemini_cli/gemini-2.5-flash", "mode": "list", - "cachedResultName": "gemini_cli/gemini-2.5-flash-lite" + "cachedResultName": "gemini_cli/gemini-2.5-flash" }, "responsesApiEnabled": false, - "options": {} + "options": { + "responseFormat": "json_object", + "maxRetries": 2 + } }, "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "typeVersion": 1.3, @@ -141,6 +145,19 @@ "fieldToAggregate": [ { "fieldToAggregate": "data.title" + }, + { + "fieldToAggregate": "=data.dl", + "renameField": true + }, + { + "fieldToAggregate": "data.id" + }, + { + "fieldToAggregate": "data.series_info" + }, + { + "fieldToAggregate": "data.author_info" } ] }, @@ -228,6 +245,64 @@ "name": "n8n-media" } } + }, + { + "parameters": { + "mode": "combine", + "combineBy": "combineByPosition", + "options": {} + }, + "type": "n8n-nodes-base.merge", + "typeVersion": 3.2, + "position": [ + 1328, + 0 + ], + "id": "d8c36a24-7337-453f-b403-f5c631025096", + "name": "Merge" + }, + { + "parameters": { + "workflowId": { + "__rl": true, + "value": "kRZyX9H2uDHHncpE", + "mode": "list", + "cachedResultUrl": "/workflow/kRZyX9H2uDHHncpE", + "cachedResultName": "MAM Transmission Manager" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": {}, + "matchingColumns": [], + "schema": [], + "attemptToConvertTypes": false, + "convertFieldsToString": true + }, + "mode": "each", + "options": {} + }, + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.3, + "position": [ + 1728, + 0 + ], + "id": "b6ec81b3-4f62-494e-9437-1a31c82a0299", + "name": "Call 'MAM Transmission Manager'" + }, + { + "parameters": { + "language": "pythonNative", + "pythonCode": "# Output list\nresults = []\n\n# Access the input item\ninput_item = _items[0]['json']\n\n# Extract Lists\nmissing_books = input_item.get('data', [])\navailable_titles = input_item.get('title', [])\navailable_ids = input_item.get('id', [])\navailable_series_info = input_item.get('series_info', [])\navailable_author_info = input_item.get('author_info', [])\navailable_catnames = input_item.get('catname', []) # New: For category detection\navailable_tags = input_item.get('tags', []) # New: For category detection\n\n# 1. Match missing titles to their IDs & Metadata\nfor book_title in missing_books:\n # Normalize the target title\n target_clean = str(book_title).strip()\n \n # Find index\n found_index = -1\n for i, title in enumerate(available_titles):\n if str(title).strip() == target_clean:\n found_index = i\n break\n \n if found_index != -1:\n # Get raw data\n book_id = available_ids[found_index]\n series_raw = available_series_info[found_index]\n author_raw = available_author_info[found_index]\n catname_raw = available_catnames[found_index] if i < len(available_catnames) else \"\"\n tags_raw = available_tags[found_index] if i < len(available_tags) else \"\"\n \n # --- PARSING LOGIC ---\n \n # Parse Series\n meta_series = \"Unknown\"\n meta_book_num = \"0\"\n \n if isinstance(series_raw, str):\n import json\n try:\n series_raw = json.loads(series_raw)\n except:\n pass\n\n if isinstance(series_raw, dict):\n keys = list(series_raw.keys())\n if keys:\n data = series_raw[keys[0]]\n if isinstance(data, list) and len(data) >= 2:\n meta_series = str(data[0]).replace(''', \"'\")\n meta_book_num = str(data[1])\n\n # Parse Author\n meta_author = \"Unknown\"\n \n if isinstance(author_raw, str):\n import json\n try:\n author_raw = json.loads(author_raw)\n except:\n pass\n\n if isinstance(author_raw, dict):\n keys = list(author_raw.keys())\n if keys:\n meta_author = str(author_raw[keys[0]])\n\n # --- CATEGORY DETECTION ---\n # Check if \"Light Novel\" or \"Anime\" appears in category name or tags\n is_anime = \"Light Novel\" in str(catname_raw) or \"anime\" in str(tags_raw).lower()\n category = 'anime' if is_anime else 'general'\n\n # Construct download link\n full_url = f\"https://www.myanonamouse.net/tor/download.php?tid={book_id}\"\n \n results.append({\n \"json\": {\n # Metadata\n \"meta_title\": target_clean,\n \"meta_series\": meta_series,\n \"meta_book_number\": meta_book_num,\n \"meta_author\": meta_author,\n \"mam_id\": book_id,\n \"category\": category, # <--- Sending this to the DB now\n \n # Download Info\n \"dl_link\": full_url,\n \"torrent_hash\": \"PENDING\"\n }\n })\n\nreturn results" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 1520, + 0 + ], + "id": "c7496e8c-e307-4659-bdd0-530cd5feeef3", + "name": "Create Download Links" } ], "connections": { @@ -305,6 +380,11 @@ "node": "Select Book Titles", "type": "main", "index": 0 + }, + { + "node": "Merge", + "type": "main", + "index": 1 } ] ] @@ -332,6 +412,40 @@ ] }, "Basic LLM Chain": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + } + ], + [] + ] + }, + "Merge": { + "main": [ + [ + { + "node": "Create Download Links", + "type": "main", + "index": 0 + } + ] + ] + }, + "Create Download Links": { + "main": [ + [ + { + "node": "Call 'MAM Transmission Manager'", + "type": "main", + "index": 0 + } + ] + ] + }, + "Call 'MAM Transmission Manager'": { "main": [ [] ] @@ -341,8 +455,8 @@ "executionOrder": "v1", "availableInMCP": false }, - "triggerCount": 0, - "versionId": "b179028c-8bde-4e62-8183-1464046a348b", + "triggerCount": 1, + "versionId": "8a3af598-bd9e-48e1-a025-cb2059cfebbd", "owner": { "type": "personal", "projectId": "FeLO36wNUAcn61Wj",