[
  {
    "id": "kadrenta-tab-01",
    "type": "tab",
    "label": "Kadrenta delivery",
    "disabled": false,
    "info": "Sends one local file to Kadrenta as a delivery and prints back the share link.\n\nSet KADRENTA_API_KEY (your kad_up_… delivery key) as an environment variable on this flow or on the Node-RED process before deploying. KADRENTA_BASE_URL defaults to https://app.kadrenta.com and only needs setting for a different deployment.\n\nSee https://app.kadrenta.com/docs/api for the full reference, the error codes, and how to adapt this for a file large enough to need uploading in parts.",
    "env": [
      {
        "name": "KADRENTA_API_KEY",
        "type": "str",
        "value": ""
      },
      {
        "name": "KADRENTA_BASE_URL",
        "type": "str",
        "value": "https://app.kadrenta.com"
      }
    ]
  },
  {
    "id": "kadrenta-comment-01",
    "type": "comment",
    "z": "kadrenta-tab-01",
    "name": "Kadrenta: send a file",
    "info": "Five-step import, from https://app.kadrenta.com/docs/api:\n1. Download this file.\n2. Node-RED menu (top right) → Import.\n3. Choose this file, or paste its contents, and press Import.\n4. Open this new tab, set KADRENTA_API_KEY on it (tab properties → Environment Variables), or set it as an environment variable on the Node-RED process.\n5. Edit the inject node's filePath (and fileName), deploy, and press it.\n\nThis flow sends the whole file in one PUT, which is fine for anything up to a few dozen megabytes. For a large file, replace the single 'Upload bytes' step with a loop over POST <base>/v1/transfers/:id/parts, the way the curl examples on the docs page show — the file stays the same, only the sending changes.",
    "x": 160,
    "y": 40,
    "wires": []
  },
  {
    "id": "kadrenta-inject-01",
    "type": "inject",
    "z": "kadrenta-tab-01",
    "name": "Set input",
    "props": [
      { "p": "payload" },
      { "p": "filePath", "v": "/path/to/render.mov", "vt": "str" },
      { "p": "fileName", "v": "render.mov", "vt": "str" }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 150,
    "y": 120,
    "wires": [["kadrenta-file-01"]]
  },
  {
    "id": "kadrenta-file-01",
    "type": "file in",
    "z": "kadrenta-tab-01",
    "name": "Read the file",
    "filename": "filePath",
    "filenameType": "msg",
    "format": "",
    "chunk": false,
    "sendError": true,
    "encoding": "none",
    "allProps": false,
    "x": 350,
    "y": 120,
    "wires": [["kadrenta-fn-open-01"]]
  },
  {
    "id": "kadrenta-fn-open-01",
    "type": "function",
    "z": "kadrenta-tab-01",
    "name": "Build: open delivery",
    "func": "// msg.payload is the whole file as a Buffer, from the file-in node.\nconst key = env.get(\"KADRENTA_API_KEY\");\nconst baseUrl = (env.get(\"KADRENTA_BASE_URL\") || \"https://app.kadrenta.com\").replace(/\\/$/, \"\");\nif (!key) {\n    node.error(\"Set KADRENTA_API_KEY on this flow's environment variables first.\", msg);\n    return null;\n}\n\nconst fileName = msg.fileName || (msg.filename || \"\").split(/[\\\\/]/).pop();\nconst bytes = Buffer.isBuffer(msg.payload) ? msg.payload.length : 0;\n\n// The file's bytes travel with this one message through the whole flow, keyed\n// by its own _msgid so a second, concurrent run cannot cross wires with this\n// one - flow context is shared by every message passing through this tab.\nflow.set(\"kadrenta_bytes_\" + msg._msgid, msg.payload);\nflow.set(\"kadrenta_name_\" + msg._msgid, fileName);\n\nmsg.url = baseUrl + \"/v1/transfers\";\nmsg.method = \"POST\";\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + key,\n    \"Content-Type\": \"application/json\",\n};\nmsg.payload = {\n    name: fileName,\n    expiresInDays: 7,\n    idempotencyKey: \"node-red-\" + msg._msgid,\n    files: [{ filename: fileName, bytes: bytes }],\n};\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 380,
    "y": 200,
    "wires": [["kadrenta-http-open-01"]]
  },
  {
    "id": "kadrenta-http-open-01",
    "type": "http request",
    "z": "kadrenta-tab-01",
    "name": "POST /v1/transfers",
    "method": "use",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 400,
    "y": 280,
    "wires": [["kadrenta-fn-put-01"]]
  },
  {
    "id": "kadrenta-fn-put-01",
    "type": "function",
    "z": "kadrenta-tab-01",
    "name": "Build: upload bytes",
    "func": "// msg.payload is now the answer from opening the delivery: the file's own\n// upload target, plus completeUrl for the last step.\nconst opened = msg.payload;\nconst file = opened.files && opened.files[0];\nif (!file || !file.upload) {\n    node.error(\"Open the delivery did not answer with an upload target - see the message field on the response.\", msg);\n    return null;\n}\n\nconst fileBuffer = flow.get(\"kadrenta_bytes_\" + msg._msgid);\n\n// Kept for the next step; the delivery's own id and completeUrl are the two\n// things nothing after this point can read back out of msg.payload.\nflow.set(\"kadrenta_complete_url_\" + msg._msgid, opened.completeUrl);\n\nmsg.url = file.upload.url;\nmsg.method = file.upload.method || \"PUT\";\nmsg.headers = {};\nmsg.payload = fileBuffer;\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 400,
    "y": 360,
    "wires": [["kadrenta-http-put-01"]]
  },
  {
    "id": "kadrenta-http-put-01",
    "type": "http request",
    "z": "kadrenta-tab-01",
    "name": "PUT the bytes",
    "method": "use",
    "ret": "txt",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 380,
    "y": 440,
    "wires": [["kadrenta-fn-complete-01"]]
  },
  {
    "id": "kadrenta-fn-complete-01",
    "type": "function",
    "z": "kadrenta-tab-01",
    "name": "Build: close the delivery",
    "func": "const key = env.get(\"KADRENTA_API_KEY\");\nconst completeUrl = flow.get(\"kadrenta_complete_url_\" + msg._msgid);\nconst baseUrl = (env.get(\"KADRENTA_BASE_URL\") || \"https://app.kadrenta.com\").replace(/\\/$/, \"\");\n\nmsg.url = completeUrl.startsWith(\"http\") ? completeUrl : baseUrl + completeUrl;\nmsg.method = \"POST\";\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + key,\n    \"Content-Type\": \"application/json\",\n};\n// Empty body: the close call asks storage which upload is standing on each\n// file's key by itself, so there is nothing this flow has to remember.\nmsg.payload = {};\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 390,
    "y": 520,
    "wires": [["kadrenta-http-complete-01"]]
  },
  {
    "id": "kadrenta-http-complete-01",
    "type": "http request",
    "z": "kadrenta-tab-01",
    "name": "POST .../complete",
    "method": "use",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 400,
    "y": 600,
    "wires": [["kadrenta-fn-cleanup-01"]]
  },
  {
    "id": "kadrenta-fn-cleanup-01",
    "type": "function",
    "z": "kadrenta-tab-01",
    "name": "Extract the link",
    "func": "flow.set(\"kadrenta_bytes_\" + msg._msgid, undefined);\nflow.set(\"kadrenta_name_\" + msg._msgid, undefined);\nflow.set(\"kadrenta_complete_url_\" + msg._msgid, undefined);\n\nconst closed = msg.payload;\nmsg.payload = {\n    link: closed.link,\n    expiresAt: closed.expiresAt,\n    bytes: closed.bytes,\n};\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 380,
    "y": 680,
    "wires": [["kadrenta-debug-01"]]
  },
  {
    "id": "kadrenta-debug-01",
    "type": "debug",
    "z": "kadrenta-tab-01",
    "name": "Link",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "statusVal": "",
    "statusType": "auto",
    "x": 380,
    "y": 760,
    "wires": []
  },
  {
    "id": "kadrenta-tab-02",
    "type": "tab",
    "label": "Kadrenta: render to shot version",
    "disabled": false,
    "info": "Lands a render straight on a job's file dock and hangs it under its shot as the next version, reading the shot NUMBER out of the render's own filename (BICPEN_010_v003.mp4 -> shot 010) rather than a uuid copied out of a browser - added 28-09-2026, see https://app.kadrenta.com/docs/api#review.\n\nUses the same KADRENTA_API_KEY as the 'Kadrenta delivery' tab, and needs the key made with the review reach ('Bin, review drafts and shot versions'), early access. Set KADRENTA_PROJECT_ID to the job's id - GET /v1/projects lists it once if you only have the name.\n\nIf your studio runs more than one breakdown on the same job (a main cut alongside an R&D pass, say) and both use the same shot number, the last function node's request answers 409 ambiguous_shot_number with the candidate breakdowns; add film_id there once you know which one.",
    "env": [
      {
        "name": "KADRENTA_PROJECT_ID",
        "type": "str",
        "value": ""
      }
    ]
  },
  {
    "id": "kadrenta-comment-02",
    "type": "comment",
    "z": "kadrenta-tab-02",
    "name": "Kadrenta: render to shot version, by number",
    "info": "Same import steps as the 'Kadrenta delivery' tab. Set KADRENTA_API_KEY on the 'Kadrenta delivery' tab (shared across both), and KADRENTA_PROJECT_ID on this tab's own Environment Variables. Edit the inject node's filePath, deploy, and press it.\n\nThis flow: opens a drop on the job -> PUTs the bytes -> lands the file in the dock -> hangs it under the shot the filename names, as the next version. The filename is only ever read for the number; nothing about which shot this is comes from the name itself once the call is made - the number resolves to an id on Kadrenta's side.",
    "x": 160,
    "y": 40,
    "wires": []
  },
  {
    "id": "kadrenta-inject-02",
    "type": "inject",
    "z": "kadrenta-tab-02",
    "name": "Set input",
    "props": [
      { "p": "payload" },
      { "p": "filePath", "v": "/path/to/renders/BICPEN_010_v003.mp4", "vt": "str" },
      { "p": "fileName", "v": "BICPEN_010_v003.mp4", "vt": "str" }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 150,
    "y": 120,
    "wires": [["kadrenta-file-02"]]
  },
  {
    "id": "kadrenta-file-02",
    "type": "file in",
    "z": "kadrenta-tab-02",
    "name": "Read the render",
    "filename": "filePath",
    "filenameType": "msg",
    "format": "",
    "chunk": false,
    "sendError": true,
    "encoding": "none",
    "allProps": false,
    "x": 360,
    "y": 120,
    "wires": [["kadrenta-fn-open-02"]]
  },
  {
    "id": "kadrenta-fn-open-02",
    "type": "function",
    "z": "kadrenta-tab-02",
    "name": "Build: open drop, parse shot number",
    "func": "// msg.payload is the whole render as a Buffer, from the file-in node.\nconst key = env.get(\"KADRENTA_API_KEY\");\nconst projectId = env.get(\"KADRENTA_PROJECT_ID\");\nconst baseUrl = (env.get(\"KADRENTA_BASE_URL\") || \"https://app.kadrenta.com\").replace(/\\/$/, \"\");\nif (!key) {\n    node.error(\"Set KADRENTA_API_KEY on the 'Kadrenta delivery' tab's environment variables first.\", msg);\n    return null;\n}\nif (!projectId) {\n    node.error(\"Set KADRENTA_PROJECT_ID on this tab's environment variables. GET /v1/projects lists a job's id by name.\", msg);\n    return null;\n}\n\nconst fileName = msg.fileName || (msg.filename || \"\").split(/[\\\\/]/).pop();\n// A plain three-or-four-digit number in the filename - the same shape\n// formatShotNumber prints (010, 020, ... 990). Adjust this to match your own\n// studio's render naming if it differs.\nconst match = fileName.match(/(?:^|_)(\\d{3,4})(?:_|\\.)/);\nif (!match) {\n    node.error(\"No shot number found in '\" + fileName + \"' - expected something like BICPEN_010_v003.mp4.\", msg);\n    return null;\n}\nconst shotNumber = parseInt(match[1], 10);\n\nconst bytes = Buffer.isBuffer(msg.payload) ? msg.payload.length : 0;\n\n// The file's bytes and the parsed shot number travel with this one message\n// through the whole flow, keyed by its own _msgid - flow context is shared by\n// every message passing through this tab.\nflow.set(\"kadrenta_bytes_\" + msg._msgid, msg.payload);\nflow.set(\"kadrenta_shotnumber_\" + msg._msgid, shotNumber);\n\nmsg.url = baseUrl + \"/v1/projects/\" + projectId + \"/files\";\nmsg.method = \"POST\";\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + key,\n    \"Content-Type\": \"application/json\",\n};\nmsg.payload = { files: [{ filename: fileName, bytes: bytes }] };\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 420,
    "y": 200,
    "wires": [["kadrenta-http-open-02"]]
  },
  {
    "id": "kadrenta-http-open-02",
    "type": "http request",
    "z": "kadrenta-tab-02",
    "name": "POST /v1/projects/:id/files",
    "method": "use",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 440,
    "y": 280,
    "wires": [["kadrenta-fn-put-02"]]
  },
  {
    "id": "kadrenta-fn-put-02",
    "type": "function",
    "z": "kadrenta-tab-02",
    "name": "Build: upload bytes",
    "func": "// msg.payload is now the answer from opening the drop: the file's own\n// upload target, plus completeUrl for landing it in the dock.\nconst opened = msg.payload;\nconst file = opened.files && opened.files[0];\nif (!file || !file.upload) {\n    node.error(\"Opening the drop did not answer with an upload target - see the message field on the response.\", msg);\n    return null;\n}\n\nconst fileBuffer = flow.get(\"kadrenta_bytes_\" + msg._msgid);\nflow.set(\"kadrenta_file_id_\" + msg._msgid, file.id);\nflow.set(\"kadrenta_complete_url_\" + msg._msgid, opened.completeUrl);\n\nmsg.url = file.upload.url;\nmsg.method = file.upload.method || \"PUT\";\nmsg.headers = {};\nmsg.payload = fileBuffer;\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 420,
    "y": 360,
    "wires": [["kadrenta-http-put-02"]]
  },
  {
    "id": "kadrenta-http-put-02",
    "type": "http request",
    "z": "kadrenta-tab-02",
    "name": "PUT the bytes",
    "method": "use",
    "ret": "txt",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 400,
    "y": 440,
    "wires": [["kadrenta-fn-complete-02"]]
  },
  {
    "id": "kadrenta-fn-complete-02",
    "type": "function",
    "z": "kadrenta-tab-02",
    "name": "Build: land in the dock",
    "func": "const key = env.get(\"KADRENTA_API_KEY\");\nconst completeUrl = flow.get(\"kadrenta_complete_url_\" + msg._msgid);\nconst fileId = flow.get(\"kadrenta_file_id_\" + msg._msgid);\nconst baseUrl = (env.get(\"KADRENTA_BASE_URL\") || \"https://app.kadrenta.com\").replace(/\\/$/, \"\");\n\nmsg.url = completeUrl.startsWith(\"http\") ? completeUrl : baseUrl + completeUrl;\nmsg.method = \"POST\";\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + key,\n    \"Content-Type\": \"application/json\",\n};\nmsg.payload = { files: [{ fileId: fileId, folder: null }] };\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 410,
    "y": 520,
    "wires": [["kadrenta-http-complete-02"]]
  },
  {
    "id": "kadrenta-http-complete-02",
    "type": "http request",
    "z": "kadrenta-tab-02",
    "name": "POST .../files/complete",
    "method": "use",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 420,
    "y": 600,
    "wires": [["kadrenta-fn-version-02"]]
  },
  {
    "id": "kadrenta-fn-version-02",
    "type": "function",
    "z": "kadrenta-tab-02",
    "name": "Build: shot version by number",
    "func": "const key = env.get(\"KADRENTA_API_KEY\");\nconst projectId = env.get(\"KADRENTA_PROJECT_ID\");\nconst baseUrl = (env.get(\"KADRENTA_BASE_URL\") || \"https://app.kadrenta.com\").replace(/\\/$/, \"\");\nconst shotNumber = flow.get(\"kadrenta_shotnumber_\" + msg._msgid);\n\nconst landed = msg.payload;\nconst landedFile = landed.files && landed.files[0];\nif (!landedFile) {\n    node.error(\"Landing the file did not answer with a landed file - see the message field on the response.\", msg);\n    return null;\n}\n\nflow.set(\"kadrenta_bytes_\" + msg._msgid, undefined);\nflow.set(\"kadrenta_file_id_\" + msg._msgid, undefined);\nflow.set(\"kadrenta_complete_url_\" + msg._msgid, undefined);\nflow.set(\"kadrenta_shotnumber_\" + msg._msgid, undefined);\n\nmsg.url = baseUrl + \"/v1/projects/\" + projectId + \"/shots/by-number/\" + shotNumber + \"/versions\";\nmsg.method = \"POST\";\nmsg.headers = {\n    \"Authorization\": \"Bearer \" + key,\n    \"Content-Type\": \"application/json\",\n};\n// Add film_id here (from a 409 ambiguous_shot_number answer's candidates)\n// if this job runs more than one breakdown sharing the same shot number.\nmsg.payload = { media_file_id: landedFile.id };\nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 420,
    "y": 680,
    "wires": [["kadrenta-http-version-02"]]
  },
  {
    "id": "kadrenta-http-version-02",
    "type": "http request",
    "z": "kadrenta-tab-02",
    "name": "POST .../shots/by-number/:n/versions",
    "method": "use",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "",
    "tls": "",
    "persist": false,
    "proxy": "",
    "insecureHTTPParser": false,
    "authType": "",
    "senderr": false,
    "headers": [],
    "x": 440,
    "y": 760,
    "wires": [["kadrenta-debug-02"]]
  },
  {
    "id": "kadrenta-debug-02",
    "type": "debug",
    "z": "kadrenta-tab-02",
    "name": "Shot version",
    "active": true,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "payload",
    "targetType": "msg",
    "statusVal": "",
    "statusType": "auto",
    "x": 420,
    "y": 840,
    "wires": []
  }
]
