ig-poster: 69e395f2 + sync IMMICH_PG_* via ESO for CLIP scoring; postiz publish-notify n8n workflow

This commit is contained in:
Viktor Barzin 2026-05-09 13:16:24 +00:00
parent cb83972b79
commit 29bb434e1e
60 changed files with 419 additions and 113 deletions

View file

@ -15,7 +15,7 @@
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [250, 300],
"notes": "Trigger every 30 minutes."
"notes": "Trigger every 30 minutes. Polling cadence is conservative for a personal pipeline; matches instagram-poster scan rate."
},
{
"parameters": {
@ -24,6 +24,7 @@
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"},
{"name": "Content-Type", "value": "application/json"}
]
},
@ -35,7 +36,7 @@
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [500, 300],
"notes": "POST /scan returns {\"new_items\": [\"asset-uuid\", ...]}. Internal cluster service, no auth."
"notes": "Calls instagram-poster /scan endpoint. Returns {\"new_items\": [\"asset-uuid\", ...]}. Authorization header is optional (internal cluster service); leave INSTAGRAM_POSTER_TOKEN blank if unused."
},
{
"parameters": {
@ -47,7 +48,7 @@
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [750, 300],
"notes": "Fan out one candidate per execution branch."
"notes": "Fan out one candidate per execution branch so each photo gets its own approval message."
},
{
"parameters": {
@ -59,35 +60,70 @@
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [970, 300],
"notes": "Process one asset at a time so a single bad photo doesn't fan out."
"notes": "Process one asset at a time so errors on a single asset don't fan out and spam Telegram."
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.IMMICH_BASE_URL }}/api/assets/{{ $json.new_items }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "x-api-key", "value": "={{ $env.IMMICH_API_KEY }}"},
{"name": "Accept", "value": "application/json"}
]
},
"options": {"timeout": 30000}
},
"id": "fetch-asset-meta",
"name": "Fetch Immich asset metadata",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1190, 300],
"notes": "Pull asset metadata from Immich for caption preview (filename, fileCreatedAt, EXIF location)."
},
{
"parameters": {
"jsCode": "const item = $input.first().json;\nconst assetId = item.id || $('Loop one at a time').item.json.new_items;\nconst filename = item.originalFileName || item.originalPath || 'unknown';\nconst createdAt = item.fileCreatedAt || item.localDateTime || '';\nconst exif = item.exifInfo || {};\nconst city = exif.city || '';\nconst country = exif.country || '';\nconst location = [city, country].filter(Boolean).join(', ');\n\nconst dateStr = createdAt ? new Date(createdAt).toISOString().slice(0, 10) : '';\n\nconst lines = [\n '<b>New Immich candidate</b>',\n '',\n '<code>' + assetId + '</code>',\n '',\n '<b>File:</b> ' + filename\n];\nif (dateStr) lines.push('<b>Taken:</b> ' + dateStr);\nif (location) lines.push('<b>Where:</b> ' + location);\nlines.push('');\nlines.push('Approve to enqueue for posting, reject to mark seen.');\n\nreturn [{ json: { asset_id: assetId, caption: lines.join('\\n') } }];"
},
"id": "build-caption",
"name": "Build caption + asset id",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [1410, 300],
"notes": "Compose the caption from Immich metadata. HTML parse_mode is used downstream."
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.INSTAGRAM_POSTER_INTERNAL_URL }}/deliver/{{ $json.new_items }}",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendPhoto",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "Content-Type", "value": "application/json"}
]
},
"sendBody": false,
"options": {"timeout": 120000}
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $env.TELEGRAM_CHAT_ID, photo: $env.PUBLIC_INSTAGRAM_POSTER_URL + '/image/' + $json.asset_id, caption: $json.caption, parse_mode: 'HTML', reply_markup: { inline_keyboard: [[ { text: 'Approve', callback_data: 'approve:' + $json.asset_id }, { text: 'Reject', callback_data: 'reject:' + $json.asset_id } ]] } }) }}",
"options": {"timeout": 30000}
},
"id": "deliver",
"name": "Deliver via Telegram + tag posted",
"id": "telegram-send-photo",
"name": "Telegram sendPhoto with buttons",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [1190, 300],
"notes": "Single-shot endpoint: instagram-poster fetches the original from Immich, converts HEIC->JPEG full-quality, multipart-uploads to Telegram chat, then tags the asset 'posted' in Immich and flips queue row to posted. Telegram URL-fetch fails through Cloudflare for >5MB files, so we push bytes directly from inside the cluster."
"position": [1630, 300],
"notes": "Telegram needs a public URL for the photo (it fetches the image from outside the cluster). callback_data uses the action:asset_id format consumed by instagram-approval."
}
],
"connections": {
"Every 30 minutes": {"main": [[{"node": "Scan Immich for new candidates", "type": "main", "index": 0}]]},
"Scan Immich for new candidates": {"main": [[{"node": "Split new_items array", "type": "main", "index": 0}]]},
"Split new_items array": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]},
"Loop one at a time": {"main": [[{"node": "Deliver via Telegram + tag posted", "type": "main", "index": 0}]]},
"Deliver via Telegram + tag posted": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]}
"Loop one at a time": {"main": [[{"node": "Fetch Immich asset metadata", "type": "main", "index": 0}]]},
"Fetch Immich asset metadata": {"main": [[{"node": "Build caption + asset id", "type": "main", "index": 0}]]},
"Build caption + asset id": {"main": [[{"node": "Telegram sendPhoto with buttons", "type": "main", "index": 0}]]},
"Telegram sendPhoto with buttons": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]}
},
"settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true},
"staticData": null,

