No description
  • C# 75.4%
  • HTML 15.1%
  • Python 9.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tiago Águeda f8d64b7dad
All checks were successful
build / dotnet (push) Successful in 1m18s
build / tool-tests (push) Successful in 59s
build / release (push) Successful in 1m25s
Trickle in guide details, waiting out DIGI's dropped requests
Measured through a VPN exit on 4 November: at one request a second DIGI
answers about fifteen and then stalls; at one every five seconds it
answers about three in four and silently drops the rest, while a control
site through the same tunnel answers nearly all. So stopping at the first
drop, as 0.1.1 did, ended every run after some 30 details.

Details now go one every 25 seconds for up to two and a half hours a run.
A dropped request is waited out for a minute; three in a row stop the run.
The guide is rewritten every 50 details and Jellyfin re-reads it every
150, and before a trickle longer than that, so the new listing and the
details show while the run goes on. Saving the settings ends a trickle
early so the new settings apply at once.

Jellyfin's interval trigger counts from the end of the last run, so the
task now runs an hour after one ends: a trickling run every three and a
half hours, about 2,470 details a day, over the 2,100 or so the next 24
hours hold. Requests time out after 20 seconds instead of 30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 13:15:44 +01:00
.forgejo/workflows Fix the settings page script, which failed to parse 2026-11-02 06:26:38 +01:00
assets Recover the channel list from the playlist; add the plugin image 2026-11-02 08:53:47 +01:00
data Scaffold DIGI TV Portugal Live TV plugin 2026-11-02 04:01:52 +01:00
src/Jellyfin.Plugin.DigiTv Trickle in guide details, waiting out DIGI's dropped requests 2026-11-04 13:15:44 +01:00
tests/Jellyfin.Plugin.DigiTv.Tests Trickle in guide details, waiting out DIGI's dropped requests 2026-11-04 13:15:44 +01:00
tools Document the keys per channel, read from the device's licence log 2026-11-04 11:28:43 +01:00
.gitignore Read the DIGI lineup and guide, from the decompiled TV app 2026-11-02 06:13:45 +01:00
AGENTS.md Document the keys per channel, read from the device's licence log 2026-11-04 11:28:43 +01:00
build.yaml Trickle in guide details, waiting out DIGI's dropped requests 2026-11-04 13:15:44 +01:00
image.png Recover the channel list from the playlist; add the plugin image 2026-11-02 08:53:47 +01:00
Jellyfin.Plugin.DigiTv.sln Scaffold DIGI TV Portugal Live TV plugin 2026-11-02 04:01:52 +01:00
README.md Trickle in guide details, waiting out DIGI's dropped requests 2026-11-04 13:15:44 +01:00

jellyfin-plugin-digitv

Live TV from a DIGI TV Portugal subscription, inside Jellyfin.

This plugin signs in with your DIGI TV account and turns the subscription's channel lineup into an M3U playlist and an XMLTV guide for Jellyfin's own M3U tuner and XMLTV provider, so the channels appear in the guide alongside everything else.

Status

Lineup and guide work; DIGI's own streams do not. The DIGI API in DigiProtocol.cs was read from the DIGI TV Android TV app (ro.digionline.tv 2.7.0); see AGENTS.md for how and what was found. That app plays every channel under Widevine DRM, which Jellyfin cannot play, and this project does not strip DRM. So every channel is listed with a red cross (❌) and opening it answers 503, unless you supply a stream of your own for it in the settings' Channels tab.

  • Sign-in, session renewal (refresh token, then password), and DIGI's expired / logout answers
  • Channel list: the subscription's channels, with preset numbers and logos
  • Guide by day, for every channel
  • Guide details for the next hours: synopsis, image, category, year, credits, age rating, season and episode
  • M3U playlist and XMLTV guide for Jellyfin's built-in tuner, and automatic Live TV setup
  • Your own stream per channel, which removes the unavailable mark
  • Playback from DIGI: its streams are Widevine-protected
  • Checked against the live service: the field names come from the app's code, not from a captured response

Requirements

  • Jellyfin 12.0 or later
  • A DIGI TV Portugal subscription, and the e-mail and password you use in the DIGI TV app
  • A server in Portugal. DIGI picks the country service (its app_id) by the caller's IP address; elsewhere, set DIGI service id in the settings.

1. Checking the DIGI API against a capture (optional)

