clean up supabase, MAM automation continues.

This commit is contained in:
2026-01-08 15:15:02 +00:00
parent 17cbee3ab9
commit c0a38e8c8d
9 changed files with 974 additions and 384 deletions

View File

@@ -1,16 +0,0 @@
{
"id": "BRiFacyi0A60Y7ZZ",
"name": "mam_id",
"type": "httpBasicAuth",
"data": {
"user": "",
"password": ""
},
"ownedBy": {
"type": "personal",
"projectId": "FeLO36wNUAcn61Wj",
"projectName": "Ben W <admin@ben.io>",
"personalEmail": "admin@ben.io"
},
"isGlobal": false
}

View File

@@ -1,16 +0,0 @@
{
"id": "dsnKfvOBMkgU21Lt",
"name": "supabase postgres account",
"type": "postgres",
"data": {
"host": "",
"password": ""
},
"ownedBy": {
"type": "personal",
"projectId": "FeLO36wNUAcn61Wj",
"projectName": "Ben W <admin@ben.io>",
"personalEmail": "admin@ben.io"
},
"isGlobal": false
}

View File

@@ -61,6 +61,30 @@
{ {
"workflowId": "J3uKCCbSuQ1fdJkC", "workflowId": "J3uKCCbSuQ1fdJkC",
"tagId": "FydpKYmttDwoZVAA" "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"
} }
] ]
} }

View File

@@ -601,6 +601,6 @@
"projectName": "Ben W <admin@ben.io>", "projectName": "Ben W <admin@ben.io>",
"personalEmail": "admin@ben.io" "personalEmail": "admin@ben.io"
}, },
"parentFolderId": "of8yoeyjjIAhYdnQ", "parentFolderId": "eWW72giJDI4fxlWw",
"isArchived": false "isArchived": false
} }

View File

