4grayandClaude Fable 5 00aa623b83 fix(downloads): reconnect interrupted transfers and resume without validators (#1446)
* fix(downloads): reconnect interrupted transfers and resume without validators

Xtream panels commonly kill each VOD connection after a byte/time burst
(~130-260 MB) and send no ETag/Last-Modified. The validator-only resume
path then deleted the partial and surfaced a raw "aborted" failure, so
every Retry restarted from zero and large files could never finish.

- Resume without a validator through overlap verification: the Range
  request rewinds by 256 KiB and the replayed window must match the
  partial's tail byte-for-byte before anything is appended; a mismatch
  truncates the partial and restarts from scratch (download-overlap.ts).
- Reconnect automatically on recoverable interruptions and clean short
  responses (download-reconnect.ts): progress >=64 KiB past the best
  attempt resets a 3-stall budget; request-phase failures during
  reconnects are converted into retained interruptions so automation can
  never delete a partial.
- Extract pure response-header helpers into
  download-resume-validation.ts to keep download-transfer.ts within the
  file-size guideline.

Verified against a real throttling portal: a 1.6 GB and a 3 GB movie
completed through 15 and 23 connection resets respectively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): address review findings on reconnect baseline and small partials

- Judge reconnect progress against the previous attempt instead of a
  high-water mark, so a transfer that legitimately restarted from byte
  zero mid-loop (overlap mismatch, ignored Range) is measured by its
  rebuilt file; at most two such regressions are tolerated per transfer
  to keep the loop structurally bounded (Greptile P1).
- Floor reported progress at the partial's retained size while
  appending, so a response that ends inside the overlap window can never
  move persisted progress backwards (Greptile P1).
- Verify partials smaller than the overlap window in full from byte zero
  and append, instead of rewriting the .part in place — an early-dying
  reconnect can now only grow the file (Codex P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): keep tolerated regressions off the reconnect stall budget

A tolerated restart regression consumed a regression credit AND counted
as a stalled attempt (its negative delta is below the progress
threshold), so a legitimately rebuilt file that grows in sub-64 KiB
steps was failed one reconnect early. Regressions are now charged to
their own bounded budget only; the stall budget stays reserved for
attempts that genuinely fail to grow the file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): gate success and validator promotion on complete overlap verification

Round-3 review findings:

- Success is rejected while the overlap verifier has not consumed its
  entire window: a complete 206 that ends inside the window proves the
  remote entity shrank, so the transfer truncates and restarts from
  scratch instead of finalizing the old suffix as a completed file; an
  early-dying stream stays an ordinary retained interruption (Codex P1).
- A verify-append attempt promotes the response's ETag/Last-Modified
  only after the complete overlap matched — an unverified partial is
  never blessed with a validator the next resume would If-Range-append
  onto (Codex P1).
- A tolerated restart regression resets stalls accumulated against the
  discarded representation, so the rebuilt file starts with the full
  stall budget (Greptile P1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): carry the known total through total-less reconnect responses

A resumed response without a usable total (chunked, or an unsatisfiable
Content-Range) erased task.totalBytes, so a reset over that response
could no longer classify as a retained interruption and generic cleanup
deleted the verified partial. The previously learned total is now
carried forward for appending attempts; fresh and restarted transfers
still drop it, since it described a discarded file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): never fabricate a total for an unverified retained partial

A retained failure with an unknown total persisted
totalBytes = bytesDownloaded, so after stalled reconnects over a
chunked, validator-less response that kept ending inside the overlap
window, Retry's completed-partial shortcut saw the .part size equal the
fabricated total and finalized the unverified partial without a request.
The fallback is now explicit per call site: only a finalization failure
after a complete transfer records its byte count as the total; retained
interruptions keep an unknown total unknown, forcing Retry to re-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): treat an unsatisfiable resume range as a representation change

A range-capable server whose entity shrank below the rewound overlap
offset answers 416 before any response body exists, which rejected the
request into the generic partial-deleting failure path. The 416 is now
recognized as a representation change: the partial is truncated and the
transfer restarts against the current entity from byte zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): treat an indeterminate Content-Range total as unknown

`Content-Range: bytes 200-299/*` fell through to the Content-Length
fallback, deriving a "total" equal to the end of the selected range —
a resumed response ending there was declared complete and the truncated
partial finalized. An indeterminate total now yields null, letting the
previously known total carry forward and classify the short response as
a retained truncation instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): signal restarts explicitly and keep carried totals informational

Round-8 review findings, both fixed at the design level instead of
another byte-comparison patch:

- Restart epochs (Greptile P1): the transfer layer now reports every
  rewrite-from-zero via task.transferRestarts (overlap mismatch, shrunk
  entity, 416, ignored Range), and the reconnect loop opens a fresh
  progress epoch on that signal — clean stall budget, no baseline. Byte
  inference could not recognize a rebuild landing near the previous
  attempt's count; the explicit signal can. Two restarts are tolerated
  per transfer; an unsignalled regression is an ordinary stall.
