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

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