diff --git a/NPO/__init__.py b/NPO/__init__.py index 1664edd..6f4b697 100644 --- a/NPO/__init__.py +++ b/NPO/__init__.py @@ -17,21 +17,21 @@ from unshackle.core.tracks import Chapter, Tracks, Subtitle class NPO(Service): """ Service code for NPO Start (npo.nl) - Version: 1.0.0 + Version: 1.1.0 Authorization: optional cookies (free/paid content supported) - Security: FHD @ L3 (Widevine) + Security: FHD @ L3 + FHD @ SL3000 + (Widevine and PlayReady support) Supports: • Series ↦ https://npo.nl/start/serie/{slug} • Movies ↦ https://npo.nl/start/video/{slug} - - Only supports widevine at the moment - Note: Movie that is inside in a series (e.g. - 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 + Note: Movie inside a series can be downloaded as movie by converting URL to: + https://npo.nl/start/video/slug + + To change between Widevine and Playready, you need to change the DrmType in config.yaml to either widevine or playready """ TITLE_RE = ( @@ -68,6 +68,9 @@ class NPO(Service): if self.config is None: 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: super().authenticate(cookies, credential) if not cookies: @@ -165,7 +168,6 @@ class NPO(Service): if not product_id: raise ValueError("no productId detected.") - # Get JWT 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.raise_for_status() @@ -176,7 +178,7 @@ class NPO(Service): self.config["endpoints"]["streams"], json={ "profileName": "dash", - "drmType": "widevine", + "drmType": self.config["DrmType"], "referrerUrl": f"https://npo.nl/start/video/{self.slug}", "ster": {"identifier": "npo-app-desktop", "deviceType": 4, "player": "web"}, }, @@ -205,12 +207,17 @@ class NPO(Service): # 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") + location = sub.get("location") + if not location: + continue # skip if no URL provided subtitles.append( Subtitle( id_=sub.get("name", lang), - url=sub["location"].strip(), + url=location.strip(), language=Language.get(lang), is_original_lang=lang == "nl", codec=Subtitle.Codec.WebVTT, @@ -233,9 +240,14 @@ class NPO(Service): for tr in tracks.videos + tracks.audio: if getattr(tr, "drm", None): - tr.drm.license = lambda challenge, **kw: self.get_widevine_license( - challenge=challenge, title=title, track=tr - ) + 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( + challenge=challenge, title=title, track=tr + ) return tracks @@ -244,11 +256,34 @@ class NPO(Service): def get_widevine_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.") + raise ValueError("DRM token not set, login or paid content may be required.") r = self.session.post( - self.config["endpoints"]["widevine_license"], + self.config["endpoints"]["license"], params={"custom_data": self.drm_token}, data=challenge, ) r.raise_for_status() - return r.content \ No newline at end of file + 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 diff --git a/NPO/config.yaml b/NPO/config.yaml index 3dfbe08..3caf4f0 100644 --- a/NPO/config.yaml +++ b/NPO/config.yaml @@ -4,5 +4,6 @@ endpoints: 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" 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" +DrmType: "widevine" diff --git a/README.md b/README.md index 52cc63d..a75f1df 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ 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.