Skip to content

Prismarr v1.3.0

Latest

Choose a tag to compare

@github-actions github-actions released this 21 Oct 22:08

Security

  • PHP2Shell: the image now ships PHP 8.4.27 (CVE-2026-109618, CVE-2026-109619). Every earlier Prismarr image, 1.2.0 and the 1.3.0 betas included, ran PHP 8.4.2, which is affected by two critical flaws that let an unauthenticated request execute code on the server. The base image moves from FrankenPHP 1.3.6 to 1.13.2, which also fixes four FrankenPHP issues of its own (one of them could leak a previous request's errors to the next one in worker mode). As an extra layer, session.upload_progress.enabled is now off (Prismarr never used it). Update as soon as possible, especially if your instance is reachable from the internet.

Added

  • Search page (formerly Discover). The page is now built around one search box: movies, series and people in a single query, ranked so exact titles come first, accents and case are ignored, and a year typed after the title ("dune 2021") picks the right one. Half-typed words with accents work too ("Pokém" finds Pokémon), and an obscure exact title no longer outranks a famous one that starts the same way. A mistyped query gets a Google-style correction from a daily vocabulary of well-known titles and people built in the background ("pokumon" → Pokémon, "the mtrix" → The Matrix, "cristopher nolan"): when nothing or only obscure titles match, the corrected search runs straight away ("Showing results for…", with a link to search the original spelling), otherwise a "Did you mean" link is offered. Searching a known person's name puts them in the top card with a link to their filmography; obscure namesakes are left out. Inline filters (type, genre, years, rating, sort, hide what you already have) also work without a query to browse the catalogue. The top hit gets a large card with its backdrop, genres, overview and library status; people open their filmography. The trending movie and series banners, recommendations and rows stay on the page when the search box is empty. The sidebar entry, its icon and the page's other labels are renamed Search; the /decouverte URL is unchanged.
  • Optional Bazarr integration for subtitles (#103). Configure a Bazarr URL and API key in /admin/settings → Services and an admin-only Subtitles entry appears in the sidebar with four views: Wanted (badge counters, a "most missing" poster row and the wanted movie/episode lists), Movies and Series (searchable, filterable, lazily-rendered poster grids over Bazarr's own lists), and History. Every in-library title also gains a subtitle-status badge — on the Films and Series grids and tables, in the quick-look modal, in the film/series detail modals (with per-language present/missing chips) and in Ctrl+K results — and clicking a "missing" badge opens a subtitle search you can download from in place, per movie or per episode. The badge read path never talks to Bazarr during a page render: a full-library fetch takes seconds and the Films page renders hundreds of badges, so the datasets are served stale-while-revalidate from cache.app and rebuilt in the existing messenger-worker service, at most one Bazarr list fetch per dataset per minute. A cold cache renders a "warming" placeholder that fills itself in rather than blocking, a mutation patches just the acted-on item, and everything fails closed — an unreachable or unconfigured Bazarr shows the standard service banner, never a 500. The whole feature is off unless configured, and has its own kill switch alongside the other services.
  • Transmission dashboard widget (optional). A new "Transmission transfers" section, available in the dashboard layout editor (reorder / hide) and shown only when Transmission is configured: download / seeding / paused counts, global download and upload speeds, and the five fastest torrents currently downloading with their progress, plus a link to the Transmission page. It hydrates after first paint, refreshes every 10 s (paused while the tab is hidden), and falls back to an "unreachable" message instead of breaking the dashboard when Transmission is down.
  • Tracearr activity page (optional). A read-only integration with Tracearr, the Plex / Jellyfin / Emby monitoring tool (a Tautulli alternative). Configure the URL and a public API key (trr_pub_…, generated in Tracearr → Settings) in /admin/settings. It has a kill switch, a Test connection button, a dashboard health chip and the same circuit breaker as the other services. A dedicated Tracearr page (own sidebar entry) shows now-playing cards refreshed every 10 s, today / 30-day overview tiles, plays-over-time, by-hour, by-day-of-week, per-platform, stream-quality and concurrent-stream charts with a 7-day / 30-day / 1-year toggle, a paged watch-history grid and a users table. Only GET endpoints of the public API v1 are used (never stream termination). The key never leaves the server, responses are allow-list sanitised (no IPs, user ids or raw codec payloads), posters go through a server-side, SSRF-guarded image proxy, and every section fails open independently. The dashboard's activity widget (now playing + recently watched) and the sidebar streaming pill can be fed by Tracearr: a new Live activity source display setting (Automatic — Tautulli first —, Tautulli or Tracearr) picks the service when both are configured, falling back to the other one if the chosen service isn't set up; fed by Tracearr, the widget reads "Current media activity" (it also covers Jellyfin / Emby). Now-playing cards and history entries open the app-global quick-look (TMDb id from Tracearr's v2 public API — episodes resolve their show's id, cached a day — with a v1 fallback for older Tracearr), cards get a Plex / Jellyfin / Emby badge, history pages through v2 cursors, and the users table derives "Last seen" from the 100 most recent plays when Tracearr doesn't report it.
  • Emby activity widget (optional). A "Current Emby activity" dashboard card fed directly by Emby Server's Sessions API, for people who run Emby instead of Plex. Configure the server URL and an API key under /admin/settings → Monitoring (per-service kill switch, Test connection and health chip like every other service). The card is the Plex/Tautulli widget with an Emby source: one row per active stream with poster, user, client, Direct Play / Direct Stream / Transcode badge, LAN/WAN, resolution, HDR / Dolby Vision, bitrate and a progress bar, refreshed every 10 s, and a click on a title opens the TMDb quick-look. Read-only: nothing can pause, stop or change playback. The API key stays server-side (posters are proxied), and the payload is reduced to an allow-list before it reaches the browser, so remote addresses, device ids, tokens and file paths never leave the server.
  • Episode quality and custom-format score in the series episode list (#39). The downloaded quality was only visible by hovering the per-episode info icon, and the custom-format score wasn't shown at all. Each downloaded episode now shows its quality as a badge inline in the list, plus the custom-format score when it is non-zero (coloured green/red, mirroring the interactive-search table; a score of 0 is hidden to avoid a column of zeros). The info-hover tooltip still carries the full media detail (codec, resolution, audio, size, release group). No API changes — Sonarr's episode-file payload already includes the quality, custom formats and score.
  • Default quality profile per Radarr / Sonarr instance (#5). Each instance's edit form in /admin/settings gains a "Default quality profile" picker (its options load live from that instance's Radarr/Sonarr when the modal opens). When set, that profile is preselected in the quick-add, suggested-add and library add forms instead of whichever profile the service happens to return first; leaving it on "None" keeps the previous behaviour, so existing installs are unaffected. The default is stored per instance (a new nullable default_quality_profile_id column + migration) because profile ids aren't shared across instances, and a server-side fallback applies it to programmatic adds that don't specify a profile.
  • Interactive search links release titles to their tracker page (#35). When the indexer reports an info URL, the release title opens it in a new tab (http/https only).
  • Grab Prowlarr search results to the download client (#71). Each result on the Prowlarr search page gets a grab button that hands the release to the client configured in Prowlarr.
  • SABnzbd / NZBGet recent history inline (#47). The downloads page lists the latest completed and failed jobs under the queue.
  • Season/episode code on Plex episode rows. Now playing and history rows in the Tautulli views show S01E02 next to the show name.
  • Every configured service in both health views. The topbar dropdown and the dashboard "Services health" widget now read one shared service list, so Deluge, SABnzbd, NZBGet, Tautulli, Tracearr, Emby, Bazarr and Transmission show up in both when configured (previously each view had its own partial list, and Transmission was counted in the topbar total but never shown). Dashboard chips also show each service's logo.

Performance

  • The Radarr/Sonarr library is served stale-while-revalidate, from one shared entry. The 45 s MediaLibraryCache window still expired inside a request, so whoever arrived first after it lapsed paid the full multi-second getMovies() / getSeries() fetch before anything rendered — and the dashboard kept its own second copy of the same payload (dash.movies / dash.series) with no invalidation hook, so a change made on the Films page could stay invisible on the dashboard for the rest of the window. A new shared StaleWhileRevalidateCache splits the window in two: 45 s is now the soft TTL (a lapsed entry is served immediately and a rebuild is queued to the already-running messenger-worker service, so the refetch happens outside the request) and 600 s is the hard ceiling past which a read still blocks, because the library pages cannot render without the list. The dashboard now reads the same per-instance entry as the library pages, so the three duplicate fetch paths collapse into one and existing library invalidations finally reach the dashboard. Measured on a large library: the Films page's cold load drops from ~11.5 s to ~0.32 s and the dashboard's recent-additions fragment from ~3.8 s to ~0.25 s. An empty or failed fetch is still never cached, a mutation still invalidates hard (the next read refetches), a refresh is skipped for an instance whose circuit breaker is open, and a rebuild that goes unanswered for 180 s logs one rate-limited error — the visible symptom of a stopped worker.
  • The Ctrl+K search index is normalized once, not on every keystroke. /search transliterated three fields (title, originalTitle, sortTitle) on every row of the whole library on every request, then re-normalized both sides of every comparison in its usort() — so the accent-insensitive matching that makes the dropdown pleasant to type into was being recomputed from scratch for each character typed. The Any-Latin; Latin-ASCII; Lower() work now happens once per 60 s index build, with a single reusable Transliterator instead of the procedural transliterator_transliterate() (which re-parses the compound rule id on every call); matching is then plain str_contains over the pre-normalized fields, the comparator reads the pre-normalized title, and the helper fields are stripped before the JSON so nothing extra reaches the DOM. The index itself no longer runs its own second /api/v3/movie + /api/v3/series fetch either — it reads the shared MediaLibraryCache entry, so search results can no longer drift out of sync with the Films/Series pages. Measured on a large live library: a warm Ctrl+K query drops from 1.6–5 s to 13–31 ms (isolated, without HTTP overhead: 0.8–3.3 s to ~3 ms). Result order is unchanged (titles starting with the term first, then alphabetical, capped at 12) and the cache keys bump to _v3, since a cached index built before this change would silently match nothing.
  • OPcache preloading of the framework + app class graph. php.ini now sets opcache.preload to Symfony's config/preload.php (backed by the compiled var/cache/prod preload file that the image's build-time cache:warmup already generates), so ~1,300 framework/app classes are linked into shared memory once at boot instead of on first use. FrankenPHP reports PHP_SAPI as frankenphp (not embed), so Symfony's preload file runs rather than short-circuiting — verified in a fresh container (1336 scripts preloaded, clean boot, health endpoint OK). Since the code is baked into the image and opcache.validate_timestamps=0, the preloaded graph stays valid for the container's lifetime.
  • Opt-in FrankenPHP/Symfony worker mode (PRISMARR_WORKER). Setting PRISMARR_WORKER=1 boots the Symfony kernel once and keeps it resident, so requests skip the per-request kernel bootstrap (the single biggest remaining per-request cost). It is OFF by default — unset, the container runs exactly as before in classic single-request mode. PRISMARR_WORKER_NUM optionally pins the worker thread count (default: FrankenPHP's 2× CPU). No image rebuild is needed to toggle it. Symfony's default runtime natively drives FrankenPHP's worker loop, so no APP_RUNTIME or extra Composer package is involved. Correctness across the shared-kernel boundary relies on request-scoped services implementing ResetInterface (Symfony's services_resetter clears their per-request state between requests); this change adds it to HealthService (in-process health memo + shared-pool generation token) and DashboardController (per-request movie/series library memo), which also removes a latent staleness edge in classic mode.

Changed

  • Mercure hub config moved to the Mercure 1.0 syntax. FrankenPHP 1.13 ships Mercure 1.0, which no longer accepts transport_url or the top-level publisher_jwt / subscriber_jwt; the Caddyfile now uses transport bolt and an issuer prismarr block (strict token validation). Prismarr itself does not publish or subscribe yet, so nothing changes for users.
  • Transmission now shows its real logo. The sidebar entry and the Transmission page title use the Dashboard Icons transmission.svg (already shipped for the admin settings card) instead of the generic red droplet, and Transmission gets its own chip — with its logo — in the dashboard's services-health widget.
  • Every service in the sidebar shows its own logo. Radarr, Sonarr, Prowlarr, Seerr, qBittorrent, Deluge, SABnzbd, NZBGet, Tautulli, Bazarr and Tracearr use their logo instead of a generic icon, and the Deluge, Bazarr, Emby and Tautulli logos are now the official marks from Dashboard Icons instead of hand-drawn stand-ins.
  • Setup wizard rebuilt on stock Tabler primitives. The first-run wizard's bespoke step/status/section markup and ~268 lines of custom CSS are replaced with Tabler's own steps, status, hr-text and input-group components, so the wizard inherits the same theme, spacing and dark-mode behaviour as the rest of the app instead of maintaining a parallel style. No change to the wizard's fields, validation, per-service connection tests or flow — purely a markup/CSS consolidation.
  • Library filters collapse into a drawer on mobile. On the Films, Series and Discover pages the filter controls used to sit in an always-visible toolbar that ate a large slice of the screen above the poster grid on phones. Below 992px the Films/Series filter card is now a Bootstrap offcanvas drawer opened by a Filtres (n) button (n = active-filter count) — search, status, and the dropdowns move into the drawer, and an Apply button runs a single navigation so changing filters no longer reloads the page (and closes the drawer) on every tap; at ≥992px the toolbar renders inline exactly as before (same live auto-submit, unchanged desktop). Discover's existing Filters panel becomes a slide-in drawer at all widths, reusing its Apply/Reset/count logic. Same responsive offcanvas markup drives both states, so there is one source of truth per page.
  • Shorter first-run wizard. Setup now asks for the interface language and the admin account only, then opens /admin/settings on the Services section, where every service is configured and tested. The per-service wizard steps (TMDb, managers, indexers, downloads, finish) and their test endpoint are gone, so there is one place to configure services instead of two.
  • Redesigned Services settings. Connected services are grouped, collapsible rows showing each service's URL and live status, with fields, Test and the enable switch one click away; Radarr / Sonarr expand to a compact instance list. Services you haven't set up are listed as collapsed rows (one click opens their fields) instead of empty forms full of placeholder URLs, and a configured service can be removed in one click (its fields, secrets included, are wiped on save; Undo before saving). A search box filters the list, and the save bar counts unsaved changes.

Fixed

  • Bulk delete works again on Films, and honours the exclusion box on Series. The Films multi-select delete called Radarr's non-existent /api/v3/movie/bulk (a 404, so nothing was deleted); it now uses /api/v3/movie/editor with Radarr's addImportExclusion key. The Series bulk delete already hit the right endpoint but sent addImportExclusion instead of Sonarr's addImportListExclusion, so the "add import list exclusion" option was silently ignored.
  • Settings: the Tautulli card's Test button always answered "Fill in the URL and credentials first" because the service was missing from the Test button's field map; it now tests the URL and API key like the other cards.
  • Series shelf view escapes titles and posters. The shelf cards re-inserted the series title and poster URL as raw HTML; they are now escaped like the Films shelf.
  • Poster proxies only pass raster images. The Tautulli (and new Emby / Tracearr) poster proxies served any image/* type from Prismarr's own origin, SVG included; they now accept JPEG, PNG, WebP, GIF, AVIF and BMP only.
  • Release dates, certifications and watch providers were pinned to French regions for everyone. The Discover/quick-look TMDb lookups picked release dates, content ratings, alternative titles and streaming providers from a hardcoded FR-first country list (e.g. ['FR', 'BE', 'LU', 'US', 'GB']), so an English-locale install saw French theatrical dates and provider availability instead of its own. Region priority is now derived from the active locale — a new TmdbClient::regionPriority(locale, append) leads with the locale's home region (en → US/GB/CA/AU, fr → FR/BE/LU/CA, plus es/de/pt/it), falls back through a broad common chain, and appends any extra countries actually present in the payload, de-duplicated and order-preserving. Wired at all six lookup sites; covered by a unit test.
  • SABnzbd actions could report a false success. action() (pause/resume/delete/speed-limit/add) defaulted an absent status key to true, so any unrecognised HTTP 200 — from a reverse proxy or a different SABnzbd version — was reported to the user as a successful action even though nothing happened. Pure SABnzbd actions always carry a status flag; its absence is now treated as failure and logged, matching NzbgetClient's existing strictness.
  • Transmission detail panel always showed 0 B/s. getTorrentDetail() read rateDownload/rateUpload off the torrent object for the General tab's speeds, but the torrent-get RPC's field list (DETAIL_FIELDS) never requested them — and Transmission only returns fields that were explicitly asked for, so both coalesced to 0 regardless of actual transfer speed. The two fields are now included in DETAIL_FIELDS; a reflection-based test asserts the list contains every field the detail mapper reads.
  • Radarr / Sonarr library pages report the instance unreachable even though the connection test passes (#41). Two-part fix. (1) The breaker no longer confuses "slow" with "down": the heavy getMovies() / getSeries() payload is fetched through the shared get(), and a read timeout there used to execute the same mark-down branch as a connection failure — so a big library on slow storage tripped the per-instance circuit breaker and every subsequent call short-circuited to "unreachable", while the lightweight connection test hit a fast endpoint and passed. get() and multiGet() now distinguish the two: a timeout after a successful connect fails only that call (and still short-circuits the rest of the same request to avoid stacking waits) but never opens the cross-request breaker; connection-level failures (refused / DNS / connect timeout) trip it exactly as before. (2) The library routes get room to finish: the full-library fetches accept a per-call timeout, and the library/collections/filter/search-index routes — which already allow set_time_limit(120) — pass a generous 30 s (LIBRARY_TIMEOUT). Frequent paths (dashboard widgets, quick-look membership checks, admin statistics) keep the old 8 s / 4 s bound so one slow instance can't pin their requests for 30 s.
  • Dead space in the External Services settings cards. The service-card grid on the admin settings "External services" tab allocated every CSS-grid row track to its tallest card, so a short card left an empty gap beside/below a tall neighbour and an odd last card left the whole second column empty. The grid is now CSS multi-column packing (columns: 2 320px), which flows cards into balanced stacks with no row tracks — cards pack tightly by height. Responsive behaviour is unchanged (drops to one column under ~660px); the multi-instance Radarr/Sonarr cards still span both columns via column-span: all.
  • The Display date/time preference is honoured everywhere (#54). Pages that still formatted dates with a fixed locale now use the date and time format chosen in Settings.
  • Dashboard greeting follows the display timezone (#112). The "Good morning / afternoon / evening" line and weekday used the PHP process timezone instead of the one set in Settings.
  • Reverse-proxy sub-path respected in JS-built URLs. Hardcoded root-relative links and fetch calls across the templates now go through the base path, so Prismarr works when mounted under a sub-path.
  • Series queue and header escape indexer and Sonarr strings. Release names, episode titles, status messages, network, certification and genres were inserted as raw HTML on the Series page.
  • "Not configured" links no longer bounce back to the home page. Opening an unconfigured service, its banner button, or a card on the empty-home welcome page pointed at a wizard step that redirects home once setup is done; they now open the Services settings for admins and the home page for other users.
  • A short TMDb outage no longer leaves searches and lists empty. A failed TMDb call was cached as an empty answer for the whole cache lifetime (10 minutes for a search, an hour for a list); it is now retried on the next request.
  • The detail modal no longer opens several times on the Search page. Coming back to the page through the sidebar added its click handlers again on every visit, so one click opened the detail modal (or the quick-add) once per previous visit.