- TypeScript 42.9%
- JavaScript 29.2%
- CSS 16%
- HTML 11.5%
- Dockerfile 0.4%
| public | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| schedule.example.yaml | ||
| tsconfig.json | ||
DA Scheduler
A self-hosted Node.js/TypeScript scheduler for posting images to DeviantArt on a fixed weekly schedule, with a web UI, auto-tagging via ImageDNA, and bulk upload support.
Features
- Posts queued images to DeviantArt at fixed times per weekday (configurable; days can be skipped entirely)
- Auto-suggests tags via a local ImageDNA instance, formatted to DA's rules (lowercase, no spaces, underscores, capped at 30)
- Web UI for managing the queue, schedule, auth, and settings — no CLI required for day-to-day use
- Bulk upload mode — drop multiple images at once, review/edit each card, queue or send to Sta.sh in one click
- Per-post flags: mature content, AI generated, add watermark
- Global publish settings via
.env: Creative Commons license, commercial use, modification rights, display resolution - Queue thumbnail previews with configurable size (64–512 px) and aspect ratio (1:1, 16:9, 9:16)
- Force-post next item immediately (bypasses schedule, useful for testing)
- Retry failed posts; clear published history
- SQLite queue persists across restarts
- OAuth2 PKCE flow with automatic token refresh
What it does NOT do (by design)
- Subscriber-exclusive posts — the DA API has no working parameter for this. Post exclusives manually via DA's Studio scheduler and leave those slots out of
schedule.yaml. - Gallery/folder targeting — the
galleryidsparameter on the publish endpoint is unreliable and falls back to the Featured gallery. Sort posts into folders manually after publishing.
Setup
1. Register a DeviantArt application
Go to https://www.deviantart.com/developers/apps and create an app.
- Set the redirect URI to
http://localhost:4000/callback(must matchDA_REDIRECT_URIin.envexactly) - Note the Client ID and Client Secret
2. Install dependencies
npm install
3. Configure
cp .env.example .env
cp schedule.example.yaml schedule.yaml
Edit .env — at minimum set DA_CLIENT_ID, DA_CLIENT_SECRET, and DA_REDIRECT_URI. See .env.example for all options including license settings and thumbnail configuration.
Edit schedule.yaml to set posting times per weekday. Empty list = no posts that day.
timezone: "Europe/Amsterdam"
slots:
monday: ["09:00", "18:00"]
tuesday: ["09:00", "18:00"]
wednesday: ["09:00", "18:00"]
thursday: ["09:00", "18:00"]
friday: ["09:00", "18:00"]
saturday: []
sunday: []
4. Start ImageDNA (optional, for auto-tagging)
docker run -d -p 5000:5000 nuclear314/image-dna:latest
Set IMAGEDNA_URL=http://localhost:5000 in .env. If ImageDNA is unavailable, tagging is skipped and you can enter tags manually.
5. Run the scheduler
# Development (TypeScript directly)
npm run dev
# Production (compile first)
npm run build
npm start
Or with Docker Compose:
docker compose up -d
Leave the process running (systemd, pm2, Docker, or screen/tmux). Every minute it checks whether the current time matches a configured slot; if so it publishes the next pending item.
A missed slot (queue was empty at the time) is skipped — it does not carry over.
6. Connect your DeviantArt account
Open the web UI at http://localhost:4000 and go to the Auth tab, or run:
npm run authorize
Authorize the app in the browser. Tokens are saved to data/tokens.json and refreshed automatically — you only need to do this once unless you revoke access.
Web UI
Open http://localhost:4000 (or your configured WEB_PORT).
Queue tab
Lists all posts (pending, posted, failed) with thumbnail previews. From here you can:
- Reorder pending posts (↑ / ↓)
- Delete pending posts
- Retry failed posts (resets them to pending)
- Force Post Next — publishes the next pending item immediately, bypassing the schedule
- Clear Published — removes all posted entries from the history
Add Post tab
Two modes:
Single — drop one image, get auto-tag suggestions, edit title/description/tags, then either add to queue or upload directly to Sta.sh.
Bulk — drop multiple images at once. Each image gets a card with:
- Auto-populated title (derived from filename)
- Description field
- Editable tags (auto-suggested)
- Mature content, AI generated, and watermark checkboxes
Use the action bar to mark all images as mature, AI generated, or watermarked in one click. Then Queue All or Upload All to Sta.sh.
Schedule tab
Displays your current schedule.yaml posting slots per weekday.
Settings tab
Configure all .env values through the UI without editing files directly. Changes take effect after restarting the server.
| Group | Settings |
|---|---|
| DeviantArt | Client ID, Client Secret, Redirect URI |
| ImageDNA | Instance URL, tag confidence threshold, max tags |
| Server | Web UI port |
| Thumbnails | Size (64/128/256/512 px), aspect ratio (1:1 / 16:9 / 9:16) |
| License | Creative Commons, commercial use, modification rights |
| Display | Display resolution (0–8), DA display resolution |
CLI (alternative to web UI)
# Add a single image interactively
npm run add -- path/to/image.png
# List queue with statuses
npm run queue
# OAuth2 authorization (if not using the web UI)
npm run authorize
Configuration reference (.env)
# Required
DA_CLIENT_ID=
DA_CLIENT_SECRET=
DA_REDIRECT_URI=http://localhost:4000/callback
# ImageDNA
IMAGEDNA_URL=http://localhost:5000
DA_TAG_LIMIT=30 # max tags (DA hard limit is 30)
DA_THRESHOLD=0.35 # confidence threshold 0.0–1.0
# Server
WEB_PORT=4000
# License (applied to all published deviations)
DA_LICENSE_CC=true # Creative Commons
DA_LICENSE_COMMERCIAL=true # allow commercial use
DA_LICENSE_MODIFY=no # no | yes | share
# Display
DA_DISPLAY_RESOLUTION=1 # 0=original (no watermark) 1=400px … 8=1920px
# Queue thumbnails
THUMB_SIZE=128 # 64 | 128 | 256 | 512
THUMB_ASPECT=1:1 # 1:1 | 16:9 | 9:16
Known rough edges
The DA Sta.sh API is sparsely documented. Two details in src/deviantart/client.ts are best-effort:
- File field name on
/stash/submitis currentlytest(matching DA's own API console example). If uploads are rejected, try renaming it tofileorimage. - Tag handling — tags are sent as
tags[]on both submit and publish. If tags don't appear on published deviations, check the logged API response; it's usually a one-line fix.
Debug logging for both stash/submit and stash/publish is active and visible in the server console.