Added Hulu service script.

Added support for Hulu Movies and TV series
remotes/1718796989131948331/master
TPD94 2024-03-10 04:59:21 -04:00
parent 38d962d118
commit 8b6de1a569
1 changed files with 193 additions and 0 deletions

View File

@ -0,0 +1,193 @@
import json
import logging
from abc import ABCMeta, abstractmethod
from http.cookiejar import CookieJar
from typing import Optional, Union
from urllib.parse import urlparse
import click
import requests
from requests.adapters import HTTPAdapter, Retry
from rich.padding import Padding
from rich.rule import Rule
from devine.core.service import Service
from devine.core.titles import Movies, Movie, Titles_T, Title_T, Series, Episode
from devine.core.cacher import Cacher
from devine.core.config import config
from devine.core.console import console
from devine.core.constants import AnyTrack
from devine.core.credential import Credential
from devine.core.tracks import Chapters, Tracks
from devine.core.utilities import get_ip_info
from devine.core.manifests import HLS, DASH
class HULU(Service):
"""
Service code for Hulu
Written by TPD94
Authorization: Cookies (Ad free account only tested)
Security: HD@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="HULU", short_help="https://hulu.com", help=__doc__)
# Using series ID for hulu
@click.argument("title", type=str)
# Option if it is a movie
@click.option("--movie", is_flag=True, help="Specify if it's a movie")
# Pass the context back to the CLI with arguments
@click.pass_context
def cli(ctx, **kwargs):
return HULU(ctx, **kwargs)
# Accept the CLI arguments by overriding the constructor (The __init__() method)
def __init__(self, ctx, title, movie):
# Pass the series_id argument to self so it's accessable across all methods
self.title = title
self.movie = movie
# Overriding the constructor
super().__init__(ctx)
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
self.session.cookies.update(cookies)
def get_titles(self) -> Titles_T:
if self.movie:
metadata = self.session.get(url=f'https://discover.hulu.com/content/v5/hubs/movie/{self.title}').json()
movie = Movie(id_=metadata['id'],
service=self.__class__,
name=metadata['name'],
year=metadata['details']['entity']['premiere_date'][:4],
language="en",
data={"eab_id": metadata['details']['vod_items']['focus']['entity']['bundle']['eab_id']})
return Movies([movie])
else:
metadata = self.session.get(url=f'https://discover.hulu.com/content/v5/hubs/series/{self.title}').json()
all_episodes = []
s = 1
for season_count in metadata['components'][0]['items']:
season_metadata = self.session.get(url=f'https://discover.hulu.com/content/v5/hubs/series/{self.title}/season/{s}').json()
e = 0
for episode_count in season_metadata['items']:
all_episodes.append(Episode(
id_=season_metadata['items'][e]['id'],
service=self.__class__,
title=metadata['name'],
season=season_metadata['items'][e]['season'],
number=season_metadata['items'][e]['number'],
name=season_metadata['items'][e]['name'],
year=season_metadata['items'][e]['premiere_date'][:4],
language="en",
data={"eab_id": season_metadata['items'][e]['bundle']['eab_id']}
))
e += 1
s += 1
return Series(all_episodes)
def get_tracks(self, title: Title_T) -> Tracks:
json_data = {
'deejay_device_id': 214,
'version': 1,
'all_cdn': True,
'content_eab_id': f'{title.data["eab_id"]}',
'region': 'US',
'language': 'en',
'unencrypted': True,
'playback': {
'version': 2,
'video': {
'codecs': {
'values': [
{
'type': 'H264',
'width': 1920,
'height': 1080,
'framerate': 60,
'level': '4.2',
'profile': 'HIGH',
},
],
'selection_mode': 'ONE',
},
},
'audio': {
'codecs': {
'values': [
{
'type': 'AAC',
},
],
'selection_mode': 'ONE',
},
},
'drm': {
'values': [
{
'type': 'WIDEVINE',
'version': 'MODULAR',
'security_level': 'L3',
},
{
'type': 'PLAYREADY',
'version': 'V2',
'security_level': 'SL2000',
},
],
'selection_mode': 'ALL',
},
'manifest': {
'type': 'DASH',
'https': True,
'multiple_cdns': True,
'patch_updates': True,
'hulu_types': True,
'live_dai': True,
'multiple_periods': True,
'secondary_audio': True,
'unified_asset_signaling': False,
'live_fragment_delay': 3,
},
'segments': {
'values': [
{
'type': 'FMP4',
'encryption': {
'mode': 'CENC',
'type': 'CENC',
},
'https': True,
},
],
'selection_mode': 'ONE',
},
},
}
title_metadata = self.session.post(url=f'https://play.hulu.com/v6/playlist', json=json_data).json()
if 'stream_url' in title_metadata:
tracks = DASH.from_url(url=title_metadata['stream_url']).to_tracks(language="en")
title.data['widevineurl'] = title_metadata['wv_server']
return tracks
def get_chapters(self, title: Title_T) -> Chapters:
return []
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
return self.session.post(url=f'{title.data["widevineurl"]}', data=challenge).content