[object Object]

← back to Dw Slideshow Gen

postiz: per-provider settings in draft body (hosted API requires them even for drafts) + idempotent/paced bulk staging (posted.json marker, --restage)

0b44dfbbb1015c24f7727500fdab611432b05eeb · 2026-06-29 13:10:39 -0700 · Steve Abrams

Files touched

Diff

commit 0b44dfbbb1015c24f7727500fdab611432b05eeb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Jun 29 13:10:39 2026 -0700

    postiz: per-provider settings in draft body (hosted API requires them even for drafts) + idempotent/paced bulk staging (posted.json marker, --restage)
---
 index.js      | 32 ++++++++++++++++++++++----------
 lib/post.js   |  9 ++++-----
 lib/postiz.js | 22 ++++++++++++++++++----
 3 files changed, 44 insertions(+), 19 deletions(-)

diff --git a/index.js b/index.js
index a1e7da4..a62d698 100644
--- a/index.js
+++ b/index.js
@@ -42,6 +42,7 @@ function parseArgs(argv) {
     else if (t === '--no-photos') a.photos = false
     else if (t === '--confirm') a.confirm = true
     else if (t === '--postiz') a.postiz = true
+    else if (t === '--restage') a.restage = true
     else if (t === '--channel') a.channel = argv[++i]
     else if (t === '--schedule') a.schedule = argv[++i]
     else a._.push(t)
@@ -99,19 +100,30 @@ if (cmd === 'post') {
       : []
   }
   if (!dirs.length) { console.error('no render dirs found — run `render`/`catalog`/`all` first'); process.exit(1) }
+  const sleep = ms => new Promise(r => setTimeout(r, ms))
+  let staged = 0, skipped = 0, failed = 0, i = 0
   for (const dir of dirs) {
-    const pkg = packageDraft(dir, { zip: true })
-    console.error(`  [post] ${pkg.spec_id}: local draft package (${pkg.slides} slides) -> ${pkg.zipPath || pkg.pkgDir}  ($0, no network)`)
-    if (!a.postiz) continue
+    i++
+    if (!a.postiz) {                       // local-package mode (no network)
+      const pkg = packageDraft(dir, { zip: true })
+      console.error(`  [post] ${pkg.spec_id}: local draft package (${pkg.slides} slides) -> ${pkg.zipPath || pkg.pkgDir}  ($0)`)
+      continue
+    }
+    const marker = path.join(dir, 'posted.json')
+    const id = path.basename(dir)
+    if (!a.restage && fs.existsSync(marker)) { skipped++; continue }   // idempotent: already staged
     const r = await stageToPostiz(dir, process.env, { confirm: a.confirm, channel: a.channel, schedule: a.schedule })
-    const chNames = (r.channels || []).map(c => `${c.identifier}(${c.name})`).join(', ')
-    if (r.gated) console.error(`  [post] ${pkg.spec_id}: Postiz skipped — ${r.reason}`)
-    else if (r.error) console.error(`  [post] ${pkg.spec_id}: Postiz error — ${r.error}`)
-    else if (r.dryRun) {
-      console.error(`  [post] ${pkg.spec_id}: DRY-RUN ${r.type} → ${chNames}, ${r.images} images. Add --confirm to stage. Body:`)
-      console.error('  ' + JSON.stringify(r.body).replace(/\n/g, ' ').slice(0, 700))
-    } else if (r.staged) console.error(`  [post] ${pkg.spec_id}: Postiz ${r.type} STAGED → ${chNames} postIds=${(r.postIds || []).join(',')} — review/publish at platform.postiz.com`)
+    const chNames = (r.channels || []).map(c => c.identifier).join('+')
+    if (r.staged) {
+      staged++
+      writeJSON(marker, { postIds: r.postIds, channels: (r.channels || []).map(c => c.identifier), type: r.type, at: new Date().toISOString() })
+      console.error(`  [post ${i}/${dirs.length}] STAGED ${id} → ${chNames} (${(r.postIds || []).length} entries)`)
+      await sleep(a.confirm ? 700 : 0)     // pace API to avoid throttling
+    } else if (r.dryRun) {
+      console.error(`  [post ${i}/${dirs.length}] DRY-RUN ${r.type} → ${chNames}, ${r.images} imgs (add --confirm to stage)`)
+    } else { failed++; console.error(`  [post ${i}/${dirs.length}] ${r.gated ? 'SKIP' : 'FAIL'} ${id}: ${r.reason || r.error}`) }
   }
+  if (a.postiz) console.error(`\n  [post] done — staged ${staged}, skipped(already) ${skipped}, failed ${failed} of ${dirs.length}. Review at platform.postiz.com`)
   console.error([
     '',
     '  ── GATE ─────────────────────────────────────────────',
diff --git a/lib/post.js b/lib/post.js
index 840385c..0313eb4 100644
--- a/lib/post.js
+++ b/lib/post.js
@@ -12,7 +12,7 @@ import fs from 'node:fs'
 import path from 'node:path'
 import { execFileSync } from 'node:child_process'
 import { readJSON, ensureDir, exists } from './util.js'
-import { Postiz, buildDraftBody, buildDraftBodyMulti, buildScheduleBody, pickIntegration, pickIntegrations } from './postiz.js'
+import { Postiz, buildDraftBodyMulti, buildScheduleBody, pickIntegration, pickIntegrations } from './postiz.js'
 
 function loadManifest(renderDir) {
   const mp = path.join(renderDir, 'render-manifest.json')
@@ -81,11 +81,10 @@ export async function stageToPostiz(renderDir, env, opts = {}) {
   } catch (e) { return { staged: false, error: `integrations: ${e.message}` } }
   if (!chosen.length) return { staged: false, gated: true, reason: 'no connected channels — connect a channel in Postiz first' }
 
-  const ids = chosen.map(c => c.id)
-  // schedule needs per-provider settings, so it only supports ONE channel here; draft is multi.
+  // schedule needs a single provider's settings; draft is multi-channel (each gets its own settings)
   const mkBody = media => opts.schedule
-    ? buildScheduleBody({ integrationId: ids[0], identifier: chosen[0].identifier, content, media, dateISO, tiktok: opts.tiktok })
-    : buildDraftBodyMulti({ integrationIds: ids, content, media, dateISO })
+    ? buildScheduleBody({ integrationId: chosen[0].id, identifier: chosen[0].identifier, content, media, dateISO, tiktok: opts.tiktok })
+    : buildDraftBodyMulti({ integrations: chosen, content, media, dateISO })
 
   // dry-run: show the body with placeholder media, no upload, no post
   if (!opts.confirm) {
diff --git a/lib/postiz.js b/lib/postiz.js
index 8cd8520..13bd90a 100644
--- a/lib/postiz.js
+++ b/lib/postiz.js
@@ -55,15 +55,29 @@ export class Postiz {
 // ---- pure request-body builders (testable without a key, via --dry-run) ----
 const imageRefs = media => media.map(m => ({ id: m.id, path: m.path, ...(m.alt ? { alt: m.alt } : {}) }))
 
-export function buildDraftBody({ integrationId, content, media, dateISO }) {
-  return buildDraftBodyMulti({ integrationIds: [integrationId], content, media, dateISO })
+// per-provider settings — the hosted API requires these even for drafts
+export function settingsFor(identifier) {
+  const id = identifier || ''
+  if (id === 'tiktok') return {
+    __type: 'tiktok', title: '', privacy_level: 'SELF_ONLY',
+    duet: false, stitch: false, comment: true, autoAddMusic: 'no',
+    brand_content_toggle: false, brand_organic_toggle: false, video_made_with_ai: false,
+    content_posting_method: 'UPLOAD',
+  }
+  if (id.startsWith('instagram')) return { __type: id, post_type: 'post' }
+  if (id.startsWith('facebook')) return { __type: id, post_type: 'post' }
+  return { __type: id }
 }
 
 // stage ONE carousel to MANY channels in a single draft (posts[] = one entry per channel)
-export function buildDraftBodyMulti({ integrationIds, content, media, dateISO }) {
+export function buildDraftBodyMulti({ integrations, content, media, dateISO }) {
   return {
     type: 'draft', shortLink: false, date: dateISO, tags: [],
-    posts: integrationIds.map(id => ({ integration: { id }, value: [{ content, image: imageRefs(media) }] })),
+    posts: integrations.map(i => ({
+      integration: { id: i.id },
+      value: [{ content, image: imageRefs(media) }],
+      settings: settingsFor(i.identifier),
+    })),
   }
 }
 

← 95bb63b postiz: multi-channel draft staging (--channel all|a,b), pic  ·  back to Dw Slideshow Gen  ·  RESUME.md: snapshot for restart — DW 500-SKU Postiz batch re cb523f9 →