The API was read from the app's code. To check it against real traffic, capture the DIGI TV app's traffic with a proxy that exports HAR, such as HTTP Toolkit or mitmproxy (mitmdump --set hardump=capture.har). Android apps only trust a user CA on a debuggable build or an emulator, so an Android emulator with the DIGI TV app is usually the easiest route. While capturing:

  1. sign in
  2. open the channel list
  3. open the guide, and scroll a day ahead
  4. play two or three channels for a few seconds each

Then:

python tools/inspect_capture.py capture.har

It prints every API endpoint with its query, headers, request body and response shape, tags the ones that look like sign-in, channels, guide and stream, and classifies every HLS and DASH manifest as clear, AES-128 or DRM. The last line says whether the streams can play in Jellyfin at all.

A redacted copy goes to data/capture_summary.json. Passwords, tokens, cookies, e-mail addresses and query values are removed from it and JSON values are reduced to their types, so it is the file to share when correcting DigiProtocol. The HAR itself holds your password and session: do not share it or commit it. .gitignore excludes *.har.

2. Install the plugin

Add the plugin repository in Dashboard → Plugins → Repositories (once the repository exists and a release has been tagged):

https://source.tiagoagueda.com/tiagoagueda/jellyfin-digitv/raw/branch/manifest/manifest.json

then install DIGI TV from the catalogue.

Releasing

Push a v* tag (git tag v0.1.0 && git push origin v0.1.0). CI builds the package with the tag as its version, attaches it to a Forgejo release and adds it to manifest.json on the manifest branch. Update changelog in build.yaml first: it becomes the release notes Jellyfin shows.

Building by hand

dotnet build -c Release src/Jellyfin.Plugin.DigiTv

Copy Jellyfin.Plugin.DigiTv.dll into <jellyfin-config>/plugins/DIGI TV/ and restart the server.

3. Configure

Dashboard → Plugins → DIGI TV:

  • E-mail and Password: the DIGI TV app account. The password is never sent back to the settings page; leave it blank to keep the saved one.
  • Guide days: how far ahead to fetch the guide. DIGI publishes only a few days ahead, so more fetches no more.
  • Programme details, hours ahead: entries starting within this many hours (24 by default) get their synopsis, image, category, credits, rating, and season and episode numbers. DIGI sends these one programme per request, stalls a server that asks fast and drops about one request in four or five even at a slow pace. So each run trickles them in, soonest first: one every 25 seconds for up to two and a half hours (about 360 a run), waiting a minute after a dropped request and stopping after three drops in a row. The guide is rewritten every 50 details and Jellyfin re-reads it about once an hour, so they show as they come; a run still trickling ends early when the settings are saved. They are kept in digitv_details.json, so the guide fills in over a few runs. 0 turns them off.
  • Output folder: where the playlist and guide go. Blank uses digitv under Jellyfin's data folder (/config/data/digitv in the linuxserver image).
  • Stream base URL: how Jellyfin's tuner reaches the plugin's stream endpoint. Blank uses the loopback address, which is right unless a reverse proxy changes the base path.
  • DIGI service id: leave blank. DIGI picks the country service by the server's IP address; set it only if a server outside Portugal gets the wrong lineup.
  • HTTP proxy: every DIGI request goes through it, e.g. http://127.0.0.1:8899. DIGI serves Portugal from its Spanish platform and blocks addresses it does not like: when it blocks the server, a proxy with a Spanish exit (a VPN container such as gluetun with its HTTP proxy on) gets through. Only http:// proxies; blank connects directly.
  • Guide time zone: the zone DIGI writes its guide times in. Leave it on Automatic: DIGI's times are exact (RTP1's Telejornal at 19:58 UTC on 4 November). A guide fetched before 2 November had them two hours early, Lisbon's clock written as Bucharest's; should that happen again, choose Europe/Bucharest.
  • Channel number offset: added to DIGI's channel numbers, 5000 by default, so RTP1 is 5001. Jellyfin's guide sorts every tuner's channels by number together; this keeps DIGI's in a block of their own.
  • Set up Live TV automatically: on by default; see below

The Channels tab lists the channels of the last update. Each has a Stream URL field: leave it blank and the channel stays listed, with the guide, marked unavailable; enter a valid http, https, udp, rtp, rtsp, rtmp, rtmps or srt URL (another IPTV source, a DVB tuner, …) and the channel plays from it and loses the mark. The URL is only checked for form, not played. Unavailable mark picks a red cross after the name (RTP1 ❌, the default) or the text (unavailable) / (indisponível) for TV apps that cannot show emoji. Saving rewrites the playlist at once, from the saved channel list if DIGI does not answer.

The settings page follows each viewer's Jellyfin interface language, English or Portuguese.

