|
#!/usr/bin/env python3 |
|
# -*- coding: utf-8 -*- |
|
|
|
try: |
|
import argparse |
|
import datetime |
|
import json |
|
import os |
|
import urllib3 |
|
import re |
|
import shutil |
|
import subprocess |
|
import sys |
|
import time |
|
|
|
import certifi |
|
except ImportError as e: |
|
import sys |
|
if sys.version_info.major < 3: |
|
print('ERROR: This must be run on Python 3') |
|
try: |
|
input('Press [ENTER] to exit') |
|
finally: |
|
sys.exit() |
|
else: |
|
print('ERROR: {}'.format(e)) |
|
try: |
|
input('Press [ENTER] to exit') |
|
finally: |
|
sys.exit() |
|
|
|
CWD = os.path.dirname(os.path.realpath(__file__)) |
|
TEMP = os.path.join(CWD, 'tmp') |
|
OUTPUT = os.path.join(CWD, 'output') |
|
PKGTOOLS = os.path.join(CWD, 'tools') |
|
|
|
|
|
def safe_delete(path): |
|
if os.path.isfile(path): |
|
os.remove(path) |
|
|
|
|
|
def bootstrapped(): |
|
try: |
|
fail = False |
|
if not os.path.exists(TEMP): |
|
os.mkdir(TEMP) |
|
else: |
|
for root, dirs, files in os.walk(TEMP): |
|
for f in files: |
|
os.unlink(os.path.join(root, f)) |
|
for d in dirs: |
|
shutil.rmtree(os.path.join(root, d)) |
|
if not os.path.exists(OUTPUT): |
|
os.mkdir(OUTPUT) |
|
if not os.path.exists(PKGTOOLS): |
|
os.mkdir(PKGTOOLS) |
|
if not os.path.exists(os.path.join(PKGTOOLS, 'ext')): |
|
os.mkdir(os.path.join(PKGTOOLS, 'ext')) |
|
except (OSError, PermissionError): |
|
print('Could not create necessary directories') |
|
fail = True |
|
|
|
if not os.path.isfile(os.path.join(PKGTOOLS, 'orbis-pub-cmd.exe')): |
|
print('Missing "orbis-pub-cmd.exe" from tools directory!') |
|
fail = True |
|
|
|
if not os.path.isfile(os.path.join(PKGTOOLS, 'ext', 'sc.exe')): |
|
print('Missing "ext\\sc.exe" from tools directory!') |
|
fail = True |
|
|
|
if not os.path.isfile(os.path.join(PKGTOOLS, 'ext', 'di.exe')): |
|
print('Missing "ext\\di.exe" from tools directory!') |
|
fail = True |
|
|
|
if fail: |
|
try: |
|
input('Press [ENTER] to exit') |
|
finally: |
|
return False |
|
|
|
return True |
|
|
|
|
|
def build_header(lang, region): |
|
return { |
|
'Host': 'store.playstation.com', |
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0', |
|
'Accept': '*/*', |
|
'Accept-Language': '{}-{}'.format(lang, region), |
|
'Accept-Encodings': 'gzip, deflate, br', |
|
'Connection': 'keep-alive', |
|
'Pragma': 'no-cache', |
|
'Cache-Control': 'no-cache' |
|
} |
|
|
|
|
|
def get_json(http, url, header): |
|
response = http.request('GET', url, headers=header) |
|
|
|
return json.loads(response.data.decode('utf-8')) |
|
|
|
|
|
def validate_cid(cid): |
|
cid = cid.upper() |
|
|
|
if len(cid) == 9: |
|
cid = '{}_00'.format(cid) |
|
|
|
if len(cid) > 36: |
|
match = re.search(r'([A-Z]{2}[\d]{4}\-[A-Z]{4}[\d]{5}\_00\-[A-Z\d]{16})', cid) |
|
if match: |
|
return match.group(1) |
|
|
|
if re.match(r'^[A-Z]{2}[\d]{4}\-[A-Z]{4}[\d]{5}\_00\-[A-Z\d]{16}$', cid) or re.match(r'^[A-Z]{4}[\d]{5}\_00$', cid): |
|
return cid |
|
|
|
return '' |
|
|
|
|
|
def build_SFX(sfxFile, title, cid): |
|
pattern = re.compile(r'[\W_]+', re.UNICODE) |
|
safeTitle = pattern.sub('', title) |
|
sfx = '''<?xml version="1.0" encoding="utf-8" standalone="yes"?> |
|
<paramsfo> |
|
<param key="ATTRIBUTE">0</param> |
|
<param key="CATEGORY">ac</param> |
|
<param key="CONTENT_ID">{}</param> |
|
<param key="FORMAT">obs</param> |
|
<param key="TITLE">{}</param> |
|
<param key="TITLE_ID">{}</param> |
|
<param key="VERSION">01.00</param> |
|
</paramsfo>'''.format(cid, safeTitle, cid[7:16]) |
|
|
|
with open(sfxFile, 'wb+') as buf: |
|
buf.write(bytes(sfx, 'utf-8')) |
|
|
|
|
|
def build_GP4(gp4File, cid, sfoFile): |
|
generationTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
|
gp4 = '''<?xml version="1.0" encoding="utf-8" standalone="yes"?> |
|
<psproject fmt="gp4" version="1000"> |
|
<volume> |
|
<volume_type>pkg_ps4_ac_nodata</volume_type> |
|
<volume_id>PS4VOLUME</volume_id> |
|
<volume_ts>{}</volume_ts> |
|
<package content_id="{}" passcode="00000000000000000000000000000000"/> |
|
</volume> |
|
<files img_no="0"> |
|
<file targ_path="sce_sys/param.sfo" orig_path="{}"/> |
|
</files> |
|
<rootdir> |
|
<dir targ_name="sce_sys"/> |
|
</rootdir> |
|
</psproject>'''.format(generationTime, cid, sfoFile) |
|
|
|
with open(gp4File, 'w+') as buf: |
|
buf.write(gp4) |
|
|
|
|
|
def main(): |
|
if bootstrapped(): |
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
|
parser.add_argument('contentIDs', nargs='+', help='Content ID/Store URL to create PKGs for') |
|
parser.add_argument('-r', '--region', type=str, required=False, default='en/US', help='Region to search, will not effect full URLs') |
|
args = parser.parse_args() |
|
|
|
region = args.region |
|
if not region or not re.match(r'[a-zA-Z]{2}\/[a-zA-Z]{2}', region): |
|
print('Invalid Region!') |
|
return |
|
|
|
http = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where()) |
|
header = build_header(region.split('/')[0].lower(), region.split('/')[1].upper()) |
|
|
|
for cid in args.contentIDs: |
|
if len(cid) > 36: |
|
match = re.search(r'^http[s]{0,1}\:\/\/store\.playstation\.com\/([a-zA-Z]{2}\-[a-zA-Z]{2})\/', cid) |
|
if match: |
|
region = match.group(1).replace('-', '/') |
|
cid = validate_cid(cid) |
|
|
|
if not cid: |
|
print('Invalid Content ID!') |
|
continue |
|
|
|
url = 'https://store.playstation.com/valkyrie-api/{}/999/resolve/{}'.format(region, cid) |
|
try: |
|
data = get_json(http, url, header) |
|
except json.decoder.JSONDecodeError: |
|
print('Error Decoding JSON!') |
|
continue |
|
try: |
|
actualCid = data['included'][0]['id'] |
|
actualTid = actualCid[7:19] |
|
except KeyError: |
|
print('Error checking for redirect') |
|
continue |
|
|
|
if cid != actualCid and cid != actualTid: |
|
if len(cid) == 12: |
|
actual = actualTid |
|
else: |
|
actual = actualCid |
|
print('Expected {}, but found {}, skipping PKG creation'.format(cid, actual)) |
|
continue |
|
|
|
for entry in data['included']: |
|
try: |
|
if entry['attributes']['kamaji-relationship'] == 'add-ons' and (not entry['attributes']['file-size']['value'] or entry['attributes']['file-size']['unit'] == 'KB' or (entry['attributes']['file-size']['unit'] == 'MB' and entry['attributes']['file-size']['value'] < 3)): |
|
sfxFile = os.path.join(TEMP, entry['id'] + '.sfx') |
|
sfoFile = os.path.join(TEMP, entry['id'] + '.sfo') |
|
gp4File = os.path.join(TEMP, entry['id'] + '.gp4') |
|
pkgFile = os.path.join(OUTPUT, entry['id'] + '-A0000-V0100.pkg') |
|
|
|
try: |
|
build_SFX(sfxFile, entry['attributes']['name'], entry['id']) |
|
build_GP4(gp4File, entry['id'], sfoFile) |
|
|
|
orbisPub = os.path.join(PKGTOOLS, 'orbis-pub-cmd.exe') |
|
|
|
try: |
|
with open(os.devnull, 'w') as fnull: |
|
print('Creating {}...'.format(sfoFile.replace(TEMP + '\\', ''))) |
|
subprocess.check_call([orbisPub, 'sfo_create', sfxFile, sfoFile], stdout=fnull) |
|
print('Creating {}...'.format(pkgFile.replace(OUTPUT + '\\', ''))) |
|
subprocess.check_call([orbisPub, 'img_create', gp4File, pkgFile], stdout=fnull) |
|
except subprocess.CalledProcessError: |
|
print('Unable to create SFO/PKG!') |
|
|
|
safe_delete(sfxFile) |
|
safe_delete(sfoFile) |
|
safe_delete(gp4File) |
|
except KeyboardInterrupt: |
|
safe_delete(sfxFile) |
|
safe_delete(sfoFile) |
|
safe_delete(gp4File) |
|
safe_delete(pkgFile) |
|
print('Canceling!') |
|
return 0 |
|
except KeyError: |
|
pass |
|
|
|
print('Done!') |
|
return 0 |
|
|
|
return 1 |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |