Skip to content

Instantly share code, notes, and snippets.

@Etheldreda-MU
Last active May 10, 2021 21:27
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Etheldreda-MU/5baa0c6166caf6a7074a7caf0737db01 to your computer and use it in GitHub Desktop.
Save Etheldreda-MU/5baa0c6166caf6a7074a7caf0737db01 to your computer and use it in GitHub Desktop.
0-byte PSN DLC Downloader

Requirements

  • Windows
  • Python3 with urllib3 and certifi
  • Fake PKG Tools (tools/orbis-pub-cmd.exe, tools/ext/sc.exe, tools/ext/di.exe)

How to use

  • Download main.py
  • Create a tools directory in the same place as main.py and place the Fake PKG tools in it
  • Run in the command line with python main.py X
    • X can be the Title ID (CUSA05258), Content ID (UP0700-CUSA05258_00-PS4TOBERSERIA001), or the store URL (https://store.playstation.com/en-us/product/UP0700-CUSA05258_00-PS4TOBERSERIA001)
    • You may specify a region with -r ja/JP the region is only used with the Title/Content ID. The region is taken from the URL when using a URL input.
      • Default if not specified is en/US
    • You may specify more than one input python main.py CUSA05258 CUSA2510, however you may only specify one region at a time (Unless using URL input as regions are pulled from the URL)
  • PKG files will be created in output

Credits

@TheRadziu - Original Concept

@Al-Azif - API Info and Python Help

#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment