Upload images to a Discord channel via webhook, archive the originals, and (optionally) post the Discord jump link to Twitch chat using Streamer.bot.
Dockerfile— Python 3.11-slim base for a tiny FastAPI service.requirements.txt— Python dependencies for the service.app.py— FastAPI app exposingPOST /upload:- reads a file from a host-mounted folder,
- posts it to a Discord webhook (
?wait=true), - returns the Discord jump URL,
- moves the original file into an archive (preserves subfolders, avoids name collisions).
docker-compose.yml— service definition with environment variables, volume mounts, and host port binding.discord_push.ps1— helper PowerShell script you can call from Streamer.bot ("Run Program") to post a file + caption.
Typically you will generate a caption file (e.g.,
image_info.txt) with your own script (such asextract_info.pyor a.cmd), then calldiscord_push.ps1with the image path.
- Your automation (e.g., Streamer.bot File Watcher) creates
image_info.txtand produces an image under your configured upload folder. - Streamer.bot runs
discord_push.ps1 "<full path to image>". - The script computes a relative path (under your upload root), reads
image_info.txt, and POSTs JSON to the Docker service:{ "rel_path": "sub/dirs/filename.jpg", "caption": "Shot on ...\n1/125s • f/2.8 • ISO 400" } - The Docker service loads the file from
/shared/<rel_path>, uploads to Discord via webhook using?wait=true, builds the jump URL, returns it, and moves the original file to/archive/<rel_path>. - The script writes the link to
last_discord_link.txtin the upload root so you can post it to Twitch chat in a later step.
[ Streamer.bot ] --(Run Program)--> [ discord_push.ps1 ]
| |
| JSON: rel_path + caption v
|------------------------------> [ Docker service ]
|
Discord upload (webhook with ?wait=true)
|
<--- returns message id (jump URL)
|
moves original to /archive/...
- Docker Desktop on Windows (enable drive sharing for the drive you mount, e.g.,
X:/Your_working_dir/). - A Discord webhook for the target text channel (Server → Channel → Integrations → Webhooks).
- The channel’s Guild (Server) ID and Channel ID (Discord Settings → Advanced → enable Developer Mode; then right‑click server/channel → Copy ID).
- Optional: Streamer.bot (Windows).
Inside the container, Python deps are minimal (from requirements.txt):
fastapi
uvicorn[standard]
requests
The example binds host port 8082 → container 8080 to avoid conflicts (e.g., SRTLA info ports). Change as needed.
version: "3.8"
services:
discord-uploader:
build: .
container_name: discord-uploader
environment:
DISCORD_WEBHOOK_URL: "https://discord.com/api/webhooks/xxxxx/xxxxx"
DISCORD_GUILD_ID: "123456789012345678"
DISCORD_CHANNEL_ID: "234567890123456789"
SHARED_DIR: "/shared" # service reads images from here
ARCHIVE_DIR: "/archive" # originals are moved here after upload
API_KEY: "super-secret-long-random-string" # shared secret required by the API
volumes:
- "X:/Your_working_dir/upload:/shared:ro"
- "X:/Your_working_dir/archive:/archive:rw"
ports:
- "127.0.0.1:8082:8080" # host:container
restart: alwaysWindows path tips
- Keep the quotes around host paths with spaces.
- Use forward slashes with the drive letter (
H:/...) in Docker Desktop. - Ensure the H: drive is shared in Docker Desktop (Settings → Resources → File sharing).
DISCORD_WEBHOOK_URL— the webhook to receive uploads.DISCORD_GUILD_ID/DISCORD_CHANNEL_ID— used to build the clickable jump link.SHARED_DIR— container path where files are read. Bind-mount your upload folder to this.ARCHIVE_DIR— container path where originals are moved after successful upload.API_KEY— a shared secret. Requests must include headerX-API-Key: <value>, otherwise401. (If unset/empty, the check is skipped.)
From the folder with Dockerfile, app.py, and docker-compose.yml:
docker compose up -d --build
docker compose logs -f discord-uploaderCommon gotchas
- If Docker can’t find your compose file, ensure it’s named
docker-compose.ymland you’re in that folder. - After editing volumes or env vars, recreate the service:
docker compose down docker compose up -d --build
-
POST /uploadBody (JSON):
{ "rel_path": "sub/dir/file.jpg", "caption": "text to post with the image" }Headers:
X-API-Key: <your secret>(required ifAPI_KEYis set).Success (200):
{ "jump_url": "https://discord.com/channels/<guild>/<channel>/<message_id>", "archived_to": "/archive/sub/dir/file.jpg" }Behavior summary: validates
rel_pathstays within/shared, reads the file, posts to Discord with?wait=trueto retrieve the created message, then moves the original to/archive(creates subfolders and avoids collisions).
This script:
- Verifies the image is under your upload root,
- Computes the relative path for the API,
- Reads the caption from
$Root/image_info.txt(UTF‑8), - POSTs to the Docker service with your
API_KEY, - Saves the returned link to
$Root/last_discord_link.txt(so you can post it to Twitch later).
param(
[Parameter(Mandatory=$true, Position=0)]
[string]$ImagePath,
[string]$Root = "X:/Your_working_dir/",
[string]$Api = "http://127.0.0.1:8082/upload",
[string]$ApiKey = "super-secret-long-random-string"
)
$ErrorActionPreference = "Stop"
# Normalize paths
$full = [IO.Path]::GetFullPath((Resolve-Path -LiteralPath $ImagePath))
$rootFull = ([IO.Path]::GetFullPath($Root)).TrimEnd('\','/') + '\'
if (-not $full.ToLower().StartsWith($rootFull.ToLower())) {
throw "Image '$full' is not under root '$rootFull'."
}
# Relative path for API
$rel = $full.Substring($rootFull.Length).TrimStart('\','/')
# Caption: $Root/image_info.txt (UTF-8)
$captionPath = [IO.Path]::Combine($rootFull, 'image_info.txt')
$caption = ""
if (Test-Path -LiteralPath $captionPath) {
$caption = [System.IO.File]::ReadAllText($captionPath, [System.Text.Encoding]::UTF8)
}
# POST
$payload = @{ rel_path = $rel; caption = $caption } | ConvertTo-Json -Depth 3 -Compress
$headers = @{ "X-API-Key" = $ApiKey }
$r = Invoke-RestMethod -Method Post -Uri $Api `
-Headers $headers -ContentType "application/json; charset=utf-8" -Body $payload
# Hand-off options
Set-Variable -Name discordLink -Value $r.jump_url -Scope Script
[IO.File]::WriteAllText(([IO.Path]::Combine($rootFull,'last_discord_link.txt')), $r.jump_url)
$r.jump_url- Action trigger: your file watcher (pointing at the upload folder).
- Sub-actions:
- Run Program — generate caption (your
extract_info.pyor.cmd) with Start In set to your upload root so it writesimage_info.txtthere. - Run Program — call PowerShell to upload:
- Program:
powershell - Arguments:
-NoProfile -ExecutionPolicy Bypass -File "X:/Your_working_dir/discord_push.ps1" "%{file.fullPath}%"
- Program:
- Send Message (Twitch) — read and post the link from the file:
- PowerShell sub-action example:
$link = Get-Content "X:/Your_working_dir/last_discord_link.txt" -Raw # Use $link as the message text in your Twitch Send Message step
- Or run a separate action that watches for
last_discord_link.txtcreation.
- PowerShell sub-action example:
- Run Program — generate caption (your
Why file hand-off? Some Streamer.bot builds don’t expose a “capture stdout” value from Run Program. Writing
last_discord_link.txtmakes the link available to any later step without extra wiring.
{"detail":"file not found: /shared/..."}— The container can’t see your file. Check composevolumes, drive sharing, and that PowerShell$Rootmatches the host path mapped to/shared.401 bad key— The headerX-API-Keydoesn’t match the container’sAPI_KEY.- Execution policy errors — Use
-ExecutionPolicy Bypasswith-Filein Streamer.bot’s Run Program. - Weird symbols for
•in the console — Console code page only; the API receives UTF‑8 correctly. - Discord “Request entity too large (40005)” — Image exceeded the server’s upload limit (rare if camera JPEGs are 2–8 MB).
- Keep the service bound to
127.0.0.1(as in the compose example) unless you need LAN access. - Always set a strong
API_KEY. If it leaks, rotate it (change compose env + redeploy). - Treat the webhook URL as a secret.
MIT (or your preference).