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