sp4rky-devine-services/services/HM/__init__.py
2025-04-09 15:07:35 -06:00

139 lines
5.4 KiB
Python

from http.cookiejar import CookieJar
from typing import Optional, Union
import click
import uuid
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
from devine.core.manifests import DASH
class HM(Service):
"""
Service code for Hallmark Now
Written by TPD94
Authorization: Cookies
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="HM", short_help="https://www.hmnow.com/", help=__doc__)
# Using series ID for crunchyroll
@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 HM(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
# Set boolean if movie was used
self.movie = movie
# Overriding the constructor
super().__init__(ctx)
# Define the authenticate function
def authenticate(self, cookies: Optional[CookieJar] = None, credential: Optional[Credential] = None) -> None:
self.session.cookies.update(cookies)
def get_titles(self) -> Titles_T:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.5',
'X-Device-Info': f'{{"id":"{uuid.uuid4()}","hardware":{{"manufacturer":"Browser (Desktop)","model":"Firefox","version":"124.0"}},"os":{{"name":"Windows","version":"10"}},"display":{{"width":1920,"height":1080}},"legal":{{}}',
}
if self.movie:
movie_metadata = self.session.get(url=f'https://www.hmnow.com/api/core/catalog/item/{self.title}', headers=headers).json()
return Movies([Movie(
id_=movie_metadata['data']['id'],
service=self.__class__,
name=movie_metadata['data']['title'],
year=movie_metadata['data']['releaseDate'][:4],
language="en",
data={'playbackUrl': movie_metadata['data']['video']['playback']}
)])
else:
series_metadata = self.session.get(url=f'https://www.hmnow.com/api/core/catalog/collection/{self.title}', headers=headers).json()
season_ids = []
for season in series_metadata['data']:
season_ids.append(season['id'])
episodes = []
for season in season_ids:
season_metadata = self.session.get(url=f'https://www.hmnow.com/api/core/catalog/collection/{season}', headers=headers).json()
for episode in season_metadata['data']:
episodes.append(Episode(
id_=episode['id'],
service=self.__class__,
title=season_metadata['collection']['tvShow']['title'],
season=episode['tvShowSeason']['season']['number'],
number=episode['episode']['number'],
name=episode['title'],
year=episode['releaseDate'][:4],
language="en",
data={'playbackUrl': episode['video']['playback']}
))
return Series(episodes)
def get_tracks(self, title: Title_T) -> Tracks:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.5',
'X-Device-Info': f'{{"id":"{uuid.uuid4()}","hardware":{{"manufacturer":"Browser (Desktop)","model":"Firefox","version":"124.0"}},"os":{{"name":"Windows","version":"10"}},"display":{{"width":1920,"height":1080}},"legal":{{}}',
}
episode_metadata = self.session.get(url=title.data['playbackUrl'], headers=headers).json()
tracks = DASH.from_url(episode_metadata['playbackInfo']['videoStreams'][1]['url']).to_tracks("en")
title.data['licenseUrl'] = episode_metadata['playbackInfo']['videoStreams'][1]['drms'][0]['licenseUrl']
title.data['token'] = episode_metadata['playbackInfo']['videoStreams'][1]['drms'][0]['licenseTokenHeader']['value']
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]]:
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',
'Authorization': f'{title.data["token"]}',
}
return requests.post(url='https://multidrm.vsaas.verimatrixcloud.net/widevine', headers=headers, data=challenge).content