# PostersPlus environment configuration # Copy this file to .env and fill in your values. Everything is optional except # the two keys marked [Required]. # # This file is the settings most instances actually set. Tuning knobs — OCR # thresholds, cache TTLs, logo sizing, anime providers, poster selection — live # in ADVANCED.md. You should not need ADVANCED.md to run PostersPlus. # ============================================================================= # API keys # ============================================================================= # [Required] Fetches posters, logos, and metadata. # Get one free at https://www.themoviedb.org/settings/api TMDB_API_KEY= # Fetches aggregated ratings (Letterboxd, Trakt, Rotten Tomatoes, Metacritic, # IMDb, ...), keywords (used for the festival/cult/true-story/must-see sashes # and Oscar detection), and age ratings. # Optional despite the name suggesting otherwise in earlier docs: leaving this # blank does not stop the app from running. Posters still render with genre, # year, TMDB-sourced sashes (trending, Golden Globe, studio/director/cast, # foreign-language, release-status, structural), and quality badges; only the # MDBList-only sashes (festival, cult classic, true story, Metacritic # must-see, Oscar wins/noms) and the weighted rating score are unavailable, # and the score reads "N/A" unless IMDB_DATASET_ENABLED supplies the "imdb" # weight from the local dataset below instead. # Get one free at https://mdblist.com/ MDBLIST_API_KEY= # Optional second MDBList key. When the primary is rate-limited PostersPlus # rotates to this one and back, so ratings survive peak usage. MDBLIST_API_KEY_2= # Optional TheTVDB v4 key. When set, TVDB becomes a *fallback* art source: for # titles where TMDB has no usable logo or textless art, PostersPlus pulls a TVDB # clearlogo, backdrop, or (opt-in) poster instead of falling back to a plain text # title or genre canvas. Blank disables TVDB entirely. # Get a key at https://thetvdb.com/api-information TVDB_API_KEY= # Only needed for user-supported ("subscriber") TVDB keys; blank for company keys. TVDB_SUBSCRIBER_PIN= # Per-asset TVDB toggles. Logos and backdrops are low-risk pure fallbacks, so # they are on. Posters often carry burned-in title text, so they are off; when # enabled they are only used if text detection confirms the image is clean. TVDB_USE_LOGOS=true TVDB_USE_BACKDROPS=true TVDB_USE_POSTERS=false # Where a TVDB clearlogo ranks in the logo chain. TVDB logos are often better # than TMDB/Metahub, so raising this is usually an improvement. # 1 = TVDB first (beats TMDB and Metahub) # 2 = after TMDB's own logos, before the Metahub CDN # 3 = last resort, only when TMDB and Metahub have nothing (default) TVDB_LOGO_PRIORITY=3 # Anime-native art (AniList / Kitsu). Neither needs a key. These engage ONLY # when a client passes anilist_id= or kitsu_id= on /poster — no id conversion is # ever done, so clients that only speak imdb/tmdb/tvdb are unaffected. When # engaged, the provider supplies cover art, title, genres, air dates, status and # its community score in one request. ANIME_SOURCES_ENABLED=true # ============================================================================= # Access control # ============================================================================= # Shared secret appended to every request as ?access_key=. # Blank allows unauthenticated access. ACCESS_KEY= # ============================================================================= # Server # ============================================================================= # Uvicorn worker processes. One worker avoids duplicate uncached work and shares # a single render/rating coordination layer, which is why it is the default. WORKERS=1 # ============================================================================= # Quality badges # ============================================================================= # Quality backend: aiostreams (default), scraper, or qualicache. QUALITY_SOURCE=aiostreams # Base URL of your AIOStreams instance, e.g. http://aiostreams:3000 over the # Docker network, or https://aiostreams.example.com. Blank disables the badges. AIOSTREAMS_URL= # AIOStreams config uuid:password encoded as Base64, including the colon. # A separate config is recommended for this integration. AIOSTREAMS_AUTH= # Base URL of a standalone Stremio stream addon when QUALITY_SOURCE=scraper. SCRAPER_URL= # Base URL of a QualiCache instance when QUALITY_SOURCE=qualicache, e.g. # http://qualicache:8000 over the Docker network. QualiCache crawls addons in the # background and answers from its own cache, so rendering never waits on a # scrape; uncrawled titles report "pending" and are picked up on a later request. # Configure addons in QualiCache's own STREMIO_ADDONS, not here. QUALICACHE_URL= # Only needed if QualiCache sets an ACCESS_KEY; must match that value. QUALICACHE_API_KEY= # Lowest QualiCache release-group tier to accept: high, medium, or low. QUALICACHE_MIN_TRUST=high # Days to cache quality data for titles older than 2 weeks. New titles always use # a 1-day TTL. Raise to reduce traffic to your quality source. QUALITY_OLD_CACHE_DURATION=90 # Max concurrent background quality fetches. Caps burst load when many uncached # titles arrive at once — the rest queue rather than being dropped. QUALITY_BG_CONCURRENCY=5 # Max seconds to wait when a client requests wait_for_quality=true. QUALITY_WAIT_TIMEOUT=30 # Max concurrent outbound MDBList calls per worker. MDBLIST_CONCURRENCY=3 # Max uncached poster renders in flight per worker. Cache hits are never held # back. A cold catalog grid can request 50+ posters in a second; past this many # the rest queue instead of overwhelming TMDB/MDBList and the connection pool. POSTER_RENDER_CONCURRENCY=8 # ============================================================================= # IMDb local ratings dataset (MDBList-free IMDb score) # ============================================================================= # Every weighted rating source, including the one labelled "imdb", normally # comes from MDBList. Enabling this pulls IMDb's own free, no-key, # daily-refreshed non-commercial dataset instead # (https://datasets.imdbws.com/title.ratings.tsv.gz) and answers "imdb" weight # lookups from a local table — no MDBList key needed for that source, and it # still works if you have no MDBList key configured at all. Off by default. # # Selected per instance/request (default: mdblist, unchanged behaviour): # imdb_rating_source=dataset always use the local dataset # imdb_rating_source=fallback use MDBList, and fall back to the dataset # only when it has no IMDb score for a title — # a rate-limited or exhausted key, or a title # MDBList simply doesn't carry one for # # Note that "imdb" ships at weight 0 in the default movie/TV weights, so you # must also give it a weight on the configurator's Weights tab — otherwise # changing the source changes nothing and scores still read N/A. # # IMDb licenses these files for personal and non-commercial use only: # https://developer.imdb.com/non-commercial-datasets/ IMDB_DATASET_ENABLED=false # Where the downloaded dataset is stored (SQLite file). Put it on the same # persistent volume as the rest of the cache. Budget ~84 MB for it: the # download is ~8.6 MB gzipped and expands to ~1.7 million rows. IMDB_DATASET_PATH=/app/cache/imdb_ratings.db # Hours between dataset refreshes. IMDb regenerates the source file about once # a day, so anything below 24 just re-downloads the same data. IMDB_DATASET_REFRESH_HOURS=24 # Minimum vote count for a dataset rating to be used, mirroring RATING_MIN_VOTES. IMDB_DATASET_MIN_VOTES=10 # ============================================================================= # Output format and caching # ============================================================================= # Poster output format: webp (default) or jpeg. WebP compresses better. IMAGE_FORMAT=webp # Output quality, 70-95. Higher means larger files. Only the one matching # IMAGE_FORMAT is used. WEBP_QUALITY=85 JPEG_QUALITY=85 # Adds Cache-Control: public, max-age= to poster responses, capped at the # composite's own remaining life so a cached copy never outlives the trending # rank or release status baked into it. Use "auto" to advertise that remaining # life with no fixed ceiling, which is what a caching client wants: a # trending-sashed poster expires in a day, an Ended title lasts the full # COMPOSITE_CACHE_TTL. 0 sends no Cache-Control at all (default; recommended # unless you front this with a CDN or a caching client). CDN_CACHE_TTL=0 # Seconds to keep a composited poster before re-rendering. Pruning runs off this # value so abandoned configs don't grow the cache forever. Default: 7 days. COMPOSITE_CACHE_TTL=604800 # Max composite cache entries. 0 = no cap, rely on the TTL alone. Worth setting # on a public instance as a failsafe. COMPOSITE_MAX_ENTRIES=0 # ============================================================================= # Trending # ============================================================================= # Time of day to fetch the trending list, e.g. "04:00". Empty = every 24 hours. TRENDING_FETCH_TIME= # Timezone for that schedule, e.g. "America/New_York". TRENDING_FETCH_TIMEZONE=UTC # How many trending / broad-trending items to fetch per background cycle. TRENDING_FETCH_COUNT=40 TRENDING_BROAD_FETCH_COUNT=100 # Where "trending" comes from. Empty = TMDB's own global list, which is # US-weighted and takes no region parameter. Setting one replaces TMDB's list for # that media type, for both the sash's ranking and what the cache warmer # pre-renders. The list's own order IS the ranking. Two shapes are accepted: # # MDBList — paste the ordinary list page URL, no MDBList API key needed: # TRENDING_SOURCE_MOVIE=https://mdblist.com/lists/snoak/trending-movies # MDBList mirrors Trakt, Letterboxd, IMDb and others, so it is the easy way to # seed trending from a service PostersPlus does not integrate with directly. # # TMDB-shaped — any URL returning {"results": [{"id": 1234}, ...]}, which is # how you get a regional list that TMDB's /trending endpoint cannot express: # TRENDING_SOURCE_MOVIE=https://api.themoviedb.org/3/discover/movie?api_key=KEY®ion=FR&sort_by=popularity.desc # # Entries must carry numeric TMDB ids; IMDb-keyed lists are rejected rather than # imported. Nothing checks that a list is genuinely "trending". If a source is # unreachable, malformed, or yields nothing usable, the error is logged and NO # trending is served for that media type until the next refresh — it does not # quietly fall back to TMDB, so a broken URL is visible instead of looking fine. TRENDING_SOURCE_MOVIE= TRENDING_SOURCE_TV= # ============================================================================= # Cache warming # ============================================================================= # Pre-populate the TMDB and MDBList caches for trending, popular and top-rated / # now-playing / on-the-air titles, so the first real request for a hot title is # already warm. ON by default in the ElfHosted fork (upstream: off); set false # to opt out, e.g. if your keys' rate limits or disk are tight. CACHE_WARM_ENABLED=true # Ceiling on API calls spent per warm cycle. Cache hits are free and do not count. CACHE_WARM_TMDB_BUDGET=2000 CACHE_WARM_MDBLIST_BUDGET=500 # Hours between the end of one warm cycle and the start of the next. # See CACHE_WARM_AT_HOUR in ADVANCED.md to pin cycles to a fixed local hour. CACHE_WARM_INTERVAL_HOURS=24 # ============================================================================= # Poster rendering # ============================================================================= # Burned-in-text detection runs in the foreground only for titles at or below # this TMDB vote count. Higher-vote assets render immediately and are scanned # later by an idle background queue. Raise for accuracy, lower for faster bursts # on a cold cache. Detection itself, and its thresholds, are in ADVANCED.md. # Burned-in-title detection. OFF by default in the ElfHosted fork: the detector # holds ~170MiB of RAM per process from startup. Set true to opt in. TEXTLESS_TEXT_DETECTION=false TEXTLESS_DETECTION_MAX_VOTES=3000 # ============================================================================= # Localisation # ============================================================================= # ISO 639-1 language code for title logos (en, fr, de, ja, ...). Falls back to # English when the preferred language has no logo. Can also be passed per-request # as a URL parameter — see the web configurator. DEFAULT_LOGO_LANGUAGE=en # =========================================================================== # ElfHosted fork — hosted-mode configuration (all opt-in; unset = upstream) # =========================================================================== # Storage backend (ElfHosted fork — hosted mode, opt-in) # ============================================================================= # Leave DATABASE_URL unset to use SQLite (upstream default — recommended for # private/single-user deployments). Set it to a postgresql:// URL to switch # the cache layer to Postgres for multi-replica hosted deployments. # Example: DATABASE_URL=postgresql://postersplus:secret@postgres:5432/postersplus DATABASE_URL= # Connection pool sizing (only used when DATABASE_URL is set). DB_POOL_MIN_SIZE=1 DB_POOL_MAX_SIZE=10 # ============================================================================= # Coordination backend (ElfHosted fork — hosted mode, opt-in) # ============================================================================= # Leave REDIS_URL unset to use in-process coordination (upstream default). # Set it to a redis:// URL to share MDBList backoff + background-fetch claims # across replicas. Render coalescing stays per-process either way. # Example: REDIS_URL=redis://redis:6379/0 REDIS_URL= # Prefix for Redis keys. Useful if multiple PostersPlus tenants share a # single Redis instance and you want their keyspaces separated. REDIS_KEY_PREFIX=postersplus # ============================================================================= # Blob store backend (ElfHosted fork — hosted mode, opt-in) # ============================================================================= # Leave OBJECT_STORE_URL unset to keep TMDB poster/logo bytes on the local # filesystem (upstream default). Set to an s3:// URL to route them to an # S3-compatible object store. # Example: OBJECT_STORE_URL=s3://postersplus-cache?endpoint=https://s3.us-east-005.backblazeb2.com # Credentials come from standard AWS env vars (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) # or the pod's IAM role. OBJECT_STORE_URL= # Optional public CDN URL — when set, hosted-mode storage can issue 302 # redirects instead of proxying bytes through the app. OBJECT_STORE_PUBLIC_URL= # ============================================================================= # Render concurrency (ElfHosted fork — Phase 4) # ============================================================================= # Deprecated alias for POSTER_RENDER_CONCURRENCY (see above). v1.2.0 grew its # own render-admission cap that gates the whole render pipeline — the upstream # API calls included — rather than just the Pillow encode this used to wrap, so # the upstream setting supersedes this one. Kept so deployments that already # set it are not silently ignored: any value above 0 overrides # POSTER_RENDER_CONCURRENCY. 0 = leave the upstream setting alone. RENDER_CONCURRENCY=0 # Seconds /poster and /p will wait for a render slot before returning 503 with # Retry-After. Upstream queues indefinitely, which is right for a private # instance and wrong for a public one, where a saturated queue just becomes # client timeouts with no signal to back off. Set to 0 to restore upstream's # wait-forever behaviour. Default 30s. RENDER_QUEUE_TIMEOUT=30 # ============================================================================= # Observability (ElfHosted fork — Phase 6) # ============================================================================= # Optional shared secret guarding /metrics. Unset = open. Bind the app # behind an ingress that gates /metrics if you don't want it public. METRICS_ACCESS_KEY= # Log output format: "text" (upstream default, human-readable) or "json" # (one JSON object per line — for Loki / Elasticsearch / etc.). LOG_FORMAT=text # ============================================================================= # Per-tenant rate limit (ElfHosted fork — Phase 8) # ============================================================================= # Maximum /poster requests per tenant per second. 0 = unlimited (upstream # default). Tenant identity: # * If the user supplies tmdb_key=… or mdblist_key=… in URL params, # tenant = sha256(key)[:16]. # * Otherwise tenant = "operator". # When exceeded the request gets 429 with Retry-After: 1. RATE_LIMIT_RPS=0 # ============================================================================= # Public preset tier (ElfHosted fork) # ============================================================================= # Expose anonymous /p/{preset}/{type}/{imdb_id}.jpg preset URLs. With this on, # /poster, /logo, /stats and /debug/* require ACCESS_KEY — and with NO # ACCESS_KEY set they are closed outright, so /p is the only way in. That is # the intended posture for a public instance. PRESET_ENABLED=false # Cache-Control max-age for a fully warmed preset render (seconds). PRESET_CDN_CACHE_TTL=86400 # Let /p warm its own MDBList ratings in the background on a miss. Needed on a # preset-only instance (nothing else fetches ratings there, so presets render # N/A); leave off where /poster traffic already warms the rating cache. PRESET_MDBLIST_FETCH=false # Anonymous /search + /resolve-imdb floor (requests/second), independent of # RATE_LIMIT_RPS. 0 disables. ANONYMOUS_TMDB_RPS=5 # Accept /poster requests carrying only an IMDb id, resolving tmdb_id # server-side (cached permanently). For clients like Nuvio that drop the URL # when {tmdb_id} is empty on IMDb-keyed catalogues. Off = upstream's 400. POSTER_RESOLVE_IMDB=false # ============================================================================= # Metadata API base URLs (ElfHosted fork) # ============================================================================= # Route JSON lookups through a shared caching proxy (e.g. ElfHosted's emdb). # Defaults are the public endpoints. A proxy that strips X-RateLimit-* headers # leaves the MDBList quota snapshot unknown (429s are still honoured). #TMDB_API_BASE=https://api.themoviedb.org/3 #MDBLIST_API_BASE=https://api.mdblist.com #TVDB_API_BASE=https://api4.thetvdb.com/v4 #ANILIST_API_URL=https://graphql.anilist.co #KITSU_API_BASE=https://kitsu.io/api/edge # ============================================================================= # Server # ============================================================================= # Number of Uvicorn worker processes. # Recommended: 1-2 for home use, up to (2 x CPU cores) for heavy load. # Increase as required for large/public instances. WORKERS=2 # ============================================================================= # AIOStreams integration (quality badges) # ============================================================================= # Base URL of your AIOStreams instance, e.g. https://aiostreams.example.com # Leave blank to disable quality badges. AIOSTREAMS_URL= # AIOStreams auth encoded as Base64, user:password AIOSTREAMS_AUTH= # Days to cache quality data for titles older than 2 weeks. # New titles always use a 1-day TTL. Increase to reduce AIOStreams traffic. QUALITY_OLD_CACHE_DURATION=90 # Max concurrent background quality fetches. # Caps burst load when many uncached titles appear at once. # More checks still happen, they just wait in a queue. QUALITY_BG_CONCURRENCY=5 # ============================================================================= # CDN / caching # ============================================================================= # Adds Cache-Control: public, max-age= to poster responses. # Set to 0 to disable (default; recommended unless you're using a CDN). CDN_CACHE_TTL=0 # JPEG output quality for composited posters (70–95). Higher = larger files. # Default: 85. Raise to 92 for pixel-peepers; lower to 75 to reduce storage/bandwidth. JPEG_QUALITY=85 # Seconds to keep a composited poster before re-rendering. # Default: 604800 (7 days) # Pruning happens automatically based on this value to avoid an evergrowing cache of dead content when users abandon configs COMPOSITE_CACHE_TTL=604800 # Maximum composite poster cache entries. 0 = no cap (rely on TTL alone). # I suggest setting this on a public instance, as a failsafe. COMPOSITE_MAX_ENTRIES=0 # ============================================================================= # Localisation # ============================================================================= # ISO 639-1 language code for title logos (e.g. en, fr, de, ja). # Falls back to English if the preferred language is unavailable. # This can also be passed in the URL as a parameter (see web configurator) DEFAULT_LOGO_LANGUAGE=en