Skip to content

Production-ready Node.js external API that renders Discord leveling profile cards. Animated GIF output, with PNG fallback.

Notifications You must be signed in to change notification settings

ElectrumIsPog/levels-renderer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

levels-renderer

Production-ready Node.js external API that renders Discord leveling profile cards. Animated GIFs, with PNG fallback.

Example Output

Features

  • Animated GIF output using gifencoder (shimmer over progress bar)
  • PNG fallback if GIF path is not requested/allowed or fails
  • Robust background loader with 7s timeout, Accept: image/*, User-Agent, and first-frame decode for GIFs (incl. Giphy URLs)
  • Optional Bearer auth via API_TOKEN
  • Input validation (hex colors, blur 0..20, URL checks)
  • Simple TTL cache keyed by guildId:userId:format

Requirements

  • Node.js >= 18.19.0 (LTS recommended; Node 20 is ideal)
  • System libs for node-canvas (server should already be set up if you’re running this)
    • Debian/Ubuntu (if you need to install):
      • sudo apt-get update && sudo apt-get install -y libcairo2 libpango-1.0-0 libjpeg-turbo8 libgif7 libpng16-16 fontconfig fonts-dejavu-core fonts-noto-color-emoji
      • sudo fc-cache -f -v

Environment

  • PORT default 3000
  • API_TOKEN optional; if set, require Authorization: Bearer TOKEN
  • CACHE_SECONDS default 300
  • HTTP_TIMEOUT_MS default 7000
  • MAX_CANVAS_W default 900
  • MAX_CANVAS_H default 300

Install

npm install

Note: canvas is required at runtime. If installation fails, ensure system libs are present (see Requirements).

Run

PORT=3000 API_TOKEN=yourtoken node server.js

Health check:

curl -s http://localhost:3000/health

API

  • GET /health{ "status": "ok" }
  • POST /render/profile → raw image bytes
    • Prefers GIF when requested and when prefs.renderGIFs is true; otherwise PNG
    • Sets Content-Type: image/gif or image/png
    • Caches by guildId:userId:format

Headers you can send:

  • Accept: image/gif, image/png;q=0.8
  • X-Render-Format: gif (explicitly prefer GIF)
  • Authorization: Bearer TOKEN (only if API_TOKEN set)

Request JSON (example):

{
  "guildId": "123",
  "userId": "456",
  "userTag": "User#0001",
  "display": "Display Name",
  "totals": { "level": 8, "prestige": 1, "xp": 1234, "chatXP": 900, "voiceXP": 334, "stars": 5 },
  "progress": { "level": 8, "currLevelXP": 1000, "nextLevelXP": 1600, "intoLevel": 234, "toNext": 600, "progress": 0.39 },
  "prefs": {
    "showNick": true,
    "style": "default",
    "background": "https://picsum.photos/1200/800",
    "nameColor": "#ffffff",
    "statColor": "#cccccc",
    "barColor": "#57f287",
    "font": "default",
    "blur": 4,
    "renderGIFs": true
  },
  "settings": {
    "embedsOnly": false,
    "algorithm": { "base": 100, "exp": 2 }
  }
}

Responses:

  • 200 + Content-Type: image/gif or image/png with bytes
  • 400 { "error": "..." } invalid payload
  • 401 { "error": "unauthorized" } when token required/missing
  • 503 { "error": "render_unavailable" } rendering unavailable
  • 500 { "error": "internal_error" }

GIF backgrounds

  • Backgrounds may be PNG/JPEG or GIF (incl. Giphy CDN URLs with query params)
  • Loader behavior in src/renderer.js:
    • Always fetch with headers: User-Agent: plex-levels-renderer/1.1, Accept: image/*, timeout 7s
    • Detect GIF by extension and/or magic bytes
    • Try first-frame decode via gifwrap; fall back to canvas.loadImage if needed
    • On failure, log a concise record (status, content-type, bytes, final URL, magic) and render a neutral background

cURL test (GIF preferred)

curl -X POST http://localhost:3000/render/profile \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Accept: image/gif, image/png;q=0.8" \
  -H "X-Render-Format: gif" \
  --data @- > out.gif <<'JSON'
{
  "guildId": "1",
  "userId": "2",
  "userTag": "User#0001",
  "display": "User",
  "totals": { "level": 5, "prestige": 0, "xp": 850, "chatXP": 700, "voiceXP": 150, "stars": 3 },
  "progress": { "level": 5, "currLevelXP": 700, "nextLevelXP": 1100, "intoLevel": 150, "toNext": 400, "progress": 0.375 },
  "prefs": { "showNick": true, "style": "default", "background": "https://picsum.photos/1200/800", "nameColor": "#ffffff", "statColor": "#cccccc", "barColor": "#57f287", "font": "default", "blur": 2, "renderGIFs": true },
  "settings": { "embedsOnly": false, "algorithm": { "base": 100, "exp": 2 } }
}
JSON

Background tests

Run the included quick test across PNG, static GIF, and Giphy animated GIF URLs:

TEST_URL=http://localhost:3000/render/profile npm run test:bg
# or against a remote service
TEST_URL=http://104.131.183.49:3000/render/profile API_TOKEN=yourtoken npm run test:bg

Security

  • If API_TOKEN is set, all requests must send Authorization: Bearer TOKEN.
  • Remote background fetches honor a strict timeout; failures degrade gracefully.

Troubleshooting

  • Fontconfig error: install fonts and rebuild cache
    • Debian/Ubuntu: sudo apt-get install -y fontconfig fonts-dejavu-core fonts-noto-color-emoji && sudo fc-cache -f -v
  • Canvas build issues: ensure libcairo2, libpango-1.0-0, libjpeg-turbo8, libgif7, libpng16-16 are installed before npm install.
  • Windows: prefer Node 20 LTS for prebuilt node-canvas binaries, or use WSL/Ubuntu.

About

Production-ready Node.js external API that renders Discord leveling profile cards. Animated GIF output, with PNG fallback.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published