- C# 75.4%
- HTML 15.1%
- Python 9.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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> |
||
| .forgejo/workflows | ||
| assets | ||
| data | ||
| src/Jellyfin.Plugin.DigiTv | ||
| tests/Jellyfin.Plugin.DigiTv.Tests | ||
| tools | ||
| .gitignore | ||
| AGENTS.md | ||
| build.yaml | ||
| image.png | ||
| Jellyfin.Plugin.DigiTv.sln | ||
| README.md | ||
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/logoutanswers - 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:
- sign in
- open the channel list
- open the guide, and scroll a day ahead
- 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
digitvunder Jellyfin's data folder (/config/data/digitvin 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. Onlyhttp://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