added playready support for NPO

This commit is contained in:
FairTrade 2025-11-03 10:45:51 +01:00
parent 7952381dca
commit f3ddf2bbc3
3 changed files with 58 additions and 20 deletions

View File

@ -17,21 +17,21 @@ from unshackle.core.tracks import Chapter, Tracks, Subtitle
class NPO(Service): class NPO(Service):
""" """
Service code for NPO Start (npo.nl) Service code for NPO Start (npo.nl)
Version: 1.0.0 Version: 1.1.0
Authorization: optional cookies (free/paid content supported) Authorization: optional cookies (free/paid content supported)
Security: FHD @ L3 (Widevine) Security: FHD @ L3
FHD @ SL3000
(Widevine and PlayReady support)
Supports: Supports:
Series https://npo.nl/start/serie/{slug} Series https://npo.nl/start/serie/{slug}
Movies https://npo.nl/start/video/{slug} Movies https://npo.nl/start/video/{slug}
Only supports widevine at the moment Note: Movie inside a series can be downloaded as movie by converting URL to:
https://npo.nl/start/video/slug
Note: Movie that is inside in a series (e.g. To change between Widevine and Playready, you need to change the DrmType in config.yaml to either widevine or playready
https://npo.nl/start/serie/zappbios/.../zappbios-captain-nova/afspelen)
can be downloaded as movies by converting the URL to:
https://npo.nl/start/video/zappbios-captain-nova
""" """
TITLE_RE = ( TITLE_RE = (
@ -68,6 +68,9 @@ class NPO(Service):
if self.config is None: if self.config is None:
raise EnvironmentError("Missing service config.") raise EnvironmentError("Missing service config.")
# Store CDM reference
self.cdm = ctx.obj.cdm
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None: def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
super().authenticate(cookies, credential) super().authenticate(cookies, credential)
if not cookies: if not cookies:
@ -165,7 +168,6 @@ class NPO(Service):
if not product_id: if not product_id:
raise ValueError("no productId detected.") raise ValueError("no productId detected.")
# Get JWT
token_url = self.config["endpoints"]["player_token"].format(product_id=product_id) token_url = self.config["endpoints"]["player_token"].format(product_id=product_id)
r_tok = self.session.get(token_url, headers={"Referer": f"https://npo.nl/start/video/{self.slug}"}) r_tok = self.session.get(token_url, headers={"Referer": f"https://npo.nl/start/video/{self.slug}"})
r_tok.raise_for_status() r_tok.raise_for_status()
@ -176,7 +178,7 @@ class NPO(Service):
self.config["endpoints"]["streams"], self.config["endpoints"]["streams"],
json={ json={
"profileName": "dash", "profileName": "dash",
"drmType": "widevine", "drmType": self.config["DrmType"],
"referrerUrl": f"https://npo.nl/start/video/{self.slug}", "referrerUrl": f"https://npo.nl/start/video/{self.slug}",
"ster": {"identifier": "npo-app-desktop", "deviceType": 4, "player": "web"}, "ster": {"identifier": "npo-app-desktop", "deviceType": 4, "player": "web"},
}, },
@ -205,12 +207,17 @@ class NPO(Service):
# Subtitles # Subtitles
subtitles = [] subtitles = []
for sub in data.get("assets", {}).get("subtitles", []): for sub in (data.get("assets", {}) or {}).get("subtitles", []) or []:
if not isinstance(sub, dict):
continue
lang = sub.get("iso", "und") lang = sub.get("iso", "und")
location = sub.get("location")
if not location:
continue # skip if no URL provided
subtitles.append( subtitles.append(
Subtitle( Subtitle(
id_=sub.get("name", lang), id_=sub.get("name", lang),
url=sub["location"].strip(), url=location.strip(),
language=Language.get(lang), language=Language.get(lang),
is_original_lang=lang == "nl", is_original_lang=lang == "nl",
codec=Subtitle.Codec.WebVTT, codec=Subtitle.Codec.WebVTT,
@ -233,9 +240,14 @@ class NPO(Service):
for tr in tracks.videos + tracks.audio: for tr in tracks.videos + tracks.audio:
if getattr(tr, "drm", None): if getattr(tr, "drm", None):
tr.drm.license = lambda challenge, **kw: self.get_widevine_license( if drm_type == "playready":
challenge=challenge, title=title, track=tr tr.drm.license = lambda challenge, **kw: self.get_playready_license(
) challenge=challenge, title=title, track=tr
)
else:
tr.drm.license = lambda challenge, **kw: self.get_widevine_license(
challenge=challenge, title=title, track=tr
)
return tracks return tracks
@ -244,11 +256,34 @@ class NPO(Service):
def get_widevine_license(self, challenge: bytes, title: Title_T, track: AnyTrack) -> bytes: def get_widevine_license(self, challenge: bytes, title: Title_T, track: AnyTrack) -> bytes:
if not self.drm_token: if not self.drm_token:
raise ValueError("DRM token not set login or paid content may be required.") raise ValueError("DRM token not set, login or paid content may be required.")
r = self.session.post( r = self.session.post(
self.config["endpoints"]["widevine_license"], self.config["endpoints"]["license"],
params={"custom_data": self.drm_token}, params={"custom_data": self.drm_token},
data=challenge, data=challenge,
) )
r.raise_for_status() r.raise_for_status()
return r.content return r.content
def get_playready_license(self, challenge: bytes, title: Title_T, track: AnyTrack) -> bytes:
if not self.drm_token:
raise ValueError("DRM token not set, login or paid content may be required.")
headers = {
"Content-Type": "text/xml; charset=utf-8",
"SOAPAction": "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense",
"Origin": "https://npo.nl",
"Referer": "https://npo.nl/",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0"
),
}
r = self.session.post(
self.config["endpoints"]["license"],
params={"custom_data": self.drm_token},
data=challenge,
headers=headers,
)
r.raise_for_status()
return r.content

View File

@ -4,5 +4,6 @@ endpoints:
metadata_episode: "https://npo.nl/start/_next/data/{build_id}/serie/{series_slug}/seizoen-{season_slug}/{episode_slug}.json" metadata_episode: "https://npo.nl/start/_next/data/{build_id}/serie/{series_slug}/seizoen-{season_slug}/{episode_slug}.json"
streams: "https://prod.npoplayer.nl/stream-link" streams: "https://prod.npoplayer.nl/stream-link"
player_token: "https://npo.nl/start/api/domain/player-token?productId={product_id}" player_token: "https://npo.nl/start/api/domain/player-token?productId={product_id}"
widevine_license: "https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication" license: "https://npo-drm-gateway.samgcloud.nepworldwide.nl/authentication"
homepage: "https://npo.nl/start" homepage: "https://npo.nl/start"
DrmType: "widevine"

View File

@ -1,4 +1,6 @@
These services is new and in development. Please feel free to submit pull requests for any mistakes or suggestions. These services is new and in development. Please feel free to submit pull requests for any mistakes or suggestions.
Acknowledgment
- Acknowledgment
Thanks to Adef for the NPO start downloader. Thanks to Adef for the NPO start downloader.