- Authoritative vs informational totals (Codex P1): completion and
  truncation decisions now use only the response's own total or its
  advertised indeterminate range end (`bytes X-Y/*` -> Y+1); a carried
  total is informational, is dropped once the bytes on disk falsify it,
  and can flag a short transfer but never authorize finalization. A
  mid-reset 206 retains the partial even with a falsified total — the
  response proved range capability — persisting the total as unknown.

Also raises the resume spec's jest timeout and tightens its polling:
the previous 5 s default flaked on starved CI runners.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): keep unproven state fully uncommitted across the resume model

Round-9 review findings, closing the remaining commit-before-proof gaps:

- The response's total now stays uncommitted (task and row) until the
  complete overlap matched, exactly like the validator: a persisted
  total equal to the unverified partial's size let the completed-partial
  shortcut finalize unproven bytes after a pause, crash, or retained
  failure (Codex P1).
- Retained-interruption persistence syncs the live task with the row:
  a stale falsified total left in memory made the next reconnect's
  resume-offset guard reject the retained partial into generic cleanup
  (Codex P1).
- An observed 206 is remembered as task.serverAcceptsRanges, so a
  request-phase failure (no response at all) can retain an
  unknown-length partial on that evidence instead of deleting it
  (Codex P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): treat a reset after the final ranged byte as completion

A 206 that delivers every advertised byte but ends in ECONNRESET instead
of a clean close was classified as an interruption with a falsified
total; the reconnect then resumed at EOF, collected a 416, and truncated
the complete file — an endpoint that always resets after its last byte
could never finish. A retainable failure whose on-disk bytes exactly
equal the response's own completion evidence (and the proven total, with
the overlap verified) now completes the transfer. Strict equality keeps
oversized partials on the generic-failure path.

Also splits the transfer error classes and retention classification
into download-transfer-errors.ts to stay under the max-lines rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): never let an indeterminate range end authorize completion

Reaching Y of `Content-Range: bytes X-Y/*` proves the selected range
was delivered, not that the entity ends there — a range-capping server
resetting at its cap would have finalized a truncated movie as
complete. The reset-after-final-byte completion now requires the
response's authoritative total; indeterminate range ends keep flagging
short delivery but such resets stay retained interruptions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): retain any nonempty partial and keep indeterminate ranges incomplete

Round-12 findings, resolved by removing the last evidence requirements:

- Retention no longer needs a total, validator, or range proof: since
  overlap verification owns resume correctness, the next attempt can
  safely prove, resume, or restart over ANY retained partial — deleting
  bytes is the only unrecoverable outcome. This closes the whole family
  (refused reconnects, chunked responses, falsified totals, and
  reconstructed retry tasks losing the in-memory range flag) and removes
  task.serverAcceptsRanges entirely (Codex P2).
- A clean EOF at the advertised end of an indeterminate range
  (`bytes X-Y/*`) stays incomplete, matching the reset path: reaching Y
  proves the range was delivered, not that the entity ends there, so a
  range-capping server can no longer finalize a truncated movie; the
  shrunk-entity restart likewise requires an authoritative total
  (Codex P1). Responses with no range and no total keep the clean-EOF
  completion contract of unknown-length HTTP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): honor 416-confirmed completion and keep proven validators

Round-13 findings plus a CI limit:

- A 416 whose `Content-Range: bytes */N` equals the partial's size
  confirms the file IS the complete entity (under If-Range a validator
  mismatch yields 200, so the 416 also confirms identity): finalize it
  instead of truncating and redownloading forever (Codex P1).
- A validator proven by a complete overlap match is now promoted on the
  error path too, and retained-failure/pause persistence write
  resume_validator from the task — later attempts resume via If-Range
  instead of replaying the 256 KiB window, which stalled out servers
  whose per-connection cap barely exceeds the window (Codex P1).
- download-resume.spec.ts crossed the 1200-line test limit: the shared
  harness moves to download-resume.test-harness.ts and the
  overlap-family cases into download-overlap-resume.spec.ts; the runtime
  spec gets the same CI-load timeout headroom as the resume specs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): settle falsified totals on clean exits and probe EOF after zero-growth replays

Round-14 review findings plus the CI build break:

- download-resume.test-harness.ts was outside the tsconfig test-helpers
  exclude glob and broke every app typecheck/build; renamed to
  download-resume.test-helpers.ts (the excluded pattern).
- A clean indeterminate delivery that outgrows a stale carried total now
  settles that total to unknown on the row AND the live task before
  persisting or throwing — the reconnect's resume-offset guard would
  otherwise reject the retained partial into generic cleanup (Codex P1).
