4grayandClaude Opus 4.8 a5bb8dc25c fix(tmdb): series cast was the latest season only, not the show (#1242)
* fix(tmdb): series cast was the latest season only, not the show

TMDB documents a TV id's `credits` as the credits of the LATEST SEASON.
We requested exactly that and rendered it as "the cast", so every
long-running show lost every regular who had left: The Boys showed
whoever appears in the newest season, not the ensemble.

The TV details request now also appends `aggregate_credits`, which spans
the whole run — but per TMDB omits the newest season, so neither payload
alone is the cast. `unifiedTvCast` unions them: whole-run billing order
first, then people who appear only in the newest season, deduplicated by
person id. Characters come from the aggregate `roles[]` shape.

Deliberately NO cache-key bump. Rows cached before this simply lack
`aggregate_credits` and keep the previous behaviour until they expire,
which avoids invalidating every user's details cache twice — the roadmap
schedules one consolidated bump once the remaining append_to_response
additions (images, certifications, alternative_titles) land together.

Movies are untouched: /movie/{id} has no aggregate_credits and its
`credits` is already the full cast.

Tests: departed regulars retained, newest-season arrivals appended after
show billing order, characters read from roles[], no duplicates across
the two payloads, graceful fallback for pre-aggregate cache rows.

Refs docs/architecture/tmdb-roadmap.md A2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): reserve cast slots so newest-season arrivals survive the cap

The union was appended aggregate-first and then truncated to ten, so on
exactly the shows it was built for — long-running ones, where the
whole-run cast alone exceeds the limit — every newest-season arrival was
sliced back off. The original fixture had two aggregate members and
could not catch it.

unifiedTvCast now holds back up to three slots for the top-billed
arrivals instead of appending them where the cap discards them, and
gives the slots back when nobody is new.

Tests: a 12-member aggregate plus two arrivals keeps both arrivals and
top billing; an aggregate with no arrivals still gets all ten slots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(tmdb): split the series-cast suite out of the merge spec

The merge conflict resolution put both new describes back into
tmdb-merge.spec.ts, pushing it to 499 lines — past the 400-line
max-lines cap. The aggregate-credits suite moves to its own file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): stop the cast union from shrinking, and bound what it caches

Three follow-ups from a review pass over the aggregate-credits union:

- The reserved arrival slots were subtracted from the aggregate even when
  the aggregate was shorter than the cap, so a show with four regulars and
  five newcomers returned seven names instead of nine. The reservation is
  a floor for arrivals now, not a quota.
- An aggregate member's character came from the first role with any text,
  so a one-episode cameo could outrank the part the actor is known for.
  Pick the role with the most episodes.
- aggregate_credits carries a show's whole-run cast AND crew, and details
  payloads are cached verbatim — orders of magnitude of JSON for a list
  the merge truncates to ten people. Cache the billing-order prefix and
  drop the crew nothing reads.

Extracting the people-related helpers into tmdb-credits.ts keeps
tmdb-merge.ts under the line cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): keep the aggregate ids the arrival check depends on

Trimming the cached cast to its top 40 broke the property it was supposed
to preserve: `known` is built from the aggregate ids, so a returning actor
billed below the cut read as a new arrival on the cached path and took a
reserved slot. The same show then showed a different top ten on its second
open than on its first.

Keep the whole cast, and cut the two things nothing reads instead: the
aggregate crew, and every `roles[]` entry except the one the merge picks
(most episodes). A merge over the trimmed payload now provably returns
what a merge over the full one does — covered by a test that runs both.

Also points CLAUDE.md and the doc's module table at tmdb-credits.ts, where
the credit helpers now live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(tmdb): add the release note for the series-cast fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmdb): let the cache trim reuse the merge's own role choice

The trim picked the role with the most episodes; the merge picks the
NAMED role with the most episodes. TMDB uses unnamed roles for uncredited
appearances, so a member whose blank role outranked their real one lost
their character on every render after the first.

Both now call pickAggregateRole, which is the point — two copies of the
same choice are what let them drift.

Also adds a test pinning the property the earlier truncation defect broke:
the displayed cast is the cap or everyone available, whichever is smaller.
Which people make the cut at the cap is the reservation's job and is
deliberate; the count is not negotiable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(tmdb): state the aggregate-credits contract as TMDB actually words it

