Modernize
build-pack / build (push) Has been cancelled

This commit is contained in:
2026-03-18 09:16:56 -07:00
parent 0538f99e02
commit 3d86f19c0f
5 changed files with 682 additions and 59 deletions
+218 -2
View File
@@ -1,2 +1,218 @@
/__temp__/
token.txt
__temp__
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
+310
View File
@@ -0,0 +1,310 @@
import json, os, queue, threading, requests, sqlite3, zipfile, argparse
from enum import Enum
from pathlib import Path
def main(platform="quest", locale="en_US", action="assets", output_dir_param="."):
global forced_locale
global hmd_type
global target_path
global output_dir
output_dir = Path(output_dir_param)
hmd_type = HMDTypes[platform.upper()]
forced_locale = locale
target_path = Path(output_dir) / hmd_type.value["name"]
target_path.mkdir(parents=True, exist_ok=True)
print("Retrieving Appinfo")
retrieve_app_info()
print("Resolving package names")
retrieve_all_package_names()
if action == "assets":
print("Downloading images")
download_images()
json_filename = (
"appnames_" + hmd_type.value["name"] + "_" + forced_locale + ".json"
)
print("Creating " + json_filename)
create_app_names_json(json_filename)
zip_filename = "iconpack_" + hmd_type.value["name"] + ".zip"
print("Creating " + zip_filename)
create_iconpack(zip_filename)
elif action == "free":
json_filename = "free_apps_" + hmd_type.value["name"] + ".json"
print("Creating " + json_filename)
create_free_apps_json(json_filename)
connection.close()
class HMDTypes(Enum):
QUEST = {"sectionID": "1888816384764129", "hmdType": "MONTEREY", "name": "quest"}
RIFT = {"sectionID": "1736210353282450", "hmdType": "RIFT", "name": "rift"}
GO = {"sectionID": "174868819587665", "hmdType": "PACIFIC", "name": "go"}
GEARVR = {"sectionID": "174868819587665", "hmdType": "GEARVR", "name": "gearvr"}
hmd_type = None
forced_locale = None
target_path = None
url = "https://graph.oculus.com/graphql"
app_list = dict()
connection = sqlite3.connect("packages.db")
cursor = connection.cursor()
cursor.execute(
"CREATE TABLE IF NOT EXISTS packages (app_id TEXT PRIMARY_KEY, package_name TEXT)"
)
uri_queue = queue.Queue()
def retrieve_app_info():
sectionCursor = None
has_next_page = True
while has_next_page:
sectionID = hmd_type.value["sectionID"]
hmdType = hmd_type.value["hmdType"]
variables = {
"sectionId": sectionID,
"sortOrder": None,
"sectionItemCount": 100,
"hmdType": hmdType,
}
if sectionCursor is not None:
variables["sectionCursor"] = sectionCursor
parameters = {
"forced_locale": forced_locale,
"access_token": "OC|1317831034909742|",
"variables": json.dumps(variables),
"doc_id": "1934814353250664",
}
response = requests.get(url, params=parameters)
json_response = response.json()
if "data" not in json_response:
print("API ERROR:", json_response)
break
all_items = json_response["data"]["node"]["all_items"]
edges = all_items["edges"]
page_info = all_items["page_info"]
has_next_page = page_info["has_next_page"]
sectionCursor = page_info.get("end_cursor")
for edge in edges:
app_id = edge["node"]["id"]
app_name = edge["node"]["display_name"]
app_image_uri = edge["node"]["cover_landscape_image"]["uri"]
node = edge["node"]
offer = node.get("current_offer") or {}
price_info = offer.get("price") or {}
price = price_info.get("offset_amount")
app_list[app_id] = {
"name": app_name,
"uri": app_image_uri,
"price": price,
"package_name": None,
}
print(f"\rFetched {len(app_list)} apps...", end="", flush=True)
# Safety: if cursor didn't advance, bail to avoid infinite loop
if not sectionCursor:
break
print()
print("Retrieved: " + str(len(app_list)))
def retrieve_all_package_names():
"""Resolve all package names upfront and store them in app_list."""
resolved = 0
skipped = 0
for app_id in list(app_list.keys()):
package_name = retrieve_package_name(app_id)
app_list[app_id]["package_name"] = package_name
if package_name:
resolved += 1
else:
skipped += 1
print(f"Resolved: {resolved}, Skipped: {skipped}")
def retrieve_package_name(app_id):
cursor.execute("SELECT package_name FROM packages WHERE app_id=?", (app_id,))
result = cursor.fetchone()
if result is not None:
return result[0]
print("Fetching package_name for app_id " + app_id)
parameters = {
"access_token": "OC|1317831034909742|",
"variables": json.dumps({"id": str(app_id)}),
"doc_id": "3326435794041206",
}
request = requests.get(url, params=parameters)
response = request.json()
if "data" not in response:
print("API ERROR (package lookup):", response)
return None
node = response["data"].get("node") or {}
binary = node.get("latestBinary") or {}
package_name = binary.get("package_name")
if not package_name:
print(f"Skipping app_id {app_id} (no package_name)")
return None
cursor.execute(
"INSERT INTO packages (app_id, package_name) VALUES (?,?)",
(app_id, package_name),
)
connection.commit()
return package_name
def download_file(file_url, filename):
os.makedirs((os.path.dirname(filename)), exist_ok=True)
if not os.path.isfile(filename):
response = requests.get(file_url, stream=True)
print("Downloading:" + str(filename) + " from " + file_url)
with open(filename, "wb") as out_file:
for chunk in response.iter_content(chunk_size=1048576):
out_file.write(chunk)
def download_images():
for app_id, app_info in app_list.items():
package_name = app_info["package_name"]
if not package_name:
continue
uri_queue.put(
{
"package_name": target_path / (package_name + ".jpg"),
"uri": app_info["uri"],
}
)
num_worker_threads = 8
threads = []
for i in range(num_worker_threads):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
uri_queue.join()
for t in threads:
t.join()
def worker():
while 1:
try:
queue_item = uri_queue.get(block=False)
download_file(queue_item["uri"], queue_item["package_name"])
uri_queue.task_done()
except queue.Empty:
break
def create_app_names_json(json_filename):
app_names = dict()
for app_id, app_info in app_list.items():
package_name = app_info["package_name"]
if not package_name:
continue
app_names[package_name] = {"name": app_info["name"]}
with open(Path(output_dir) / json_filename, "w", encoding="utf-8") as file:
json.dump(app_names, file, sort_keys=True, indent=0, ensure_ascii=False)
def create_free_apps_json(json_filename):
free_apps = dict()
json_path = output_dir / json_filename
if json_path.is_file():
with open(json_path) as file:
free_apps = json.load(file)
for app_id, app_info in app_list.items():
if app_info["price"] == 0:
package_name = app_info["package_name"]
if not package_name:
continue
free_apps[app_id] = {
"name": app_info["name"],
"package": package_name,
}
with open(json_path, "w", encoding="utf-8") as file:
json.dump(free_apps, file, sort_keys=True, indent=0, ensure_ascii=False)
def create_iconpack(zip_filename):
with zipfile.ZipFile(Path(output_dir) / zip_filename, "w") as iconpack:
for app_id, app_info in app_list.items():
package_name = app_info["package_name"]
if not package_name:
continue
iconpack.write(target_path / (package_name + ".jpg"), package_name + ".jpg")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Download assets for QuestAppLauncher")
parser.add_argument(
"-p", "--platform", default="quest", choices=["quest", "rift", "go", "gearvr"]
)
parser.add_argument(
"-l",
"--locale",
default="en_US",
choices=[
"en_US",
"en_GB",
"de_DE",
"cs_CZ",
"da_DK",
"el_GR",
"es_ES",
"es_LA",
"fi_FI",
"fr_FR",
"it_IT",
"ja_JP",
"ko_KR",
"nb_NO",
"nl_NL",
"pl_PL",
"pt_BR",
"pt_PT",
"ro_RO",
"ru_RU",
"sv_SE",
"tr_TR",
"zh_CN",
"zh_HK",
"zh_TW",
],
)
parser.add_argument(
"-a",
"--action",
default="assets",
choices=["assets", "free"],
)
args = parser.parse_args()
main(args.platform, args.locale, args.action)
+154 -57
View File
@@ -1,74 +1,102 @@
import argparse, datetime, os, requests, shutil, subprocess, sys, zipfile
from github import Github # pip install PyGithub
from QuestAssetDL import main as QuestAssetDL
from github import Github, Auth
# Temporary directory for scratch space
TEMP_DIR = '__temp__'
LATEST_RELASE_DIR = 'latest_release'
QALAG_OUTPUT_DIR = 'qalag'
TEMP_DIR = "__temp__"
LATEST_RELASE_DIR = "latest_release"
QALAG_OUTPUT_DIR = "qalag"
QALAG_EXE_PATH = 'bin\\QALAG.exe'
WINDIFF_EXE_PATH = 'bin\\WinDiff.exe'
WINDIFF_EXE_PATH = "bin\\WinDiff.exe"
QUEST_DIR = "quest"
ICONPACK_QUEST = "iconpack_quest.zip"
APPNAMES_QUEST = "appnames_quest.json"
ICONPACK_OTHER = "iconpack_others.zip"
APPNAMES_OTHER = "appnames_other.json"
APPNAMES_GALAG_NAME = "appnames_quest_en_US.json"
QUEST_DIR = 'quest'
ICONPACK_QUEST = 'iconpack_quest.zip'
APPNAMES_QUEST = 'appnames_quest.json'
ICONPACK_OTHER = 'iconpack_others.zip'
APPNAMES_OTHER = 'appnames_other.json'
APPNAMES_GALAG_NAME = 'appnames_quest_en_US.json'
def main():
parser = argparse.ArgumentParser(description='Quest Asset Generator')
parser.add_argument('-a', '--access-token', required=True, help='GitHub acess token')
parser.add_argument('-dr', '--download-release', action='store_true', help='Download latest asset release from github')
parser.add_argument('-da', '--download-assets', action='store_true',help='Download assets from Oculus')
parser.add_argument('-c', '--compare', action='store_true', help='Compare assets')
parser.add_argument('-r', '--release', action='store_true', help='Draft a github release')
parser = argparse.ArgumentParser(description="Quest Asset Generator")
parser.add_argument(
"-a", "--access-token", required=True, help="GitHub acess token"
)
parser.add_argument(
"-dr",
"--download-release",
action="store_true",
help="Download latest asset release from github",
)
parser.add_argument(
"-da",
"--download-assets",
action="store_true",
help="Download assets from Oculus",
)
parser.add_argument("-c", "--compare", action="store_true", help="Compare assets")
parser.add_argument(
"-r", "--release", action="store_true", help="Draft a github release"
)
args = parser.parse_args()
# If nothing is specified, perform all actions
if (not args.download_release and not args.download_assets and not args.compare and not args.release):
if (
not args.download_release
and not args.download_assets
and not args.compare
and not args.release
):
args.download_release = True
args.download_assets = True
args.compare = True
args.release = True
# Instantiate github client
g = Github(args.access_token)
repo = g.get_user("HooverHigh").get_repo('QuestAppLauncher_Assets')
ga = Auth.Token(args.access_token)
g = Github(auth=ga)
repo = g.get_user("HooverHigh").get_repo("QuestAppLauncher_Assets")
# Set up temp dir
folder_path_temp = os.path.abspath(TEMP_DIR);
if (not os.path.isdir(folder_path_temp)):
os.mkdir(folder_path_temp)
folder_path_temp = os.path.abspath(TEMP_DIR)
if not os.path.isdir(folder_path_temp):
os.mkdir(folder_path_temp)
# Download latest assets
if (args.download_assets):
if args.download_assets:
download_latest_assets()
# Download latest release
if (args.download_release):
if args.download_release:
download_release_assets(repo)
if (args.compare):
if args.compare:
compare()
if (args.release):
if args.release:
create_release(repo)
def get_release_download_path():
return os.path.join(os.path.abspath(TEMP_DIR), LATEST_RELASE_DIR)
def get_galag_download_path():
return os.path.abspath(os.path.join(os.path.abspath(TEMP_DIR), QALAG_OUTPUT_DIR))
# Download latest Github release
def download_release_assets(repo):
release = repo.get_latest_release()
try:
release = repo.get_latest_release()
except Exception:
print("No published releases found, skipping download")
return
print("Downloading release: '%s'" % (release.title))
download_release_path = get_release_download_path();
if (os.path.isdir(download_release_path)):
download_release_path = get_release_download_path()
if os.path.isdir(download_release_path):
shutil.rmtree(download_release_path)
os.mkdir(download_release_path)
@@ -76,9 +104,13 @@ def download_release_assets(repo):
for asset in assets:
print("\tAsset: %s [%s]" % (asset.name, asset.url))
r = requests.get(asset.url, allow_redirects=True, headers={'Accept': 'application/octet-stream'})
r = requests.get(
asset.url,
allow_redirects=True,
headers={"Accept": "application/octet-stream"},
)
file_path = os.path.join(download_release_path, asset.name)
open(file_path, 'wb').write(r.content)
open(file_path, "wb").write(r.content)
# Extract quest icons
iconpack_quest_release_zip = os.path.join(download_release_path, ICONPACK_QUEST)
@@ -86,46 +118,105 @@ def download_release_assets(repo):
with zipfile.ZipFile(iconpack_quest_release_zip) as zip:
zip.extractall(iconpack_quest_release_ext_path)
# Download latest assets
def download_latest_assets():
galag_exe_full_path = os.path.abspath(QALAG_EXE_PATH)
galag_output_dir = get_galag_download_path()
if (os.path.isdir(galag_output_dir)):
if os.path.isdir(galag_output_dir):
shutil.rmtree(galag_output_dir)
os.mkdir(galag_output_dir)
# Launch exe, temporarily changing cwd to land results in proper place
cur_dir = os.path.abspath('.')
os.chdir(galag_output_dir)
launch_executable([], galag_exe_full_path)
os.chdir(cur_dir)
QuestAssetDL(
platform="quest",
locale="en_US",
action="assets",
output_dir_param=galag_output_dir,
)
# Rename file
os.rename(os.path.join(galag_output_dir, APPNAMES_GALAG_NAME), os.path.join(galag_output_dir, APPNAMES_QUEST))
for f in os.listdir(galag_output_dir):
if f.startswith("appnames_quest") and f.endswith(".json"):
os.rename(
os.path.join(galag_output_dir, f),
os.path.join(galag_output_dir, APPNAMES_QUEST),
)
break
else:
raise FileNotFoundError("No appnames_quest JSON found")
# Compare
def compare():
iconpack_quest_release_ext_path = os.path.join(get_release_download_path(), QUEST_DIR)
iconpack_quest_generated_ext_path = os.path.join(get_galag_download_path(), QUEST_DIR)
launch_executable(['-t', iconpack_quest_release_ext_path, iconpack_quest_generated_ext_path], bin_path=WINDIFF_EXE_PATH)
# Check if we have a release to compare against
release_path = get_release_download_path()
if not os.path.isdir(release_path) or not os.path.exists(
os.path.join(release_path, ICONPACK_QUEST)
):
print("No previous release assets found, skipping comparison")
return
# Proceed with comparison
iconpack_quest_release_ext_path = os.path.join(release_path, QUEST_DIR)
iconpack_quest_generated_ext_path = os.path.join(
get_galag_download_path(), QUEST_DIR
)
launch_executable(
["-t", iconpack_quest_release_ext_path, iconpack_quest_generated_ext_path],
bin_path=WINDIFF_EXE_PATH,
)
appnames_quest_release_path = os.path.join(release_path, APPNAMES_QUEST)
appnames_quest_generated_path = os.path.join(
get_galag_download_path(), APPNAMES_QUEST
)
launch_executable(
[appnames_quest_release_path, appnames_quest_generated_path],
bin_path=WINDIFF_EXE_PATH,
)
appnames_quest_release_path = os.path.join(get_release_download_path(), APPNAMES_QUEST)
appnames_quest_generated_path = os.path.join(get_galag_download_path(), APPNAMES_QUEST)
launch_executable([appnames_quest_release_path, appnames_quest_generated_path], bin_path=WINDIFF_EXE_PATH)
# Create release
def create_release(repo):
tag = 'v' + datetime.date.today().strftime('%m.%d.%Y')
name = tag + ': Update quest assets'
print(str.format('Creating release: tag: %s, name: %s' % (tag, name)))
release = repo.create_git_release(tag=tag, name=name, message='Updating quest assets', draft=True)
tag = "v" + datetime.date.today().strftime("%m.%d.%Y")
name = tag + ": Update quest assets"
print(f"Creating release: tag: {tag}, name: {name}")
release = repo.create_git_release(
tag=tag, name=name, message="Updating quest assets", draft=True
)
# Paths for "other" assets (from previous release)
other_appnames = os.path.join(get_release_download_path(), APPNAMES_OTHER)
other_iconpack = os.path.join(get_release_download_path(), ICONPACK_OTHER)
# Upload other assets only if they exist (first release will skip)
if os.path.exists(other_appnames):
release.upload_asset(other_appnames)
print(f"Uploaded {APPNAMES_OTHER}")
else:
print(f"Warning: {APPNAMES_OTHER} not found, skipping upload")
if os.path.exists(other_iconpack):
release.upload_asset(other_iconpack)
print(f"Uploaded {ICONPACK_OTHER}")
else:
print(f"Warning: {ICONPACK_OTHER} not found, skipping upload")
# Quest assets are always uploaded from the freshly downloaded data
quest_appnames = os.path.join(get_galag_download_path(), APPNAMES_QUEST)
quest_iconpack = os.path.join(get_galag_download_path(), ICONPACK_QUEST)
# Ensure these files exist (they should, from download_latest_assets)
if os.path.exists(quest_appnames):
release.upload_asset(quest_appnames)
else:
raise FileNotFoundError(f"Missing generated Quest appnames: {quest_appnames}")
if os.path.exists(quest_iconpack):
release.upload_asset(quest_iconpack)
else:
raise FileNotFoundError(f"Missing generated Quest iconpack: {quest_iconpack}")
# Upload the assets, refreshing quest from generated path
release.upload_asset(os.path.join(get_release_download_path(), APPNAMES_OTHER))
release.upload_asset(os.path.join(get_release_download_path(), ICONPACK_OTHER))
release.upload_asset(os.path.join(get_galag_download_path(), APPNAMES_QUEST))
release.upload_asset(os.path.join(get_galag_download_path(), ICONPACK_QUEST))
# Launch executable and return output
def launch_executable(args, bin_path):
@@ -133,8 +224,14 @@ def launch_executable(args, bin_path):
output = subprocess.check_output([bin_path] + args, stderr=subprocess.STDOUT)
return output.decode(sys.stdout.encoding)
except subprocess.CalledProcessError as e:
raise Exception(str.format(str(e) + ". Output: '%s'" % (e.output.decode(sys.stdout.encoding).rstrip()))) from e
raise Exception(
str.format(
str(e)
+ ". Output: '%s'" % (e.output.decode(sys.stdout.encoding).rstrip())
)
) from e
except Exception as e:
raise type(e)(str.format(str(e) + " when calling '%s'" % (bin_path)))
main()
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.