@@ -22,37 +22,6 @@
400 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": { "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}];" "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, 640,
400 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": { "connections": {
@@ -176,18 +184,7 @@
"main": [ "main": [
[ [
{ {
"node": "Get NPM Token", "node": "Select rows from a table",
"type": "main",
"index": 0
}
]
]
},
"Get NPM Token": {
"main": [
[
{
"node": "Fetch Certificates",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -209,7 +206,7 @@
"main": [ "main": [
[ [
{ {
"node": "Get NPM Token", "node": "Select rows from a table",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@@ -248,6 +245,17 @@
} }
] ]
] ]
},
"Select rows from a table": {
"main": [
[
{
"node": "Fetch Certificates",
"type": "main",
"index": 0
}
]
]
} }
}, },
"settings": { "settings": {
@@ -258,7 +266,7 @@
"executionOrder": "v1" "executionOrder": "v1"
}, },
"triggerCount": 1, "triggerCount": 1,
"versionId": "708d8124-5275-445c-92da-43b0f43e9826", "versionId": "67bbccd4-8c5e-4cc5-8969-9460ed529b23",
"owner": { "owner": {
"type": "personal", "type": "personal",
"projectId": "FeLO36wNUAcn61Wj", "projectId": "FeLO36wNUAcn61Wj",

View File

@@ -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 <admin@ben.io>",
"personalEmail": "admin@ben.io"
},
"parentFolderId": "6tDyZCwqELStb6Ik",
"isArchived": false
}

View File

@@ -24,13 +24,13 @@
"parameters": [ "parameters": [
{ {
"name": "X-Transmission-Session-Id", "name": "X-Transmission-Session-Id",
"value": "={{ $json.session_id }}" "value": "={{ $('Merge Session to Files').item.json.headers[\"X-Transmission-Session-Id\"] }}"
} }
] ]
}, },
"sendBody": true, "sendBody": true,
"specifyBody": "json", "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": { "options": {
"response": { "response": {
"response": { "response": {
@@ -42,7 +42,7 @@
"id": "http-add-torrent", "id": "http-add-torrent",
"name": "Add Torrent to Transmission", "name": "Add Torrent to Transmission",
"position": [ "position": [
2192, 1792,
128 128
], ],
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
@@ -62,106 +62,12 @@
"id": "code-extract-torrent-id", "id": "code-extract-torrent-id",
"name": "Extract Torrent ID", "name": "Extract Torrent ID",
"position": [ "position": [
2416, 2016,
128 128
], ],
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 2 "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": { "parameters": {
"respondWith": "allIncomingItems", "respondWith": "allIncomingItems",
@@ -170,120 +76,21 @@
"id": "respond-complete", "id": "respond-complete",
"name": "Respond Complete", "name": "Respond Complete",
"position": [ "position": [
3360, 2464,
16 128
], ],
"type": "n8n-nodes-base.respondToWebhook", "type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1 "typeVersion": 1
}, },
{ {
"parameters": { "parameters": {
"assignments": { "language": "pythonNative",
"assignments": [ "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": "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": {}
}, },
"id": "loop-back", "type": "n8n-nodes-base.code",
"name": "Loop Back to Wait", "typeVersion": 2,
"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",
"position": [ "position": [
896, 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 128
], ],
"id": "d6d9f145-6b10-452f-aa08-2dc669c3b4d6", "id": "d6d9f145-6b10-452f-aa08-2dc669c3b4d6",
@@ -298,11 +105,321 @@
"type": "n8n-nodes-base.merge", "type": "n8n-nodes-base.merge",
"typeVersion": 3.2, "typeVersion": 3.2,
"position": [ "position": [
1488, 672,
128 128
], ],
"id": "dde23141-d708-48e7-942f-08aa1d749189", "id": "dde23141-d708-48e7-942f-08aa1d749189",
"name": "Merge Session Context" "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": { "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": { "Extract Torrent ID": {
"main": [
[]
]
},
"Is Download Complete?": {
"main": [ "main": [
[ [
{ {
"index": 0, "node": "Insert rows in a table",
"node": "Respond Complete", "type": "main",
"type": "main" "index": 0
}
],
[
{
"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"
} }
] ]
] ]
@@ -399,7 +449,7 @@
"main": [ "main": [
[ [
{ {
"node": "Request Transmission Session", "node": "Transmission: Get Session",
"type": "main", "type": "main",
"index": 0 "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": { "Merge Session to Files": {
"main": [ "main": [
[ [
{ {
"node": "Code in JavaScript", "node": "MAM Get File HTTP Request",
"type": "main", "type": "main",
"index": 0 "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": {}, "settings": {},
"triggerCount": 0, "triggerCount": 0,
"versionId": "ef553d09-89f5-4f19-9003-0f2040bf82ba", "versionId": "509f1eb5-35d3-4d2a-829d-573deddc24a7",
"owner": { "owner": {
"type": "personal", "type": "personal",
"projectId": "FeLO36wNUAcn61Wj", "projectId": "FeLO36wNUAcn61Wj",

View File

@@ -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 <admin@ben.io>",
"personalEmail": "admin@ben.io"
},
"parentFolderId": "6tDyZCwqELStb6Ik",
"isArchived": false
}

View File

@@ -40,7 +40,7 @@
"genericAuthType": "httpHeaderAuth", "genericAuthType": "httpHeaderAuth",
"sendBody": true, "sendBody": true,
"specifyBody": "json", "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": {} "options": {}
}, },
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",
@@ -91,7 +91,8 @@
0 0
], ],
"id": "aef43627-3228-42ab-a2fe-9f4b50d1a855", "id": "aef43627-3228-42ab-a2fe-9f4b50d1a855",
"name": "Basic LLM Chain" "name": "Basic LLM Chain",
"onError": "continueErrorOutput"
}, },
{ {
"parameters": { "parameters": {
@@ -113,12 +114,15 @@
"parameters": { "parameters": {
"model": { "model": {
"__rl": true, "__rl": true,
"value": "gemini_cli/gemini-2.5-flash-lite", "value": "gemini_cli/gemini-2.5-flash",
"mode": "list", "mode": "list",
"cachedResultName": "gemini_cli/gemini-2.5-flash-lite" "cachedResultName": "gemini_cli/gemini-2.5-flash"
}, },
"responsesApiEnabled": false, "responsesApiEnabled": false,
"options": {} "options": {
"responseFormat": "json_object",
"maxRetries": 2
}
}, },
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi", "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3, "typeVersion": 1.3,
@@ -141,6 +145,19 @@
"fieldToAggregate": [ "fieldToAggregate": [
{ {
"fieldToAggregate": "data.title" "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" "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('&#039;', \"'\")\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": { "connections": {
@@ -305,6 +380,11 @@
"node": "Select Book Titles", "node": "Select Book Titles",
"type": "main", "type": "main",
"index": 0 "index": 0
},
{
"node": "Merge",
"type": "main",
"index": 1
} }
] ]
] ]
@@ -332,6 +412,40 @@
] ]
}, },
"Basic LLM Chain": { "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": [ "main": [
[] []
] ]
@@ -341,8 +455,8 @@
"executionOrder": "v1", "executionOrder": "v1",
"availableInMCP": false "availableInMCP": false
}, },
"triggerCount": 0, "triggerCount": 1,
"versionId": "b179028c-8bde-4e62-8183-1464046a348b", "versionId": "8a3af598-bd9e-48e1-a025-cb2059cfebbd",
"owner": { "owner": {
"type": "personal", "type": "personal",
"projectId": "FeLO36wNUAcn61Wj", "projectId": "FeLO36wNUAcn61Wj",