TMDB describes the endpoint in one sentence that contradicts itself: "it
does not return the newest season. Instead, it is a view of all the entire
cast & crew for all episodes belonging to a TV show." The doc and the code
comment asserted the first half as settled fact.

The union never depended on that reading — arrivals are a set difference,
so under "whole run" they are simply empty — but the comment implied an
assumption the code does not make. Say what TMDB says, note the ambiguity,
and note why either reading is safe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 01:55:44 +02:00
2019-10-06 17:34:49 +02:00
2023-01-23 22:42:23 +01:00
2020-09-27 20:33:37 +02:00
2025-10-07 18:02:13 +02:00
2021-02-07 00:19:57 +01:00
2020-09-27 20:33:37 +02:00
2020-09-27 20:33:37 +02:00

IPTVnator - IPTV Player Application

IPTVnator icon

Release CI status Releases Codecov Telegram Bluesky

🌐 Website | Telegram channel for discussions | Buy me a coffee | GitHub Sponsors

IPTVnator is a video player application that provides support for IPTV playlist playback (m3u, m3u8). The application allows users to import playlists using remote URLs or by uploading files from the local file system. Additionally, it supports EPG information in XMLTV format which can be provided via URL.

The application is a cross-platform, open-source project built with Electron and Angular.

⚠️ Note: IPTVnator does not provide any playlists or other digital content. The channels and pictures in the screenshots are for demonstration purposes only.

Important

Official sources only. IPTVnator is a free, open-source player — it never sells IPTV subscriptions, channels, or playlists. Websites offering "IPTVnator subscriptions/channels/premium/activated" builds are not affiliated with this project. Get the app only from the official website or GitHub Releases. See Beware of unofficial IPTVnator websites and IPTV services for details.

IPTVnator: Channels list, player and epg list

Features

Playlists & sources

  • M3U / M3U8 playlists from local files or remote URLs 📂, with automatic updates on startup
  • Xtream Codes (XC) and Stalker / Ministra (STB) portal support
  • Custom "User-Agent" header per playlist

Playback

  • Built-in HTML5 player (HLS.js or Video.js) with a resizable, resumable inline view
  • Optional unified IPTVnator controls for HTML5, Video.js, and ArtPlayer, enabled in Settings → Playback (experimental)
  • External players — MPV, VLC, and IINA on macOS (mpv.app / VLC.app bundle paths supported) (desktop)
  • Embedded MPV — native mpv rendered inside the app window on macOS, Windows & Linux 🖥️ (experimental · desktop)
  • Dedicated radio player for radio="true" streams 📻

Live TV & EPG

  • EPG / XMLTV TV guide with a live timeline ribbon and multi-channel grid (desktop)
  • TV archive / catch-up / timeshift (desktop)
  • Group-based channel list, channel-number selection, and search 🔍

Movies & series (VOD)

  • Redesigned two-state detail pages (browse ↔ watch) with season tabs and resume positions
  • Download manager for offline movies & episodes ⬇️ (desktop)
  • "Recently added" feeds and category grids with sorting & pagination

Discovery & metadata

  • Global search across live TV, movies, and series (desktop)
  • TMDB enrichment (opt-in) — plots, cast & crew, trailers, ratings, artwork, a "Similar" rail, clickable actor pages, and a trending dashboard rail (trending rail: desktop)
  • Dashboard with recently watched & continue-watching

Organization

  • Per-playlist and global favorites, aggregated across all playlists ⭐
  • Recently viewed / watch history
  • Command palette (Ctrl/Cmd+K)

Platform

  • Cross-platform desktop (Electron) and installable PWA
  • Desktop auto-updater and mobile remote control (desktop)
  • Docker self-hosting for the PWA + web backend
  • 19 languages (translation files), light & dark themes, and keyboard shortcuts

Keyboard shortcuts

Press ? or Shift+/ in the workspace to open the in-app shortcuts list.

Area Shortcut Action
Global Ctrl/Cmd+K Open command palette
Global Ctrl/Cmd+F Open global search in the desktop app
Global Ctrl/Cmd+R Open recently viewed in the desktop app
Global Enter in workspace search Submit the current search
Navigation Ctrl/Cmd+B Toggle the live sidebar
Navigation 0-9 Select an M3U channel by number
Playback Space / K Play or pause embedded MPV playback in the desktop app
Playback F Toggle embedded MPV fullscreen in the desktop app
Playback ArrowLeft / ArrowRight Seek embedded MPV playback by 5 seconds in the desktop app
Playback ArrowUp / ArrowDown Adjust volume by 5%
Playback M Mute audio
Dialogs and lists ArrowUp / ArrowDown Move command palette selection
Dialogs and lists Enter Run the selected command or open a focused item
Dialogs and lists Escape Close dialogs and dismiss overlays

