A Docker container that fetches upcoming movie and TV posters from Radarr and Sonarr, and composites them as floating, animated overlays onto background videos — ready to drop into NeXroll as Plex prerolls.
- Scans
/inputfor video files (.mov,.mp4,.m4v,.mpg,.mkv) - Each video must have a matching
.yamlfile with the same name - The yaml top-level key determines the poster source:
movie:→ fetches posters from Radarrtv:→ fetches posters from Sonarr
- Each video is processed independently using its yaml settings
- Output files are saved to
/output(orOUTPUT_DIRif set) named by theoutput=field in the yaml
Note
(Use them with the SampleTV.yaml and SampleMovie.yaml files)
/input/
RedCurtainsv2.mov ← background video
RedCurtainsv2.yaml ← matching config (must be same name)
TheaterSmokev1.mov
TheaterSmokev1.yaml
/output/
RedCurtains.mp4 ← named by output= in the yaml
TheaterSmokev1.mp4
docker pull ghcr.io/TechJedi51/floating-posters:latestservices:
floating-posters:
image: ghcr.io/TechJedi51/floating-posters:latest
volumes:
- /path/to/source:/input
- /path/to/output:/output # see Output section below for NeXroll shared volume alternative
environment:
- RADARR_URL=http://192.168.1.100:7878
- RADARR_API_KEY=${RADARR_API_KEY}
- SONARR_URL=http://192.168.1.100:8989
- SONARR_API_KEY=${SONARR_API_KEY}
- NEXROLL_URL=http://192.168.1.100:9393 # optional
- NEXROLL_API_KEY=${NEXROLL_API_KEY}
- NEXROLL_OUTPUT_PATH=/path/to/output # optional — see Output section
- RERUN_INTERVAL=24h
- CPU_THREADS=2
- VIDEO_CRF=18
- VIDEO_PRESET=fast
restart: unless-stopped
deploy:
resources:
limits:
cpus: "2.0"Tip: If floating-posters is in the same stack as Radarr, Sonarr, and NeXroll, add them all to
depends_onto ensure container start order. TheSTARTUP_RETRY_ATTEMPTSandSTARTUP_RETRY_DELAYenv vars handle the gap between a container starting and the service being ready to accept API calls.
docker compose run --rm floating-postersOnly global / connection settings go here. All per-video settings go in the yaml.
| Variable | Default | Description |
|---|---|---|
RADARR_URL |
http://localhost:7878 |
Radarr base URL |
RADARR_API_KEY |
(required) | Radarr → Settings → General → API Key |
SONARR_URL |
http://localhost:8989 |
Sonarr base URL (required for tv: yamls) |
SONARR_API_KEY |
(optional) | Sonarr API Key |
NEXROLL_URL |
(optional) | NeXroll base URL — enables registration if set |
NEXROLL_API_KEY |
(optional) | NeXroll full-access API key (Settings → API Keys) |
NEXROLL_OUTPUT_PATH |
(optional) | Host path NeXroll sees for your output folder |
STARTUP_RETRY_ATTEMPTS |
5 |
Times to retry connecting to Radarr/Sonarr before giving up |
STARTUP_RETRY_DELAY |
30 |
Seconds between retry attempts |
CPU_THREADS |
2 |
FFmpeg thread limit (0 = unlimited) |
VIDEO_CRF |
18 |
18=near-lossless · 23=default · 28=smaller |
VIDEO_PRESET |
fast |
ultrafast/fast/medium/slow |
See .yaml.example for a fully annotated template. The structure is:
movie: # or tv: for Sonarr
- output=MyPreroll
- FONT=Poppins-Bold
- NUM_POSTERS=10
- START_TIME=3.0
- TOP_MESSAGE_SHOW=true
- TOP_MESSAGE=Coming Soon to SEAL iPlex
- TOP_MESSAGE_SIZE=90
- TOP_MESSAGE_BG_OPACITY=0
- BOTTOM_MESSAGE_SHOW=true
- BOTTOM_MESSAGE=Updated
- BOTTOM_MESSAGE_ADD_DATE=true
...| Setting | Default | Options |
|---|---|---|
FONT |
Poppins-Bold |
See full list below |
Available fonts:
Poppins-Bold · Poppins-Medium · Poppins-Regular · DejaVuSans-Bold · DejaVuSans · DejaVuSerif-Bold · DejaVuSerif · DejaVuSansMono-Bold · DejaVuSansCondensed-Bold · LiberationSans-Bold · LiberationSans · LiberationSerif-Bold · LiberationMono-Bold · FreeSansBold · FreeSerifBold · Carlito-Bold · Caladea-Bold
| Setting | Default | Description |
|---|---|---|
NUM_POSTERS |
4 |
1–10. 6+ triggers automatic 2-row layout. |
UPCOMING_DAYS |
180 |
Days ahead to scan for upcoming releases |
2-row layout: 6 (3+3) · 7 (4+3) · 8 (4+4) · 9 (5+4) · 10 (5+5). Each row is independently centred.
| Setting | Default | Description |
|---|---|---|
START_TIME |
2.0 |
Seconds into video where posters appear |
POSTER_DURATION |
8.0 |
How long posters are visible (max 10s) |
FADE_DURATION |
0.75 |
Fade in/out duration |
| Setting | Default | Description |
|---|---|---|
POSTER_WIDTH |
185 |
Width in pixels (height auto-scales) |
PADDING |
28 |
Pixels between posters |
ROW_GAP |
24 |
Pixels between rows (2-row layout) |
VERTICAL_POS |
0.52 |
0.0=top · 0.5=center · 1.0=bottom |
CORNER_RADIUS |
10 |
Rounded corner radius |
| Setting | Default | Description |
|---|---|---|
ADD_SHADOW |
true |
Poster drop shadow |
SHADOW_OFFSET_X |
7 |
Horizontal offset |
SHADOW_OFFSET_Y |
9 |
Vertical offset |
SHADOW_BLUR |
9 |
Softness (Gaussian blur radius) |
SHADOW_OPACITY |
175 |
0=invisible · 255=solid |
| Setting | Default | Description |
|---|---|---|
SHOW_RELEASE_DATE |
true |
Show date below each poster |
RELEASE_DATE_COLOR |
#FFFFFF |
Text color |
RELEASE_DATE_SIZE |
15 |
Font size in pixels |
RELEASE_DATE_SHADOW |
true |
Drop shadow behind text |
RELEASE_DATE_BG_COLOR |
#000000 |
Pill background color |
RELEASE_DATE_BG_OPACITY |
170 |
0=none · 170=semi · 255=solid |
| Setting | Default | Description |
|---|---|---|
TOP_MESSAGE_SHOW |
false |
Enable top message |
TOP_MESSAGE |
(empty) | Text to display |
TOP_MESSAGE_ADD_DATE |
false |
Append today's date |
TOP_MESSAGE_COLOR |
white |
Text color |
TOP_MESSAGE_SIZE |
15 |
Font size in pixels |
TOP_MESSAGE_SHADOW |
false |
Drop shadow |
TOP_MESSAGE_BG_COLOR |
#000000 |
Pill background |
TOP_MESSAGE_BG_OPACITY |
170 |
0=none · 255=solid |
| Setting | Default | Description |
|---|---|---|
BOTTOM_MESSAGE_SHOW |
false |
Enable bottom message |
BOTTOM_MESSAGE |
(empty) | Text to display |
BOTTOM_MESSAGE_ADD_DATE |
true |
Append today's date |
BOTTOM_MESSAGE_COLOR |
white |
Text color |
BOTTOM_MESSAGE_SIZE |
15 |
Font size in pixels |
BOTTOM_MESSAGE_SHADOW |
false |
Drop shadow |
BOTTOM_MESSAGE_BG_COLOR |
#000000 |
Pill background |
BOTTOM_MESSAGE_BG_OPACITY |
170 |
0=none · 255=solid |
After each successful render, floating-posters can automatically register the output with NeXroll — creating the category if needed and optionally applying it to Plex immediately.
Requires NEXROLL_URL, NEXROLL_API_KEY, and NEXROLL_OUTPUT_PATH in docker-compose. The API key must be full access (Settings → API Keys in NeXroll).
| Setting | Default | Description |
|---|---|---|
NEXROLL_REGISTER |
false |
Enable registration after render |
NEXROLL_CATEGORY |
(empty) | NeXroll category name to register under |
NEXROLL_DISPLAY_NAME |
(empty) | Display name in NeXroll (defaults to output= value) |
NEXROLL_CREATE_CATEGORY |
true |
Create the category in NeXroll if it doesn't exist |
NEXROLL_APPLY_TO_PLEX |
false |
Immediately apply the category to Plex after registering |
Map any host folder to /output. Videos are written there directly.
volumes:
- /path/to/output:/outputOUTPUT_DIR defaults to /output and does not need to be set unless you want to write into a subfolder of the mounted volume.
When floating-posters and NeXroll run in the same stack, mount the same named volume that NeXroll uses and set OUTPUT_DIR to point at NeXroll's preroll subfolder. Set NEXROLL_OUTPUT_PATH to the same path so NeXroll can locate the files when registering them.
floating-posters:
volumes:
- /path/to/source:/input
- type: volume
source: plexserver_nexroll # same volume NeXroll uses
target: /nexroll_media
volume:
nocopy: true
environment:
- OUTPUT_DIR=/nexroll_media/Pre-Rolls
- NEXROLL_OUTPUT_PATH=/nexroll_media/Pre-Rolls
- NEXROLL_URL=http://nexroll:9393
- NEXROLL_API_KEY=${NEXROLL_API_KEY}With this setup there is no need for NEXROLL_OUTPUT_PATH to translate between host and container paths — both containers share the same volume and see the same path.
NEXROLL_OUTPUT_PATH in standalone mode: If floating-posters and NeXroll are in separate stacks (not sharing a volume), NEXROLL_OUTPUT_PATH must be set to the host filesystem path that NeXroll can access. For example if your volume is - /mnt/media/prerolls:/output, set NEXROLL_OUTPUT_PATH=/mnt/media/prerolls.
| Setting | Default | Description |
|---|---|---|
ANIMATION_STYLE |
bounce |
Animation style — see options below |
| Style | Description |
|---|---|
bounce |
Sine-wave vertical float (original behaviour) |
fade |
Static grid positions, fade in/out only |
wave |
Posters cascade in left-to-right with staggered delay |
pop-in |
Each poster scales from large down to grid size, staggered |
carousel |
Elliptical orbit with depth-based scaling (3-D feel) |
spotlight |
Full grid visible with a soft searchlight that moves between posters in random order |
drift |
One poster at a time travels across the screen; 6+ posters use two staggered rows |
Each style has optional fine-tuning settings — see .yaml.example for the full list (WAVE_STAGGER, POPIN_SCALE, CAROUSEL_RX, SPOTLIGHT_DARKNESS, DRIFT_SPACING, etc.).
| Setting | Default | Description |
|---|---|---|
FLOAT_AMPLITUDE |
14.0 |
Max pixels of vertical drift (bounce/drift styles) |
FLOAT_SPEED |
0.55 |
Oscillations per second |
When floating-posters starts at the same time as Radarr/Sonarr (e.g. on a fresh stack deploy or reboot), the *arr services may not be ready to accept API calls immediately even after their containers are running. The retry settings handle this gracefully:
| Variable | Default | Description |
|---|---|---|
STARTUP_RETRY_ATTEMPTS |
5 |
Number of connection attempts before giving up |
STARTUP_RETRY_DELAY |
30 |
Seconds to wait between attempts |
With defaults, floating-posters will wait up to 2.5 minutes for Radarr/Sonarr to become available before failing. Each attempt is logged:
⚠ Radarr not ready (attempt 1/5): Connection refused
Retrying in 30s...
⚠ Radarr not ready (attempt 2/5): Connection refused
Retrying in 30s...
[font] Poppins-Bold size=15
...
If you're running floating-posters in the same docker-compose stack as Radarr and Sonarr, also add depends_on to ensure container start order:
depends_on:
- radarr
- sonarr
- nexrollNote:
depends_ononly guarantees that the Radarr/Sonarr containers start before floating-posters — not that the services inside them are ready. The retry logic handles the remaining gap.
Set RERUN_INTERVAL in docker-compose and change restart: unless-stopped — the container runs immediately on start, then sleeps and repeats automatically. No cron, no external scheduler needed.
environment:
- RERUN_INTERVAL=24h # run every 24 hours
restart: unless-stopped # keep container alive between runsSupported interval formats:
| Value | Meaning |
|---|---|
30m |
Every 30 minutes |
6h |
Every 6 hours |
12h |
Every 12 hours |
24h |
Every 24 hours |
1d |
Every day (same as 24h) |
| (unset) | Run once and exit |
Logs show each run number, timestamp, and next scheduled run time:
══════════════════════════════════════════════════════
Run #1 — 2026-04-21 02:00:00
══════════════════════════════════════════════════════
floating-posters v1.9.0
...
✅ RedCurtains.mp4 saved to /output
Next run: 2026-04-22 02:00:00
Sleeping 24h...
If a run fails (non-zero exit), the container logs a warning and continues to the next scheduled run rather than crashing.
When floating-posters starts at the same time as Radarr/Sonarr (e.g. on a fresh stack deploy or reboot), the *arr services may not be ready to accept API calls immediately even after their containers are running. The retry settings handle this gracefully:
| Variable | Default | Description |
|---|---|---|
STARTUP_RETRY_ATTEMPTS |
5 |
Number of connection attempts before giving up |
STARTUP_RETRY_DELAY |
30 |
Seconds to wait between attempts |
With defaults, floating-posters will wait up to 2.5 minutes for Radarr/Sonarr to become available before failing. Each attempt is logged:
⚠ Radarr not ready (attempt 1/5): Connection refused
Retrying in 30s...
⚠ Radarr not ready (attempt 2/5): Connection refused
Retrying in 30s...
[font] Poppins-Bold size=15
...
If you're running floating-posters in the same docker-compose stack as Radarr and Sonarr, also add depends_on to ensure container start order:
depends_on:
- radarr
- sonarr
- nexrollNote:
depends_ononly guarantees that the Radarr/Sonarr containers start before floating-posters — not that the services inside them are ready. The retry logic handles the remaining gap.
If you prefer host-level cron over the built-in scheduler, leave RERUN_INTERVAL unset (restart: "no") and use a crontab entry instead:
# Regenerate prerolls every night at 2 AM
0 2 * * * docker compose -f /path/to/floating-posters/docker-compose.yml run --rm floating-postersgit clone https://github.com/TechJedi51/floating-posters
cd floating-posters
docker build -t floating-posters .
docker run --rm \
-v /path/to/source:/input \
-v /path/to/output:/output \
-e RADARR_URL=http://your-radarr:7878 \
-e RADARR_API_KEY=your_key \
floating-postersOn every push to main, GitHub Actions automatically:
- Builds for
linux/amd64andlinux/arm64(Apple Silicon / Unraid) - Pushes
ghcr.io/TechJedi51/floating-posters:latest - Tags version releases (
v2.2.6) as:2.2.6and:2.2
- Atomic output: video is rendered to
.tmp_<name>.mp4first, then moved atomically into place withos.replace()— Plex always sees a complete file, never a partial write mid-render
- Drift two-row layout: 6+ posters split into two rows (same split as grid styles); row 2 time-offset by
DRIFT_ROW_OFFSETso rows stagger at centre rather than hitting simultaneously - Added
DRIFT_ROW_OFFSETtuning var
- Drift rewritten: parade flow with overlapping clips — posters ease in from edge, pause at screen centre, ease out;
DRIFT_SPACINGcontrols overlap;DRIFT_ENTER/DRIFT_PAUSEcontrol timing fractions;slot_durderived fromPOSTER_DURATIONso all posters always fit - Replaced linear constant-speed movement with smoothstep easing
- Fixed spotlight double-move at slot boundary — removed the duplicate "departing" lerp branch; spotlight now dwells and moves only once per transition
- Spotlight rewritten: full grid always visible; dark semi-transparent overlay with a soft elliptical "searchlight hole" that travels between poster positions in random order
- Smooth smoothstep interpolation between poster centres; numpy pixel grid pre-computed once outside the frame loop for performance
- New tuning vars:
SPOTLIGHT_DARKNESS,SPOTLIGHT_PAD,SPOTLIGHT_INNER; oldSPOTLIGHT_SIZE/SPOTLIGHT_BREATHE/SPOTLIGHT_SCALE/SPOTLIGHT_DIMremoved
- Fixed pop-in date label spam —
make_text_imagewas being called inside themake_rgbaframe closure (once per poster per frame); date images now pre-rendered before the closure like carousel and spotlight - Added animation style + parameters to the run_job log header
- Start of v2.2.x beta cycle — animation styles feature set stabilised
- Fixed version consistency — README title and script docstring both updated alongside the
VERSIONconstant
- 7 animation styles:
bounce(original),fade,wave,pop-in,carousel,spotlight,drift— selected per-video viaANIMATION_STYLEin the yaml - Full-frame PIL renderer (
_full_frame_clip) used forcarousel,pop-in, andspotlightto support z-ordering and per-frame scaling - Each style has optional fine-tuning env vars (
WAVE_STAGGER,POPIN_SCALE,CAROUSEL_RX,SPOTLIGHT_SIZE,DRIFT_SPEED, etc.) - Grid layout pre-computed into a flat list and passed to style functions — clean separation between layout and animation
- Version bump marking the first stable public release
- All core features complete and tested: multi-video yaml processing, Radarr/Sonarr integration, NeXroll registration, built-in scheduler, multi-row poster layout, text overlays, 17 bundled fonts, startup retry logic
- Duplicate registration prevention:
nexroll_find_existing()checksGET /external/prerollsbefore registering — skips re-registration if a preroll with the same path already exists; video file on disk is always updated by the render step regardless
PYTHONUNBUFFERED=1added to Dockerfile andpython3 -uused in entrypoint — ensures all log output is flushed immediately to Docker rather than being buffered and lost if the process exits
- Fixed NeXroll registration payload field name:
file_path→path(API docs were wrong; actual validation schema requirespath) - Suppressed moviepy progress bar (
logger=None) — removes noisy▓▓▓ 37%lines from Docker logs
- Added full response body logging on HTTP errors (422 etc.) to diagnose NeXroll API rejections
- Fixed
UnboundLocalErroroncat_id— moved debug print to after the category lookup call - Category creation response now tries multiple ID field names (
id,category_id,category.id)
- Switched NeXroll auth from
Authorization: Bearerheader to?api_key=query parameter — NeXroll only accepts the query param form despite documenting both - Fixed categories response parsing to handle
{"categories": [...], "count": N}wrapper returned by actual API
- NeXroll API calls now retry on 401 (not just connection errors) — handles NeXroll still initialising its auth system when floating-posters first starts
nexrolladded todepends_onin docker-compose sample
- Improved NeXroll error messages — distinguishes 401 (bad/missing key), 403 (read-only key), connection refused, and timeout as separate cases with actionable guidance
- Startup retry: Radarr and Sonarr connection attempts retry with configurable backoff (
STARTUP_RETRY_ATTEMPTS=5,STARTUP_RETRY_DELAY=30) instead of immediately failing depends_on: [radarr, sonarr, nexroll]added to docker-compose sample- Retry progress logged per-attempt with attempt count and remaining delay
- Documented both output modes in docker-compose and README: standalone (
/outputbind mount) vs NeXroll shared volume (OUTPUT_DIR=/nexroll_media/Pre-Rolls)
- Built-in scheduler:
RERUN_INTERVAL(e.g.24h,12h,6h,1d,30m) keeps the container running and re-executes on a repeating schedule — no cron needed - Each run is numbered and timestamped; next run time shown after completion
- Failed runs log a warning and continue rather than crashing
- NeXroll integration: registers rendered videos with NeXroll after each successful render
- Category lookup and auto-creation (
NEXROLL_CREATE_CATEGORY) - Optional immediate Plex sync (
NEXROLL_APPLY_TO_PLEX) NEXROLL_OUTPUT_PATHmaps container output path to the path NeXroll sees on the host
- Multi-video scan-based processing: one run handles all video+yaml pairs in
/input - yaml-driven config: all per-video settings move from docker-compose env vars to individual
.yamlfiles — compose only needs connection/quality settings - Sonarr support:
tv:yaml key fetches upcoming TV series posters via Sonarr calendar API;movie:routes to Radarr
- Text wrapping for top/bottom messages (word-wraps at 85% of video width, each line centred)
- Multi-row poster layout: 6–10 posters split into 2 centred rows — 6 (3+3) · 7 (4+3) · 8 (4+4) · 9 (5+4) · 10 (5+5)
ROW_GAPconfig for vertical spacing between rows;NUM_POSTERSmax raised from 6 to 10
*_BG_COLORand*_BG_OPACITYfor all three text areas (release date, top message, bottom message)BG_OPACITY=0removes the pill background entirely — text only, relies on shadow for legibility
FONTconfig with 17 bundled fonts; Poppins-Bold is the default- All font packages added to Dockerfile (
fonts-liberation,fonts-freefont-ttf,fonts-crosextra-*); Poppins downloaded from Google Fonts at build time TOP_MESSAGEoverlay with full parity toBOTTOM_MESSAGEBOTTOM_MESSAGE_SHADOWoption
- Fixed release date not showing — text labels rearchitected as separate moviepy clips instead of being embedded in the poster RGBA image
- Date labels float in sync with their poster (same sine-wave phase)
BOTTOM_MESSAGEoverlay centered at bottom of frame, fades with poster group- Date format changed to
April 20, 2026style
- Python 3.13 base image;
apt-get upgradefor patched openssl/libssl CPU_THREADSenv var passes-threads Nto FFmpeg;deploy.resources.limits.cpuscaps the container
SHOW_RELEASE_DATE,RELEASE_DATE_COLOR,RELEASE_DATE_SIZE,RELEASE_DATE_SHADOW- Fixed
set_opacityTypeError with moviepy 1.0.3 — replaced withVideoClipmask for proper per-frame fade with alpha channel preservation
- Initial release: Radarr API integration, floating sine-wave animation, configurable fade/shadow/rounded corners, multi-arch Docker image (amd64 + arm64)
Built to work with NeXroll by JFLXCLOUD.
Poster art sourced from Radarr and Sonarr via their v3 APIs.