#!/usr/bin/env python3

### Extract param.json from the PS5 pkg link and display some information.
### 
### [Usage] show_ps5_pkg_metadata.py [-h] [--output] [URL of PS5 pkg (version.xml / .json / .DP.pkg / .sc.pkg)]
### [Examples]
### show_ps5_pkg_metadata.py https://sgst.prod.dl.playstation.net/sgst/prod/00/np/PPSA01280_00/d8ec167a-59da-4e54-8e2c-1161c706516a-version.xml
### show_ps5_pkg_metadata.py http://gst.prod.dl.playstation.net/gst/prod/00/PPSA01280_00/app/pkg/14/f_79cafe1a822dd62c55ebb4d08844deafe89d9e42366ddd8ade1d54de8f2f8eac/IP9100-PPSA01280_00-SFSRELE000000100-DP.pkg
### show_ps5_pkg_metadata.py https://sgst.prod.dl.playstation.net/sgst/prod/00/PPSA01280_00/app/info/13/f_504bd9d060d0861ae60bc680146dba2093041c29da446558392446d6eecc7330/IP9100-PPSA01280_00-SFSRELE000000100_sc.pkg
### show_ps5_pkg_metadata.py --output https://sgst.prod.dl.playstation.net/sgst/prod/00/np/PPSA01280_00/d8ec167a-59da-4e54-8e2c-1161c706516a-version.xml

import json
import urllib.request
import urllib.error
import sys
import ssl
import pprint
import argparse
import xml.etree.ElementTree as ET
ssl._create_default_https_context = ssl._create_unverified_context


def get_param_json(url, output=False):
    # URLの正当性を検証
    url = url.strip()
    
    #if 'gst.prod.dl.playstation.net' not in url:
    #    print(f'ERROR! Not PS5 pkg link')
    #    sys.exit(-1)

    if 'version.xml' in url:
        try:
            with urllib.request.urlopen(url) as res:
                xml_data = res.read()
        except urllib.error.HTTPError as err:
            if err.code == 404:
                print(f'error {err.code}')
                return
            elif err.code == 403:
                snoretoast('PS5 XML Check', f'ERROR! http_code: {err.code}')
                print(f'ERROR! http_code: {err.code}')
                sys.exit(-1)
        except urllib.error.URLError as err:
            print(f'error {err}')
            return 
        url = parse_ps5_xml(xml_data)[2]
        url = url.replace('.json', '_sc.pkg')

    if '.json' in url:
        url = url.replace('.json', '_sc.pkg')

    if 'DP.pkg' not in url and 'sc.pkg' not in url:
        print(f'ERROR! Not PS5 pkg link')
        sys.exit(-1)

    # ファイルを64KBのみダウンロード
    chunk_size = 1024 * 64
    try:
        with urllib.request.urlopen(url) as res:
            chunk = res.read(chunk_size)
    except urllib.error.HTTPError as err:
        if err.code == 404:
            print(f'error {err.code}')
        elif err.code == 403:
            snoretoast('PS5 XML Check', f'ERROR! http_code: {err.code}')
            print(f'ERROR! http_code: {err.code}')
            sys.exit(-1)
    except urllib.error.URLError as err:
        print(f'error {err}')

    json_data = extract_param_json(chunk)
    
    if output:
        out_file = 'param.json'
        with open(out_file, mode='wb') as f:
            f.write(json_data)
    
    return json.loads(json_data)


def parse_ps5_xml(xml_data):
    root = ET.fromstring(xml_data)
    tag_index = len(root[0]) - 1
    
    content_id = root[0].attrib['content_id']
    content_ver = root[0][tag_index].attrib['content_ver']
    manifest_url = root[0][tag_index].attrib['manifest_url']
    system_ver = root[0][tag_index].attrib['system_ver']
    
    try:
        delta_url = root[0][0].attrib['delta_url']
        delta_url_titileId = delta_url.split('/')[-1][7:19]
    except KeyError:
        delta_url = None
        delta_url_titileId = None
    
    system_ver_hex = f'{int(system_ver):x}'
    fw_version = '0' + '.'.join([system_ver_hex[0], system_ver_hex[1:3], system_ver_hex[3:5], system_ver_hex[5:7]])
    
    return content_id, content_ver, manifest_url, fw_version, delta_url, delta_url_titileId


def extract_param_json(data):
    #  "param.json" の位置を探す
    find_str = 'param.json'.encode()
    temp_position = data.find(find_str)
    
    find_str = b'\x7b\x0d\x0a'
    start_position = data.find(find_str, temp_position)
    
    find_str = 'version.xml'.encode()
    temp_position = data.find(find_str)
    
    # 0x7D 0D 0A json終端部分を探す
    find_str = b'\x7d\x0d\x0a'
    end_position = data.find(find_str, temp_position) + 3
    
    json_data = data[start_position:end_position]
    return json_data


def print_param(param_json):
    print('-'*100)
    pprint.pprint(param_json)

    param_json = adjust_param_value(param_json)
    target_keys = ['titleId',
                    'contentId',
                    'defaultLanguage',
                    'titleName',
                    'applicationCategoryType',
                    'applicationDrmType',
                    'attribute',
                    'attribute2',
                    'attribute3',
                    'userDefinedParam1',
                    'userDefinedParam2',
                    'userDefinedParam3',
                    'userDefinedParam4',
                    'contentVersion',
                    'targetContentVersion',
                    'masterVersion',
                    'requiredSystemSoftwareVersion',
                    'sdkVersion',
                    'creationDate',
                    'versionFileUri']

    print('-'*100)
    for key in target_keys:
        try:
            value = param_json[key]
        except KeyError:
            value = None
        
        print(f'{key: <32} = {value}')
    print('-'*100)


def adjust_param_value(param_json):
    # Sub Keyの取得
    param_json['defaultLanguage'] = param_json['localizedParameters']['defaultLanguage']
    param_json['titleName'] = param_json['localizedParameters'][param_json['defaultLanguage']]['titleName']
    param_json['creationDate'] =  param_json['pubtools']['creationDate']
    param_json['toolVersion'] = param_json['pubtools']['toolVersion']

    # 値の変換
    param_json['versionFileUri'] = param_json['versionFileUri'].strip()
    param_json['titleId'] = param_json['titleId'] + '_00'
    
    fw = param_json['requiredSystemSoftwareVersion']
    param_json['requiredSystemSoftwareVersion'] = '.'.join([fw[2:4], fw[4:6], fw[6:8], fw[8:10]]) + '-' + '.'.join([fw[10:12], fw[12:14], fw[14:16], fw[16:17], fw[17:18]])

    try:
        param_json['attribute'] = hex(param_json['attribute'])
        param_json['attribute2'] = hex(param_json['attribute2'])
        param_json['attribute3'] = hex(param_json['attribute3'])
        
        sdk = param_json['sdkVersion']
        param_json['sdkVersion'] = '.'.join([sdk[2:4], sdk[4:6], sdk[6:8], sdk[8:10]]) + '-' + '.'.join([sdk[10:12], sdk[12:14], sdk[14:16], sdk[16:17], sdk[17:18]])
    except Exception:
        pass
    
    return param_json


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Show PS5 Pkg Metadata')
    parser.add_argument('url', help='URL of PS5 pkg (version.xml / .json / .DP.pkg / _sc.pkg)')
    parser.add_argument('--output', action='store_true', help='Output param.json file to the same folder in the script')
    args = parser.parse_args()

    param_json = get_param_json(args.url, args.output)
    print_param(param_json)