from http.cookiejar import CookieJar from typing import Optional, Union import click import requests from devine.core.service import Service from devine.core.titles import Movies, Movie, Titles_T, Title_T, Series, Episode from devine.core.constants import AnyTrack from devine.core.credential import Credential from devine.core.tracks import Chapters, Tracks, Chapter from devine.core.manifests import DASH class CRKL(Service): """ Service code for Crackle Written by TPD94 Authorization: None Security: FHD@L3 """ GEOFENCE = ('US',) # Static method, this method belongs to the class @staticmethod # The command name, must much the service tag (and by extension the service folder) @click.command(name="CRKL", short_help="https://www.crackle.com", help=__doc__) # Using series ID for hulu @click.argument("title", type=str) # Pass the context back to the CLI with arguments @click.pass_context def cli(ctx, **kwargs): return CRKL(ctx, **kwargs) # Accept the CLI arguments by overriding the constructor (The __init__() method) def __init__(self, ctx, title): # Pass the series_id argument to self so it's accessable across all methods self.title = title # Overriding the constructor super().__init__(ctx) def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', 'Referer': 'https://www.crackle.com/', 'Content-Type': 'application/json', 'x-crackle-brand': 'crackle', 'x-crackle-platform': '5FE67CCA-069A-42C6-A20F-4B47A8054D46', 'x-crackle-region': 'us', 'x-crackle-locale': 'en-us', 'x-crackle-apiversion': 'v2.0.0', } self.session.headers.update(headers) def get_titles(self) -> Titles_T: video_type = self.session.get(url=f'https://prod-api.crackle.com/content/{self.title}').json() if video_type['data']['type'] == 'Movie': movie_metadata = video_type['data']['metadata'][0] movie_id = self.session.get(url=f'https://prod-api.crackle.com/content/{self.title}/children').json()['data'][0]['id'] return Movies([Movie( id_=movie_id, service=self.__class__, name=movie_metadata['title'], year=None, language=None )]) elif video_type['data']['type'] == 'Series': season_ids = [] seasons_metadata = self.session.get(url=f'https://prod-api.crackle.com/content/{self.title}/children').json() for season in seasons_metadata['data']: season_ids.append(season['id']) episodes = [] for season in season_ids: episodes_metadata = self.session.get(url=f'https://prod-api.crackle.com/content/{season}/children').json() for episode in episodes_metadata['data']: episodes.append(Episode( id_=episode['id'], service=self.__class__, title=video_type['data']['metadata'][0]['title'], season=episode['seasonNumber'], number=episode['episodeNumber'], name=episode['title'], year=None, language=None )) return Series(episodes) def get_tracks(self, title: Title_T) -> Tracks: title_metadata = self.session.get(url=f'https://prod-api.crackle.com/playback/vod/{title.id}').json() for manifest_url in title_metadata['data']['streams']: if manifest_url['type'] == 'dash-widevine' or manifest_url['type'] == 'dash_widevine': index_url = manifest_url['url'] base_target_url = index_url[:index_url.find(".com")+len(".com")] mpd_url_metadata = self.session.post(url=index_url).json()['manifestUrl'] final_url = base_target_url + mpd_url_metadata return DASH.from_url(final_url).to_tracks(language="en") def get_chapters(self, title: Title_T) -> Chapters: title_metadata = self.session.get(url=f'https://prod-api.crackle.com/content/{title.id}').json() return Chapters([Chapter(timestamp=title_metadata['data']['assets']['chapters'][1]['breakpoints'][0]['smpteStart'][:8])]) def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0', 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.5', } return requests.post(url='https://widevine-license.crackle.com/', data=challenge, headers=headers).content