Store: OBB game data becomes a follow-up 'Add game data' step

install_obb needs the app's running instance, which doesn't exist straight after
install, so the store no longer calls it there. The install result says the app
needs its game data; after opening the app once, 'Add game data' copies the
downloaded OBB files (a background job).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 22:13:42 +10:00
1 parent a9d78679ec
commit 9b0fedf602
4 files changed
+54 -7

No files matched your search

+18 -1
View File
@@ -20,6 +20,7 @@ from apk_sources import SourceError, SourceLimited
_lock = threading.RLock()
_running = {}
_pending = {} # source id -> newest query waiting for the running one
_game_data = {} # package -> downloaded OBB paths waiting for "Add game data"
_status = {}
TIMEOUT = 12
@@ -315,5 +316,21 @@ def install(source_id, entry_id, version_code=None, progress=None):
progress('Installing', None)
result = frame_android.install(downloaded['apk'], **kwargs)
if obb:
frame_android.install_obb(result['package'], obb)
# OBB files go into the app's own instance, which only exists while the app runs.
with _lock:
_game_data[result['package']] = list(obb)
result = dict(result, game_data=True, message='Installed ' + (entry.get('name') or result['package']) +
'. It also needs its game data: open it once on the Frame, then choose Add game data.')
return result
def add_game_data(package):
import frame_android
with _lock:
paths = _game_data.get(package)
if not paths:
raise SourceError('No downloaded game data is waiting for this app; install it again from the store')
result = frame_android.install_obb(package, paths)
with _lock:
_game_data.pop(package, None)
return dict(result, message='Game data added')
+10 -3
View File
@@ -2032,11 +2032,11 @@ function renderDetail() {
function updateInstall() {
const o=currentOffer();if(!o || !$("detailInstall"))return;
const job=sourceState.jobs.get(`${o.source}:${o.id}`), blocked=o.verdict?.tone==='blocked', page=sourcePage(o.page), b=$("detailInstall");
b.disabled=!!(job && !job.done) || (blocked && !job?.result) || (!o.downloadable && !page);
b.textContent=job?.result ? 'Open in Steam' : job && !job.done ? (job.stage||'Downloading')+(job.percent!=null?` ${Math.round(job.percent)}%`:'…') : blocked ? 'Not compatible' : o.downloadable ? (job?.error?'Try again':'Install on Frame') : 'Get from the creator ↗';
b.disabled=!!(job && !job.done) || !!job?.addingData || (blocked && !job?.result) || (!o.downloadable && !page);
b.textContent=job?.result?.game_data ? (job.addingData ? 'Adding game data…' : 'Add game data') : job?.result ? 'Open in Steam' : job && !job.done ? (job.stage||'Downloading')+(job.percent!=null?` ${Math.round(job.percent)}%`:'…') : blocked ? 'Not compatible' : o.downloadable ? (job?.error?'Try again':'Install on Frame') : 'Get from the creator ↗';
$("detailProgress").hidden=!job || job.done;
if(job?.percent!=null)$("detailProgress").value=job.percent;else $("detailProgress").removeAttribute("value");
$("detailInstallNote").textContent=job?.result ? '✓ Installed. Find it in your Steam library.' : job?.error || (job && !job.done ? 'Keep exploring. We’ll take care of the installation.' : o.verdict?.tone==='works' ? 'Adds to your Steam library.' : 'Compatibility can vary. Adds to your Steam library.');
$("detailInstallNote").textContent=job?.result?.game_data ? (job.dataError || 'Installed. It also needs its game data: open it once on the Frame, then choose Add game data.') : job?.result ? '✓ Installed. Find it in your Steam library.' : job?.error || (job && !job.done ? 'Keep exploring. We’ll take care of the installation.' : o.verdict?.tone==='works' ? 'Adds to your Steam library.' : 'Compatibility can vary. Adds to your Steam library.');
$("detailOffers").querySelectorAll('input').forEach(input=>input.disabled=!!job&&!job.done);
}
async function openStoreApp(index) {
@@ -2056,6 +2056,13 @@ async function loadOfferDetails() {
}
async function installStoreApp() {
const o=currentOffer(), key=`${o.source}:${o.id}`, existing=sourceState.jobs.get(key);
if(existing?.result?.game_data) {
let error="";existing.addingData=true;existing.dataError="";updateInstall();
const added=await runJob(`Add game data for ${o.name}`,`${key}:data`,()=>api('/api/sources',{action:'game-data',package:existing.result.package}),j=>{error=j.error||"";});
Object.assign(existing,{addingData:false,dataError:added?"":error||"We couldn't add the game data. Try again."});
if(added)existing.result={...existing.result,game_data:false};
updateInstall();return;
}
if(existing?.result) { await act(`Open ${o.name}`,()=>api('/api/android',{action:'launch',package:existing.result.package}),$("detailInstall"));return; }
if(!o.downloadable) { const url=sourcePage(o.page);if(url)window.open(url,'_blank','noopener,noreferrer');return; }
if(existing&&!existing.done)return;
+3
View File
@@ -1294,6 +1294,9 @@ def source_manage(body):
return {'message': message, 'source': {k: source.get(k) for k in
('id', 'name', 'fingerprint', 'trust_on_first_use')}}
return start_job('Add repository', add)
if action == 'game-data':
package = source_text(body, 'package')
return start_job('Add game data', lambda: apk_search.add_game_data(package))
if action == 'remove':
apk_search.manage_repo('remove_repo', source_id=source_text(body, 'source'))
return {'message': 'Repository removed'}