Gplay-4k-Downloader/movie.py

257 wiersze
12 KiB
Python

import argparse
import base64
import binascii
import json
import os
import requests
import subprocess
import sys
from colorama import init, Fore
from prettytable import PrettyTable
from pywidevine.decrypt.wvdecrypt import WvDecrypt
init(autoreset=True)
class Main(object):
def __init__(self, folders, args):
self.folders = folders
self.args = args
self.auth_json = None
self.movie_id = args.url.split('id=')[-1]
self.movie_details = None
self.movie_resources = {}
self.mpd_representations = {'video': [], 'audio': [], 'subtitle': []}
self.license = None
def auth(self):
if os.path.exists('auth.json'):
with open('auth.json', 'r') as src:
self.auth_json = json.loads(src.read())
else:
sys.exit()
def requests_headers(self):
return {
'accept': '*/*',
'accept-language': 'en-US,en;q=0.9',
'authorization': 'Bearer {0}'.format(self.auth_json['authorization']),
'Host': 'www.googleapis.com',
'origin': 'chrome-extension://gdijeikdkaembjbdobgfkoidjkpbmlkd',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'cross-site',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
'x-client-data': '{0}'.format(self.auth_json['x-client-data']),
}
def get_movie_details(self):
url = 'https://www.googleapis.com/android_video/v1/asset/list?id=yt%3Amovie%3A{0}&if=imbrg&lr=en_US&cr=US&alt=json&access_token={1}&make=Google&model=ChromeCDM-Windows-x86-32&product=generic&device=generic'.format(self.movie_id, self.auth_json['authorization'])
self.movie_details = requests.get(url=url, headers=self.requests_headers()).json()
def get_movie_resources(self):
url = 'https://www.googleapis.com/android_video/v1/mpd?id=yt%3Amovie%3A{0}&ac3=true&all51=true&nd=false&all=false&secure=true&msu=false&ma=true&fc=true&hdcp=true&alt={1}&ssrc=googlevideo&access_token={2}&make=Google&model=ChromeCDM-Windows-x86-32&product=generic&device=generic'
self.movie_resources['json'] = requests.get(
url = url.format(self.movie_id, 'json', self.auth_json['authorization']),
headers = self.requests_headers()
).json()
#self.movie_resources['protojson'] = requests.get(
#url = url.format(self.movie_id, 'protojson', self.auth_json['authorization']),
#headers = self.requests_headers()
#).json()
def parse_movie_resources(self):
av_representations = self.movie_resources['json']['representations']
for x in av_representations:
if 'audio_info' not in x:
self.mpd_representations['video'].append({
'playback_url': x['playback_url'],
'codec': x['codec'],
'init': x['init'],
'bitrate': x['bitrate'],
'quality': str(x['height'])+'p',
'fps': x['video_fps']
})
elif 'audio_info' in x:
self.mpd_representations['audio'].append({
'playback_url': x['playback_url'],
'codec': x['codec'],
'init': x['init'],
'bitrate': x['bitrate'],
'language': x['audio_info']['language']
})
#subtitle_representations = self.movie_resources['protojson']['1007']['4']
#for x in subtitle_representations:
#self.mpd_representations['subtitle'].append({
#'language': x['1'],
#'url': x['3'] ,
#'format': x['5']
#})
def aria2c(self, url, output_file_name):
aria2c = os.path.join(self.folders['binaries'], 'aria2c.exe')
aria2c_command = [
aria2c, url,
'-d', self.folders['temp'], '-j16',
'-o', output_file_name, '-s16', '-x16',
'-U', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36',
'--allow-overwrite=false',
'--async-dns=false',
'--auto-file-renaming=false',
'--console-log-level=warn',
'--retry-wait=5',
'--summary-interval=0',
]
subprocess.run(aria2c_command)
return os.path.join(self.folders['temp'], output_file_name)
def extract_pssh(self, mp4_file):
mp4dump = os.path.join(self.folders['binaries'], 'mp4dump.exe')
wv_system_id = '[ed ef 8b a9 79 d6 4a ce a3 c8 27 dc d5 1d 21 ed]'
pssh = None
data = subprocess.check_output([mp4dump, '--format', 'json', '--verbosity', '1', mp4_file])
data = json.loads(data)
for atom in data:
if atom['name'] == 'moov':
for child in atom['children']:
if child['name'] == 'pssh' and child['system_id'] == wv_system_id:
pssh = child['data'][1:-1].replace(' ', '')
pssh = binascii.unhexlify(pssh)
pssh = pssh[0:]
pssh = base64.b64encode(pssh).decode('utf-8')
return pssh
def license_request(self, pssh):
license_url = 'https://play.google.com/video/license/GetCencLicense?source=YOUTUBE&video_id={0}&oauth={1}'.format(self.movie_id, self.auth_json['authorization'])
license_headers = {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'
}
wvdecrypt = WvDecrypt(pssh)
challenge = wvdecrypt.get_challenge()
resp = requests.post(url=license_url, headers=license_headers, data=challenge)
resp1 = resp.content.split('\r\n\r\n'.encode('utf-8'))
resp2 = resp1[1]
license_b64 = base64.b64encode(resp2).decode('utf-8')
wvdecrypt.update_license(license_b64)
keys = wvdecrypt.start_process()
return keys
def mp4decrypt(self, keys):
mp4decrypt_command = [os.path.join(self.folders['binaries'], 'mp4decrypt.exe')]
for key in keys:
if key.type == 'CONTENT':
mp4decrypt_command.append('--show-progress')
mp4decrypt_command.append('--key')
mp4decrypt_command.append('{}:{}'.format(key.kid.hex(), key.key.hex()))
return mp4decrypt_command
def decrypt(self, keys, input, output):
mp4decrypt_command = self.mp4decrypt(keys)
mp4decrypt_command.append(input)
mp4decrypt_command.append(output)
wvdecrypt_process = subprocess.Popen(mp4decrypt_command)
wvdecrypt_process.communicate()
wvdecrypt_process.wait()
def video(self):
table = PrettyTable()
table.field_names = ['ID', 'CODEC', 'QUALITY', 'BITRATE', 'FPS']
for i, j in enumerate(self.mpd_representations['video']):
table.add_row([i, j['codec'], j['quality'], j['bitrate'], j['fps']])
print('\n' + Fore.RED + 'VIDEO')
print(table)
selected_video = self.mpd_representations['video'][int(input('ID: '))]
init_url = selected_video['playback_url'] + '?range={0}-{1}'.format(selected_video['init']['first'], selected_video['init']['last'])
self.aria2c(init_url, 'init.mp4')
selected_video['pssh'] = self.extract_pssh(os.path.join(self.folders['temp'], 'init.mp4'))
os.remove(os.path.join(self.folders['temp'], 'init.mp4'))
print(Fore.YELLOW+'\nAcquiring Content License')
self.license = self.license_request(selected_video['pssh'])
print(Fore.GREEN+'License Acquired Successfully')
print(Fore.YELLOW+'\nURL:', selected_video['playback_url'])
if not self.args.keys:
output_file_name = self.movie_details['resource'][0]['metadata']['title'] + ' ' + f'[{selected_video["quality"]}] Encrypted.mp4'
print(Fore.YELLOW+'\nDownloading', output_file_name)
video_downloaded = self.aria2c(selected_video['playback_url'], output_file_name.replace(':', ''))
print(Fore.YELLOW+'\nDecrypting Video')
self.decrypt(self.license, video_downloaded, video_downloaded.replace(' Encrypted', ''))
os.remove(video_downloaded)
else:
print(Fore.GREEN + 'n\KEYS')
for key in self.license:
if key.type == 'CONTENT':
print('{}:{}'.format(key.kid.hex(), key.key.hex()))
def audio(self):
table = PrettyTable()
table.field_names = ['ID', 'CODEC', 'BITRATE', 'LANGUAGE']
for i, j in enumerate(self.mpd_representations['audio']):
table.add_row([i, j['codec'], j['bitrate'], j['language']])
print('\n' + Fore.RED +'AUDIO')
print(table)
selected_audio = input('ID: ')
if self.args.audio:
init_url = self.mpd_representations['audio'][int(selected_audio.split(',')[-1])]['playback_url']
init_url += '?range={0}-{1}'.format(self.mpd_representations['audio'][int(selected_audio.split(',')[-1])]['init']['first'], self.mpd_representations['audio'][int(selected_audio.split(',')[-1])]['init']['last'])
self.aria2c(init_url, 'init.mp4')
pssh = self.extract_pssh(os.path.join(self.folders['temp'], 'init.mp4'))
os.remove(os.path.join(self.folders['temp'], 'init.mp4'))
print(Fore.YELLOW+'\nAcquiring Content License')
self.license = self.license_request(pssh)
print(Fore.GREEN+'License Acquired Successfully')
for x in selected_audio.split(','):
x = int(x.strip())
playback_url = self.mpd_representations['audio'][x]['playback_url']
print(Fore.YELLOW+'\nURL:', playback_url)
if not self.args.keys:
output_file_name = self.movie_details['resource'][0]['metadata']['title'] + ' ' + f'[{self.mpd_representations["audio"][x]["language"]}-{self.mpd_representations["audio"][x]["codec"]}-{self.mpd_representations["audio"][x]["bitrate"]}] Encrypted.mp4'
print(Fore.YELLOW+'\nDownloading', output_file_name)
audio_downloaded = self.aria2c(playback_url, output_file_name.replace(':', ''))
self.decrypt(self.license, audio_downloaded, audio_downloaded.replace(' Encrypted', ''))
os.remove(audio_downloaded)
else:
print(Fore.GREEN + 'n\KEYS')
for key in self.license:
if key.type == 'CONTENT':
print('{}:{}'.format(key.kid.hex(), key.key.hex()))
def subtitle(self):
table = PrettyTable()
table.field_names = ['ID', 'LANGUAGE', 'FORMAT']
for i, j in enumerate(self.mpd_representations['subtitle']):
table.add_row([i, j['language'], j['format']])
print('\n' + Fore.RED +'SUBTITLE')
print(table)
selected_subtitle = input('ID: ')
for x in selected_subtitle.split(','):
x = int(x.strip())
url = self.mpd_representations['subtitle'][x]['url']
output_file_name = self.movie_details['resource'][0]['metadata']['title'] + ' ' + f'{self.mpd_representations["subtitle"][x]["language"]}-{self.mpd_representations["subtitle"][x]["format"]}'
print(Fore.YELLOW+'\nDownloading', output_file_name)
self.aria2c(url, output_file_name)
cwd = os.getcwd()
folders = {'binaries': os.path.join(cwd, 'binaries'), 'output': os.path.join(cwd, 'output'), 'temp': os.path.join(cwd, 'temp')}
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('-u', '--url', required=True)
arg_parser.add_argument('-a', '--audio', action='store_true')
arg_parser.add_argument('-k', '--keys', action='store_true')
args = arg_parser.parse_args()
if __name__ == "__main__":
movie = Main(folders, args)
movie.auth()
movie.get_movie_details()
movie.get_movie_resources()
movie.parse_movie_resources()
if not args.audio:
#movie.subtitle()
movie.video()
movie.audio()