Merge branch 'sidequest' into apk-store

This commit is contained in:
saphid committed 2026-09-28 21:54:36 +10:00
commit a24d27013c
10 files changed
+809

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
"""SideQuest page links only: its terms do not authorise third-party scraping.
No API calls, cached listings or automated downloads. See docs/sidequest.md.
"""
from . import SourceError
KIND = 'sidequest'
URL = 'https://sidequestvr.com'
REASON = ('SideQuest is page-only: its terms restrict scraping and unauthorised '
'access. Browse and download with SideQuest, then import a developer-provided APK.')
def sources():
return [{'id': KIND, 'kind': KIND, 'name': 'SideQuest', 'url': URL,
'builtin': True, 'enabled': True, 'trust': 'community',
'page_only': True, 'reason': REASON}]
def search(source, query, limit=50):
# Do not invent catalogue results or interpret a query as a verified free app.
if limit <= 0:
return []
raise SourceError(REASON + ' ' + URL + '/apps')
def details(source, entry_id):
entry_id = str(entry_id)
if not entry_id.isascii() or not entry_id.isdecimal() or len(entry_id) > 12:
raise SourceError('SideQuest listing ids must be numeric')
return {'source': source['id'], 'id': entry_id, 'package': None,
'name': 'SideQuest listing ' + entry_id, 'summary': REASON,
'icon': None, 'page': URL + '/app/' + entry_id, 'version': None,
'version_code': None, 'min_sdk': None, 'abis': None, 'vr': None,
'size': None, 'free': None, 'license': None, 'updated': None,
'downloadable': False, 'versions': [], 'tags': [], 'headsets': [],
'images': {'icon': None, 'banner': None, 'screenshots': []}}
def download(source, entry_id, version_code=None):
entry = details(source, entry_id)
raise SourceError(REASON + ' ' + entry['page'])
+24
View File
@@ -8,6 +8,7 @@ Lepton Development, which wipes its apps on exit. See docs/apks.md.
Python stdlib only. CLI: python3 ui/frame_android.py
install APK [--vr|--flat] [--no-xr-compat] | info APK | versions APK-or-PKG
install-obb PKG OBB [OBB ...] | backup-data PKG ARCHIVE | restore-data PKG ARCHIVE
patch SRC DST [--add NAME=PATH ...] | list | launch PKG | stop PKG | remove PKG | probe PKG
"""
import json, os, re, shlex, shutil, struct, subprocess, sys, threading, time, zlib
@@ -333,6 +334,21 @@ def patch(src, dst, add=None):
raise FrameError(str(e)) from e
def install_obb(pkg, paths):
import frame_android_data
return frame_android_data.install_obb(pkg, paths)
def backup_data(pkg, destination):
import frame_android_data
return frame_android_data.backup_data(pkg, destination)
def restore_data(pkg, archive):
import frame_android_data
return frame_android_data.restore_data(pkg, archive)
def main():
cmd, *args = sys.argv[1:] or ['help']
try:
@@ -368,6 +384,14 @@ def main():
with open(path, 'rb') as f:
additions[entry] = f.read()
r = patch(opts.src, opts.dst, additions)
elif cmd == 'install-obb':
if len(args) < 2:
raise FrameError('install-obb requires PACKAGE OBB [OBB ...]')
r = install_obb(args[0], args[1:])
elif cmd in ('backup-data', 'restore-data'):
if len(args) != 2:
raise FrameError(cmd + ' requires PACKAGE ARCHIVE.tar.gz')
r = (backup_data if cmd == 'backup-data' else restore_data)(*args)
elif cmd == 'list':
r = list_apps()
elif cmd in ('launch', 'stop', 'probe'):
+133
View File
@@ -0,0 +1,133 @@
"""Expansion files and stopped-instance private-data backups. Python stdlib only."""
import hashlib
import json
import os
from pathlib import Path
import re
import runpy
import shlex
import subprocess
import tempfile
import uuid
import frame_android as android
REMOTE = Path(android.ROOT) / 'frame/android/app-data.py'
def _stream(command, src=None, dst=None):
try:
result = subprocess.run(['ssh', *android.SSH_OPTS, android.FRAME, command],
stdin=src if src else subprocess.DEVNULL,
stdout=dst if dst else subprocess.PIPE,
stderr=subprocess.PIPE, timeout=1800)
except subprocess.TimeoutExpired:
raise android.FrameError('app-data transfer timed out')
except OSError as error:
raise android.FrameError('app-data transfer failed: ' + str(error))
if result.returncode:
raise android.FrameError(result.stderr.decode(errors='replace').strip()[-600:] or
'app-data transfer failed')
return result.stdout
def _sha256(path):
digest = hashlib.sha256()
with open(path, 'rb') as src:
for chunk in iter(lambda: src.read(1024 * 1024), b''):
digest.update(chunk)
return digest.hexdigest()
def _meta(package):
if not android.PKG_RE.fullmatch(package or ''):
raise android.FrameError('invalid package name')
meta = android._meta_or_fail(package)
if meta['package'] != package:
raise android.FrameError('installed app metadata has a different package')
return meta
def install_obb(package, paths):
if not android.PKG_RE.fullmatch(package or ''):
raise android.FrameError('invalid package name')
paths = [Path(p).resolve() for p in paths]
if not paths:
raise android.FrameError('select at least one OBB file')
names = set()
for path in paths:
if (not re.fullmatch(r'(main|patch)\.[0-9]+\.' + re.escape(package) + r'\.obb', path.name)
or not path.is_file() or path.stat().st_size == 0 or path.name in names):
raise android.FrameError('OBB must be a nonempty main/patch.<version>.' + package + '.obb file')
names.add(path.name)
with android._install_lock:
meta = _meta(package)
container = 'lepton-steamlaunch-' + str(int(meta['instance']))
# No implicit launch. Android resolves /sdcard, never an assumed host path.
running = android.ssh('podman ps --format "{{.Names}}"').splitlines()
if container not in running:
raise android.FrameError('start this app instance before installing OBB data')
dest = '/sdcard/Android/obb/' + package
results = []
for path in paths:
digest = _sha256(path)
part = dest + '/.frame-' + uuid.uuid4().hex + '.part'
target = dest + '/' + path.name
script = (f'set -eu; mkdir -p {dest}; umask 002; '
f'trap "rm -f {part}" EXIT; cat > {part}; '
f'test "$(sha256sum {part} | cut -d " " -f 1)" = {digest}; '
f'chmod 664 {part}; mv {part} {target}')
command = shlex.join(['podman', 'exec', '-i', container, '/system/bin/sh', '-c', script])
with path.open('rb') as src:
_stream(command, src=src)
results.append({'name': path.name, 'path': target, 'sha256': digest})
return {'package': package, 'instance': meta['instance'], 'obb': results,
'verified': True}
def _data_command(action, meta):
container = 'lepton-steamlaunch-' + str(int(meta['instance']))
# Fail closed if podman cannot enumerate containers; don't mistake errors for stopped.
guard = ('running=$(podman ps --format "{{.Names}}") || exit 1; '
f'if printf "%s\\n" "$running" | grep -Fxq {shlex.quote(container)}; then '
'echo "stop the app before backup or restore" >&2; exit 1; fi; ')
return guard + shlex.join(['podman', 'unshare', 'python3', '-c', REMOTE.read_text(),
action, meta['package'], str(int(meta['instance']))])
def backup_data(package, destination):
destination = Path(destination).expanduser().absolute()
if destination.exists():
raise android.FrameError('backup destination already exists')
with android._install_lock:
meta = _meta(package)
fd, temporary = tempfile.mkstemp(prefix='.frame-backup-', dir=str(destination.parent))
try:
with os.fdopen(fd, 'wb') as dst:
_stream(_data_command('backup', meta), dst=dst)
result = _inspect(temporary, meta)
# Exclusive publication: a concurrently created backup is never overwritten.
os.link(temporary, str(destination))
return dict(result, path=str(destination), sha256=_sha256(destination))
finally:
os.unlink(temporary)
def _inspect(path, meta):
import tarfile
try:
return runpy.run_path(str(REMOTE))['inspect_archive'](path, meta['package'], int(meta['instance']))
except (OSError, EOFError, ValueError, tarfile.TarError) as error:
raise android.FrameError('invalid app-data backup: ' + str(error))
def restore_data(package, archive):
with android._install_lock:
meta = _meta(package)
_inspect(archive, meta)
with open(archive, 'rb') as src:
result = _stream(_data_command('restore', meta), src=src)
try:
return json.loads(result)
except (ValueError, TypeError):
raise android.FrameError('could not read restore result; inspect app data before retrying')