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

@@ -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",