View file

@ -0,0 +1,64 @@
{
"name": "Postiz Publish Notify",
"active": true,
"id": "9c1b3d76-4e2a-4f8b-b1d5-2a9c4e3d7f01",
"versionId": "9c1b3d76-4e2a-4f8b-b1d5-2a9c4e3d7f01",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "postiz-publish",
"responseMode": "onReceived",
"options": {}
},
"id": "postiz-webhook",
"name": "Postiz webhook (publish)",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [250, 300],
"webhookId": "9c1b3d76-postiz-publish",
"notes": "Postiz fires this webhook AFTER a successful publish (post.workflow.v1.0.2.js -> sendWebhooks). Body = full post JSON. Register URL in Postiz UI → Settings → Webhooks → https://n8n.viktorbarzin.me/webhook/postiz-publish"
},
{
"parameters": {
"jsCode": "// Postiz webhook payload is the full post object.\nconst raw = $input.first().json;\nconst body = raw.body || raw;\nconst integ = body.integration || {};\nconst providerName = integ.name || 'unknown';\nconst providerIdentifier = integ.providerIdentifier || 'unknown';\nconst content = (body.content || '').slice(0, 200);\nconst releaseURL = body.releaseURL || '';\nconst publishDate = body.publishDate || '';\nconst state = body.state || '';\nconst integrationPicture = integ.picture || '';\n\nconst when = publishDate ? new Date(publishDate).toLocaleString('en-GB', { timeZone: 'Europe/Sofia' }) : 'just now';\n\nconst lines = [\n '<b>📤 Posted to ' + providerName + '</b> (' + providerIdentifier + ')',\n '',\n];\nif (releaseURL) lines.push('<a href=\"' + releaseURL + '\">View on Instagram</a>');\nif (content) lines.push('', '<i>' + content + '</i>');\nlines.push('', 'state=' + state + ' · published ' + when);\n\nreturn [{ json: {\n text: lines.join('\\n'),\n release_url: releaseURL,\n post_id: body.id,\n integration_id: integ.id,\n}}];"
},
"id": "format-message",
"name": "Format Telegram message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [500, 300],
"notes": "Build the HTML-formatted Telegram message from Postiz's post JSON. Defensive for missing fields — Postiz only fires on success, but webhooks elsewhere might send partial data."
},
{
"parameters": {
"method": "POST",
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{"name": "Content-Type", "value": "application/json"}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ chat_id: $env.TELEGRAM_CHAT_ID, text: $json.text, parse_mode: 'HTML', disable_web_page_preview: false }) }}",
"options": {"timeout": 30000}
},
"id": "telegram-notify",
"name": "Telegram sendMessage",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [750, 300],
"notes": "Send the formatted notification to the user's Telegram chat. parse_mode=HTML so the link is clickable; preview enabled so the IG card renders inline."
}
],
"connections": {
"Postiz webhook (publish)": {"main": [[{"node": "Format Telegram message", "type": "main", "index": 0}]]},
"Format Telegram message": {"main": [[{"node": "Telegram sendMessage", "type": "main", "index": 0}]]}
},
"settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true},
"staticData": null,
"meta": {"templateCredsSetupCompleted": false},
"pinData": {}
}