forked from FairTrade/unshackle-services
added playready support for NPO
This commit is contained in:
parent
7952381dca
commit
f3ddf2bbc3
@ -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,6 +240,11 @@ 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):
|
||||||
|
if drm_type == "playready":
|
||||||
|
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(
|
tr.drm.license = lambda challenge, **kw: self.get_widevine_license(
|
||||||
challenge=challenge, title=title, track=tr
|
challenge=challenge, title=title, track=tr
|
||||||
)
|
)
|
||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user