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):
 | 
			
		||||
    """
 | 
			
		||||
    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
 | 
			
		||||
        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"
 | 
			
		||||
  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"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user