Saving the settings runs the scheduled task DIGI TV: update channels and guide, which also runs at startup and an hour after each run ends (a run that trickles in programme details takes up to two and a half hours). It signs in, writes digitv_channels.m3u and digitv_guide.xml, then adds them to Dashboard → Live TV:

  • an M3U Tuner named "DIGI TV" for the playlist
  • an XMLTV guide provider for the guide, enabled for that tuner only, so the DIGI guide is never matched against another tuner's channels

An entry that already points at the file, such as one added by hand, is reused rather than duplicated. The plugin remembers the entries it manages: if the output folder moves it repoints them, and if you delete one it stays deleted. Untick and tick Set up Live TV automatically to have it added again.

Design

Why a stream endpoint. DIGI's stream URLs carry short-lived tokens, so they cannot be written into a playlist that is refreshed twice a day. Each playlist entry instead points at the plugin:

http://127.0.0.1:8096/DigiTv/Stream/<channel>.m3u8?key=<stream key>

When Jellyfin tunes a channel, the endpoint asks DIGI for a fresh URL with the cached session and answers with a redirect to it. The .m3u8 ending makes Jellyfin's M3U tuner hand the URL to ffmpeg as HLS rather than buffer it as a raw HTTP stream, and ffmpeg follows the redirect, resolving segment URLs against DIGI's playlist.

If DIGI's CDN turns out to need headers or cookies on every segment request, a redirect cannot carry them, and the endpoint would have to proxy the playlists instead. The capture shows which.

The stream key. Jellyfin's tuner fetches those URLs without a Jellyfin token, so the endpoint is anonymous. It would otherwise hand anyone who can reach the server a stream from your subscription. It therefore answers only to a random key generated on first run and written into the playlist; any other request gets a 404. The key is in the playlist file, in Jellyfin's ffmpeg logs, and in the channel's media source, which signed-in Jellyfin users can see. Treat it like the playlist itself. Clearing the setting in the config XML generates a new one on the next run.

Credentials. Jellyfin stores plugin settings as plain XML in its config folder, so the password sits there in clear, readable by anyone with access to that folder. Only administrators can read it through the API.

One device. The plugin presents a fixed device id, generated once, so the server counts as a single device on the DIGI account.

Guide data. The day listing carries only each entry's title and times; everything else comes from a per-entry detail page, which is why details are fetched for a window rather than the whole guide. Programme images are the detail page's background. Season and episode numbers come from its labels (e.g. "T2", "Ep. 12"); Jellyfin counts a programme as a series only with an episode number. Entries not fetched yet borrow the image, category and rating of a fetched showing with the same title on the same channel, but not its synopsis or episode numbers. DIGI's categories are Portuguese, so the English word Jellyfin flags programmes by ("Movie", "Sports", "News", "Kids") is written alongside.

Channel tags. An M3U playlist cannot give Jellyfin channel tags, so after each guide refresh the plugin tags its channels itself: DIGI TV on every channel, and DIGI TV: <category> for each DIGI category, e.g. DIGI TV: Desporto. Channels without a stream of their own also get DIGI TV: Unavailable. Use them in a user's parental controls (allow or block items with tags) to show that user only some channels, only the ones that play, or none of DIGI's.

Why a tuner, not a Live TV service. As in the Orange France plugin: Jellyfin sends recordings for a plugin Live TV service's channels to that service, so recording would have meant reimplementing the DVR. Feeding the built-in M3U tuner and XMLTV provider gets all of it, and adding them through ITunerHostManager and IListingsManager keeps to public API.

Layout

build.yaml                        JPRM manifest
data/                             capture summaries (gitignored)
src/Jellyfin.Plugin.DigiTv/
  Plugin.cs                       plugin entry point
  PluginServiceRegistrator.cs     DI registration
  Configuration/                  settings, dashboard page (en/pt), its channel list
  Digi/                           API client, session handling, models
    DigiProtocol.cs               the DIGI wire format: to fill in
  Export/                         M3U + XMLTV writers, export task, Live TV setup
  Localization/                   server-side strings (en/pt)
  Streaming/                      stream endpoint, its URLs, your own streams
tests/
  Jellyfin.Plugin.DigiTv.Tests/
tools/
  inspect_capture.py              HAR capture → API map and DRM check
  test_inspect_capture.py         its self-tests
  inspect_license_log.py          logcat → keys per channel (ids only)
python tools/test_inspect_capture.py              # capture inspector
dotnet test tests/Jellyfin.Plugin.DigiTv.Tests    # playlist, guide, setup, keys