- A verified overlap replay of an indeterminate range that appends
  nothing arms a one-shot EOF probe: the next attempt requests the byte
  after the partial so a compliant 416 (bytes */N) can confirm the file
  is the complete entity, instead of repeating the rewind until the
  stall budget fails a finished download; a probe answered with more
  data is retired unappended and rewound verification resumes
  (Codex P1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): retain the partial on an inconclusive EOF-probe 416

A probe at the partial's exact end always collects a 416 when the
entity ends there, and the confirming Content-Range length is optional
— so a length-less 416 is equally consistent with a complete file, and
the unconditional restart redownloaded a likely finished movie every
cycle. The probe's 416 now restarts only when a stated total BELOW the
partial proves the entity shrank; otherwise the partial is retained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): treat any EOF-offset 416 as inconclusive, not just the probe

A validator-backed resume at the partial's exact end IS an EOF request:
when its 416 arrived without the optional Content-Range length, the
probingEof gate saw false and restartFromScratch truncated the complete
partial. The inconclusive-416 retention now keys on the request having
started at the partial's end (covering the probe and every If-Range
resume alike); a restart still requires a stated total below the
partial.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): contradiction-proof carried totals and identity-gated 416 completion

Round-17 review findings:

- A carried total now falls the moment the response's advertised range
  end contradicts it, before any byte lands — waiting for the bytes to
  reach it left a pause/exit window where an N/N row let the
  completed-partial shortcut finalize a truncated file (Codex P1).
- The 416 completion shortcut now requires identity proof: an exact-EOF
  request backed by If-Range, or the EOF probe that follows a fully
  verified overlap replay. A bare length match on a rewound request
  proves nothing about whose bytes are on disk; a contradictory 416
  (stated total says the rewound range was satisfiable) retains instead
  of restarting (Codex P2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(downloads): extract 416 classification to stay under max-lines

The previous commit pushed download-transfer.ts to 404 effective lines;
the 416 decision moves into classifyRangeNotSatisfiable() in
download-transfer-errors.ts with identical semantics, and the unused
re-export-only imports are dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): promote the proven total on the error path and fix the 416 contract doc

- The error-path promotion after a complete overlap match now updates
  task.totalBytes alongside the validator: a pause landing while the
  partial sits at a stale carried total otherwise persisted an N/N row
  that Resume's completed-partial shortcut would finalize (Codex P1).
- The download-manager contract doc's rewound-416 paragraph now matches
  classifyRangeNotSatisfiable(): completion requires identity proof at
  exact EOF, restart requires a proven shrink, everything ambiguous
  retains (Codex P1 on the doc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): correct both 416 classification boundaries

Round-19 review findings, one in each direction:

- A rewound 416 WITHOUT a stated length now retains: unsatisfiability
  alone never proves the entity shrank relative to the retained bytes,
  and the canonical contract reserves restart for stated proof
  (Codex P1).
- A stated total EQUAL to a rewound request's first byte now restarts:
  the entity ending exactly at the rewound offset makes the 416 valid
  and proves the partial extends past the entity — equality was being
  misread as a contradiction, stranding the download in retain forever
  (Codex P1).

classifyRangeNotSatisfiable() gains an exhaustive pure table spec
covering every branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): restart on reset-ended responses that completed a shorter entity in-window

The clean-EOF path already restarted when a response delivered its
complete authoritative total inside the verification window, but a
reset arriving right after that final byte took the retention path and
stranded the oversized partial in a stall loop. The catch path now
mirrors the shrink detection: authoritative total reached inside an
unproven overlap restarts from scratch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): request identity encoding for byte-exact transfers

Axios's Node adapter transparently decodes gzip/brotli responses, which
would put decoded bytes on disk while Content-Length, Content-Range,
and every Range offset speak the encoded representation — desyncing
resume offsets and overlap verification on origins that compress.
Downloads now always send Accept-Encoding: identity.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): drop carried totals an indeterminate range can exactly reach

`bytes 200-249/*` can deliver the partial exactly TO a carried total of
250; the <= guard kept that total, and a pause or exit anywhere in that
window persisted a 250/250 row the completed-partial shortcut would
finalize without EOF proof. The guard is now strict: a carried total
survives only when the advertised indeterminate range cannot reach it,
which also closes the mid-stream pause window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): arm the EOF probe after a reset-ended verified zero-growth replay

A verified overlap replay that reset right at the partial's end (zero
growth, indeterminate range, no validator) retained without arming the
EOF probe, so every retry replayed the same tail until the stall budget
expired — the clean-EOF path's probe arming now has its reset-path
mirror.

Also splits to stay under max-lines: the classifyRangeNotSatisfiable
table spec moves to download-transfer-errors.spec.ts and the DB persist
helpers to download-transfer-persistence.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 07:07:25 +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 playback
Playback F Toggle player fullscreen
Playback ArrowLeft / ArrowRight Seek VOD playback by 5 seconds
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 22.13–22.x or 24 and newer 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%