Screenshots:

Dashboard with recently watched content Live channels with inline player and EPG
Dashboard with recently watched content Live channels with inline player and EPG
Add playlist dialog for M3U, Xtream, and Stalker Live category channel list
Add playlist dialog for M3U, Xtream, and Stalker Live category channel list
Global search across live TV, movies, and series Manage visible live categories
Global search across live TV, movies, and series Manage visible live categories
Movie category grid with sorting and pagination Recently added movies and series
Movie category grid with sorting and pagination Recently added movies and series
VOD details with playback and download actions Download manager
VOD details with playback and download actions Download manager
Multi-channel EPG grid External MPV player support
Multi-channel EPG grid External MPV player support
Radio playback with dedicated audio player Light theme
Radio playback with dedicated audio player Light theme
Application settings
Application settings

Note: First version of the application which was developed as a PWA is available in an extra git branch.

Self-hosted PWA

The Docker setup builds the Angular PWA and the monorepo web backend into one image. The backend handles remote M3U parsing plus Xtream and Stalker proxy requests under /api, so a separate 4gray/iptvnator-backend container is not required for the default self-hosted flow.

docker compose -f docker/docker-compose.yml up --build -d

The application is available at http://localhost:4333. See docker/docker-compose.yml for the ready-to-run compose file and docker/README.md for environment variables, reverse proxy notes, PWA limitations, and build details.

The self-hosted image runs the browser PWA rather than the Electron desktop app: EPG/XMLTV panels, Embedded MPV, managed MPV/VLC launching, the download manager, and Electron remote-control features are not available there. If browser playback fails, copy the stream URL and open it manually in an external player such as MPV, VLC, or IINA.

Download

Download the latest version of the application for macOS, Windows, and Linux from the release page.

Alternatively, you can install the application using one of the following package managers:

Homebrew

$ brew install iptvnator

Snap

$ sudo snap install iptvnator

Arch

Also available as an Arch PKG, iptvnator-bin, in the AUR (using your favourite AUR-helper, .e.g. yay)

$ yay -S iptvnator-bin

Gentoo

You can install IPTVnator from the gentoo-zh overlay

sudo eselect repository enable gentoo-zh
sudo emerge --sync gentoo-zh
sudo emerge iptvnator-bin

Linux Embedded MPV Support

Embedded MPV on Linux is experimental and currently supports x64 desktop sessions where IPTVnator runs under X11 or Xwayland. Native Wayland embedding is not supported yet. Linux package launchers request X11 with --ozone-platform=x11, so Wayland desktops still need Xwayland available.

The Linux backend starts a system mpv executable with --wid, so mpv must be installed and available on PATH. CI validates the Linux native addon and standard packages on Ubuntu 22.04, with Flatpak packaging built on Ubuntu 24.04. Expected user targets are Ubuntu/Debian .deb, Arch/Manjaro pacman, RPM distributions, and AppImage on x64 systems with X11/Xwayland plus mpv installed. Flatpak and Snap builds remain available, but embedded MPV is not announced as supported there yet because those sandboxed formats do not expose the host mpv executable to the embedded backend by default.

Get it from the Snap Store

Sponsor on GitHub Support on Ko-fi

Troubleshooting

macOS: "App is damaged and can't be opened"

Older unsigned macOS builds may require removing the quarantine flag from the downloaded application:

xattr -c /Applications/IPTVnator.app

Alternatively, if the app is located in a different directory:

xattr -c ~/Downloads/IPTVnator.app

Linux: chrome-sandbox Issues

If you encounter the following error when launching IPTVnator:

The SUID sandbox helper binary was found, but is not configured correctly.
Rather than run without sandboxing I'm aborting now.
You need to make sure that chrome-sandbox is owned by root and has mode 4755.

Solution 1: Fix chrome-sandbox permissions (Recommended for .deb/.rpm installations)

Navigate to the IPTVnator installation directory and run:

sudo chown root:root chrome-sandbox
sudo chmod 4755 chrome-sandbox

Solution 2: Launch with --no-sandbox flag

Edit the desktop launcher file to add the --no-sandbox flag:

  1. Find your desktop file location:

    • Ubuntu/Debian: ~/.local/share/applications/iptvnator.desktop
    • System-wide: /usr/share/applications/iptvnator.desktop
  2. Edit the file and modify the Exec line:

    Exec=iptvnator --no-sandbox %U
    
  3. Save the file and relaunch the application from your application menu.

Alternatively, you can launch IPTVnator from the terminal with the flag:

iptvnator --no-sandbox

GNU/Linux: Wayland startup failure

If IPTVnator exits on GNU/Linux with errors about failing to connect to Wayland or initialize the Ozone platform, force X11/XWayland instead:

iptvnator --ozone-platform=x11

This workaround is mainly for older or problematic Linux graphics stacks. The Snap package already includes this X11 override by default. For AppImage, direct binaries, and other Linux package formats, pass the flag manually when needed.

How to Build and Develop

Requirements:

  • Node.js with pnpm (via Corepack)
  1. Clone this repository and install project dependencies:

    $ corepack enable
    $ pnpm install
    
  2. Start the application:

    $ pnpm run serve:backend
    

This will open the Electron app in a separate window, while the Angular dev server will run at http://localhost:4200.

The equivalent Nx command is:

$ nx serve electron-backend

To start Electron with an empty, isolated data directory instead of your normal ~/.iptvnator folder, set IPTVNATOR_E2E_DATA_DIR for that run:

$ rm -rf .tmp/iptvnator-empty && mkdir -p .tmp/iptvnator-empty
$ IPTVNATOR_E2E_DATA_DIR="$PWD/.tmp/iptvnator-empty" pnpm run serve:backend

This redirects the SQLite database, Electron user data, and local config under the given directory. Delete that directory whenever you want a fresh empty state.

If you need startup diagnostics for a white screen or a frozen route, you can also turn on opt-in Electron tracing. These logs are written to the Electron terminal output so they still help when the renderer DevTools never open:

$ IPTVNATOR_TRACE_STARTUP=1 pnpm run serve:backend

Nx equivalent:

$ IPTVNATOR_TRACE_STARTUP=1 nx serve electron-backend

Useful narrower flags:

  • IPTVNATOR_TRACE_IPC=1 logs renderer window.electron.* calls reaching the Electron bridge
  • IPTVNATOR_TRACE_DB=1 logs DB worker requests and request-scoped DB events
  • IPTVNATOR_TRACE_SQL=1 logs SQLite statements in both the main connection and DB worker connection
  • IPTVNATOR_TRACE_WINDOW=1 logs BrowserWindow load, navigation, and unresponsive events
  • IPTVNATOR_TRACE_RENDERER_CONSOLE=1 mirrors renderer console messages into the Electron terminal output

Security-sensitive network compatibility flags are opt-in:

  • IPTVNATOR_ALLOW_PRIVATE_NETWORK_URLS=1 permits strict EPG fetches from playlist metadata (x-tvg-url, url-tvg, or tvg-url) to resolve to localhost, LAN, or other private addresses. Directly configured Xtream/Stalker portals and private playlist servers remain supported without this flag. Prefer the in-app source-scoped “Allow source” action for a trusted EPG URL.
  • IPTVNATOR_ALLOW_INSECURE_TLS=1 disables certificate validation for remote playlist imports and refreshes for the whole Electron process. Prefer the in-app host-scoped trust action for a trusted provider with a self-signed or otherwise invalid certificate.

If the local Nx daemon gets into a bad state before rerunning Electron, reset it:

$ pnpm nx reset

To run only the Angular app without Electron, use:

$ pnpm run serve:frontend

Disclaimer

IPTVnator doesn't provide any playlists or other digital content.

Trademark

The name "IPTVnator" and the IPTVnator logo are unregistered trademarks of the project owner. The MIT license covers the source code only — it does not grant rights to the name or logo. Forks and redistributions (including app-store submissions) must use a different name and their own icon. See TRADEMARK.md for details.

All Contributors

Languages
TypeScript 81.4%
JavaScript 8.9%
HTML 3.3%
SCSS 2.8%
Astro 1.3%
Other 2.1%