138 lines
5.0 KiB
Python
138 lines
5.0 KiB
Python
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 |