149 lines
12 KiB
Python
149 lines
12 KiB
Python
from typing import Optional, Union
|
|
import click
|
|
from devine.core.service import Service
|
|
from devine.core.titles import Movies, Movie
|
|
from devine.core.constants import AnyTrack
|
|
from devine.core.titles import Title_T, Titles_T
|
|
from devine.core.tracks import Tracks
|
|
from devine.core.manifests import DASH
|
|
|
|
class RBOX(Service):
|
|
"""
|
|
Service code for Redbox
|
|
|
|
Written by TPD94
|
|
|
|
Authorization: None (Free titles)
|
|
|
|
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="RBOX", short_help="https://www.redbox.com/ondemand-movies/", help=__doc__)
|
|
|
|
# Using series ID for crunchyroll
|
|
@click.argument("title", type=str)
|
|
|
|
# Pass the context back to the CLI with arguments
|
|
@click.pass_context
|
|
def cli(ctx, **kwargs):
|
|
return RBOX(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)
|
|
|
|
# Defining a function to return titles
|
|
def get_titles(self) -> Titles_T:
|
|
|
|
# Set JSON data for the request
|
|
json_data = {
|
|
'operationName': 'fetchTitleWeb',
|
|
'variables': {
|
|
'id': f'/ondemand-movies/{self.title}',
|
|
'idType': 'PRODUCTPAGEID',
|
|
'number': 1,
|
|
'size': 500,
|
|
},
|
|
'query': 'query fetchTitleWeb($id: String!, $idType: ProductIdTypeEnum!, $number: Int!, $size: Int!) {\n product(id: $id, idType: $idType) {\n productGroupId\n name\n airDate\n type\n description {\n long\n short\n __typename\n }\n duration\n genres\n originalLanguages\n releaseYear\n productPage\n productLevelProgress: progress {\n progressPercentage\n progressSeconds\n viewingComplete\n firstViewDate\n __typename\n }\n rating {\n name\n reason\n description\n __typename\n }\n children(paging: {number: $number, size: $size}) {\n total\n hasMore\n items {\n airDate\n name\n number\n type\n screenFormats\n soundFormats\n closedCaptions\n productPagePath: productPage\n studios\n studioSubLabel\n actors: credits(where: {type_contains: "Actor"}) {\n name\n __typename\n }\n directors: credits(where: {type_contains: "Director"}) {\n name\n __typename\n }\n screenwriter: credits(where: {type_contains: "ScreenWriter"}) {\n name\n __typename\n }\n description {\n long\n __typename\n }\n images {\n boxArtSmall\n boxArtVertical\n stillFrameHome\n __typename\n }\n lockerContext {\n entitlementQuality\n entitlementType\n progressSeconds\n progressPercentage\n expirationDate\n titleConcurrency {\n canDownload\n canStream\n deviceUsage {\n device {\n deviceType\n id\n nickName\n registeredDate\n __typename\n }\n usage\n __typename\n }\n __typename\n }\n __typename\n }\n productLevelProgress: progress {\n progressPercentage\n progressSeconds\n viewingComplete\n firstViewDate\n __typename\n }\n orderablePricingPlan {\n purchaseType\n quality\n price\n basePrice\n planId: id\n __typename\n }\n titleDetails {\n redboxTitleId\n __typename\n }\n perksRedemptionPlans: titleDetails {\n plans: perksRedemptionPlans {\n purchaseType\n quality\n redemptionTypeId\n __typename\n }\n __typename\n }\n children(paging: {number: $number, size: $size}) {\n total\n hasMore\n items {\n productGroupId\n progress {\n firstViewDate\n progressPercentage\n progressSeconds\n viewingComplete\n __typename\n }\n name\n type\n number\n airDate\n description {\n long\n __typename\n }\n images {\n stillFrameHome\n __typename\n }\n rating {\n name\n __typename\n }\n duration\n lockerContext {\n entitlementQuality\n entitlementType\n progressSeconds\n progressPercentage\n expirationDate\n viewingComplete\n titleConcurrency {\n canDownload\n canStream\n deviceUsage {\n device {\n deviceType\n id\n nickName\n registeredDate\n __typename\n }\n usage\n __typename\n }\n __typename\n }\n __typename\n }\n productLevelProgress: progress {\n progressPercentage\n progressSeconds\n viewingComplete\n firstViewDate\n __typename\n }\n productPage\n orderablePricingPlan {\n purchaseType\n quality\n price\n basePrice\n planId: id\n __typename\n }\n orderablePricingPlans: orderablePricingPlan {\n purchaseType\n quality\n price\n basePrice\n id\n __typename\n }\n titleDetails {\n closedCaptions\n mediumType\n redboxTitleId\n __typename\n }\n parent {\n number\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n images {\n boxArtSmall\n boxArtLarge\n boxArtVertical\n stillFrameHome\n stillHome\n theatricalPoster\n __typename\n }\n titleTypes: titleDetails {\n mediumType\n redboxTitleId\n redboxReleaseDate\n subtitles\n sdhAvailable\n comingSoon {\n purchaseType\n redboxReleaseDate\n __typename\n }\n __typename\n }\n trailers\n closedCaptions\n studios\n studioSubLabel\n screenFormats\n soundFormats\n actors: credits(where: {type_contains: "Actor"}) {\n name\n __typename\n }\n directors: credits(where: {type_contains: "Director"}) {\n name\n __typename\n }\n screenwriter: credits(where: {type_contains: "ScreenWriter"}) {\n name\n __typename\n }\n lockerContext {\n entitlementQuality\n entitlementType\n progressSeconds\n progressPercentage\n expirationDate\n titleConcurrency {\n canDownload\n canStream\n deviceUsage {\n device {\n deviceType\n id\n nickName\n registeredDate\n __typename\n }\n usage\n __typename\n }\n __typename\n }\n __typename\n }\n orderablePricingPlan {\n price\n basePrice\n purchaseType\n quality\n planId: id\n __typename\n }\n redboxPlusAvailability: titleDetails {\n date: redboxPlusAvailabilityDate\n __typename\n }\n perksRedemptionPlans: titleDetails {\n plans: perksRedemptionPlans {\n purchaseType\n quality\n redemptionTypeId\n __typename\n }\n __typename\n }\n canPurchasePhysical\n __typename\n }\n}\n',
|
|
}
|
|
|
|
# Get the metadata
|
|
metadata = self.session.post(url="https://www.redbox.com/gapi/ondemand/hcgraphql/", json=json_data)
|
|
|
|
# Get the movie ID
|
|
movie_id = metadata.json()['data']['product']['titleTypes'][0]['redboxTitleId']
|
|
|
|
# Get the movie name
|
|
movie_name = metadata.json()['data']['product']['name']
|
|
|
|
# Get the movie year
|
|
movie_year = metadata.json()['data']['product']['releaseYear']
|
|
|
|
# Make a class instance for
|
|
movie_class = Movie(id_=movie_id, service=self.__class__, name=movie_name, year=movie_year)
|
|
|
|
# Return the movie
|
|
return Movies([movie_class])
|
|
|
|
# Define a function to get tracks
|
|
def get_tracks(self, title: Title_T) -> Tracks:
|
|
|
|
# Set headers for the request
|
|
headers = {
|
|
'x-redbox-device-type': 'RedboxWebWindows',
|
|
}
|
|
|
|
# Set the JSON data for the request
|
|
json_data = {
|
|
'query': '\n\tquery AvodStreams($redboxTitleId: String!,$filter:AvodStreamFilter) { \n\t\tavodStreams(redboxTitleId: $redboxTitleId,filter:$filter) {\n\t\t\tviewContentReference\n\t\t\tredboxTitleId\n\t\t\tname\n\t\t\tstreams {\n\t\t\t\tquality\n\t\t\t\tformat\n\t\t\t\tdrmType\n\t\t\t\turl\n\t\t\t\tdrmUrl\n\t\t\t\tlicenseRequestToken\n\t\t\t\tavailability\n\t\t\t\tadsType\n\t\t\t}\n\t\t\tcuePoints\n\t\t\tvmapUrl\n scrubbers {\n url\n }\n\t\t}\n\t}\n',
|
|
'variables': {
|
|
'redboxTitleId': f'{title.id}',
|
|
'filter': {
|
|
'drmType': [
|
|
'WIDEVINE',
|
|
],
|
|
'format': [
|
|
'DASH',
|
|
],
|
|
},
|
|
},
|
|
}
|
|
|
|
# Update the headers
|
|
self.session.headers.update(headers)
|
|
|
|
# Send the post request
|
|
metadata = self.session.post(url="https://www.redbox.com/gapi/ondemand/hcgraphql/", json=json_data)
|
|
|
|
# Get the MPD
|
|
movie_mpd = metadata.json()['data']['avodStreams']['streams'][0]['url']
|
|
|
|
# Get the licenseRequestToken
|
|
title.data = metadata.json()['data']['avodStreams']['streams'][0]['licenseRequestToken']
|
|
|
|
# Grab the tracks from the MPD
|
|
tracks = DASH.from_url(url=movie_mpd).to_tracks(language="en")
|
|
|
|
# Removing subtitles from track as it causes devine to error
|
|
tracks.subtitles = []
|
|
|
|
# Return the tracks
|
|
return tracks
|
|
|
|
# Defining a function to get chapters
|
|
def get_chapters(self, title):
|
|
return []
|
|
|
|
# Define a function to get widevine license keys
|
|
def get_widevine_license(self, *, challenge: bytes, title: Title_T, track: AnyTrack) -> Optional[Union[bytes, str]]:
|
|
|
|
# Set the redbox license server
|
|
redbox_license_server = 'https://www.redbox.com/gapi/ondemand/WidevineRightsManager.aspx'
|
|
|
|
# Set the headers
|
|
headers = {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0',
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'en-US,en;q=0.5',
|
|
'licenserequesttoken': f'{title.data}',
|
|
}
|
|
|
|
# Update the headers
|
|
self.session.headers.update(headers)
|
|
|
|
# Get the license
|
|
license = self.session.post(url=redbox_license_server, data=challenge)
|
|
|
|
# Return the license
|
|
return license.content |