From fa776a590a0972b6d5954ab73eb68f2ee76a01de Mon Sep 17 00:00:00 2001 From: FairTrade Date: Sun, 8 Mar 2026 15:18:09 +0100 Subject: [PATCH] Added HLS Support --- KNPY/__init__.py | 83 ++++++++++++++++++++++++++++++++++++++---------- KNPY/config.yaml | 2 +- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/KNPY/__init__.py b/KNPY/__init__.py index 8426d59..8b21663 100644 --- a/KNPY/__init__.py +++ b/KNPY/__init__.py @@ -286,39 +286,69 @@ class KNPY(Service): if response_json and response_json.get("errorSubcode") == "playRegionRestricted": self.log.error("Kanopy reports: This video is not available in your country.") raise PermissionError( - "Playback blocked by region restriction. Try connecting through a supported country or verify your library’s access region." + "Playback blocked by region restriction. Try connecting through a supported country or verify your library's access region." ) else: self.log.error(f"Access forbidden (HTTP 403). Response: {response_json}") raise PermissionError("Kanopy denied access to this video. It may require a different library membership or authentication.") - # Raise for any other HTTP errors r.raise_for_status() play_data = response_json or r.json() manifest_url = None + manifest_type = None + drm_info = {} + + # Iterate through manifests: prefer DASH, fallback to HLS for manifest in play_data.get("manifests", []): - if manifest["manifestType"] == "dash": - url = manifest["url"] - manifest_url = f"https://kanopy.com{url}" if url.startswith("/") else url - drm_type = manifest.get("drmType") + manifest_type_raw = manifest["manifestType"] + url = manifest["url"].strip() # Strip whitespace from URLs + + # Construct full URL if relative + if url.startswith("/"): + url = f"https://kanopy.com{url}" + + drm_type = manifest.get("drmType") + + if manifest_type_raw == "dash": + manifest_url = url + manifest_type = "dash" + if drm_type == "kanopyDrm": play_id = play_data.get("playId") - self.widevine_license_url = self.config["endpoints"]["widevine_license"].format(license_id=f"{play_id}-0") + self.widevine_license_url = self.config["endpoints"]["widevine_license"].format( + license_id=f"{play_id}-0" + ) elif drm_type == "studioDrm": license_id = manifest.get("drmLicenseID", f"{play_data.get('playId')}-1") - self.widevine_license_url = self.config["endpoints"]["widevine_license"].format(license_id=license_id) + self.widevine_license_url = self.config["endpoints"]["widevine_license"].format( + license_id=license_id + ) else: - self.log.warning(f"Unknown drmType: {drm_type}") + self.log.warning(f"Unknown DASH drmType: {drm_type}") self.widevine_license_url = None - break + break # Prefer DASH, exit loop + + elif manifest_type_raw == "hls" and not manifest_url: + # Store HLS as fallback if DASH not found + manifest_url = url + manifest_type = "hls" + + if drm_type == "fairplay": + self.log.warning("HLS with FairPlay DRM detected - not currently supported by this service") + self.widevine_license_url = None + drm_info["fairplay"] = True + else: + # HLS with no DRM or unsupported DRM type + self.widevine_license_url = None + drm_info["clear"] = True if not manifest_url: - raise ValueError("Could not find a DASH manifest for this title.") - if not self.widevine_license_url: - raise ValueError("Could not construct Widevine license URL.") + raise ValueError("Could not find a DASH or HLS manifest for this title.") + if manifest_type == "dash" and not self.widevine_license_url: + raise ValueError("Could not construct Widevine license URL for DASH manifest.") - self.log.info(f"Fetching DASH manifest from: {manifest_url}") + self.log.info(f"Fetching {manifest_type.upper()} manifest from: {manifest_url}") r = self.session.get(manifest_url) r.raise_for_status() @@ -331,14 +361,35 @@ class KNPY(Service): "Connection": "keep-alive", }) - tracks = DASH.from_text(r.text, url=manifest_url).to_tracks(language=title.language) + # Parse manifest based on type + if manifest_type == "dash": + tracks = DASH.from_text(r.text, url=manifest_url).to_tracks(language=title.language) + elif manifest_type == "hls": + # Try to import HLS parser from unshackle + try: + from unshackle.core.manifests import HLS + tracks = HLS.from_text(r.text, url=manifest_url).to_tracks(language=title.language) + self.log.info("Successfully parsed HLS manifest") + except ImportError: + self.log.error( + "HLS manifest parser not available in unshackle.core.manifests. " + "Ensure your unshackle installation supports HLS parsing." + ) + raise + except Exception as e: + self.log.error(f"Failed to parse HLS manifest: {e}") + raise + else: + raise ValueError(f"Unsupported manifest type: {manifest_type}") + + # Add subtitles/captions from play_data (works for both DASH and HLS) for caption_data in play_data.get("captions", []): lang = caption_data.get("language", "en") for file_info in caption_data.get("files", []): if file_info.get("type") == "webvtt": tracks.add(Subtitle( id_=f"caption-{lang}", - url=file_info["url"], + url=file_info["url"].strip(), codec=Subtitle.Codec.WebVTT, language=Language.get(lang) )) diff --git a/KNPY/config.yaml b/KNPY/config.yaml index 7e61f6f..50261cb 100644 --- a/KNPY/config.yaml +++ b/KNPY/config.yaml @@ -12,4 +12,4 @@ endpoints: search: "https://kanopy.com/kapi/search/videos" plays: "https://kanopy.com/kapi/plays" access_expires_in: "https://kanopy.com/kapi/users/{user_id}/history/videos/{video_id}/access_expires_in?domainId={domain_id}" - widevine_license: "https://kanopy.com/kapi/licenses/widevine/{license_id}" \ No newline at end of file + widevine_license: "https://kanopy.com/kapi/licenses/widevine/{license_id}"