Artwork sources: every optional source falls back, within limits

- Any failure of a source image or SteamGridDB (HTTPException, odd JSON) now
  becomes a warning and generated art, never an aborted install; refresh-art
  --all reports each app and carries on.
- URL artwork goes through apk_sources._images: public addresses only, at most
  three redirects, and one overall deadline for all of an install's fetches.
- PNGs are checked from their header only (any depth or interlace; Steam's
  Chromium decodes them), JPEGs may have trailing padding, and 4K screenshots
  are within limits. The slow pure-Python decoder is gone.
- SteamGridDB title matching keeps letters of every script and never matches
  on an empty name. One warning per source slot, not per candidate.

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 23:06:15 +10:00
1 parent 70897cfd1d
commit baefa105a9
6 files changed
+158 -202

No files matched your search

+15 -2
View File
@@ -140,8 +140,10 @@ class EntryPoints(unittest.TestCase):
stop.assert_not_called(); copy.assert_not_called(); self.assert_art()
def test_all_refresh_reports_partial_failures(self):
import http.client
with patch.object(android, 'list_apps', return_value=[{'package':'org.a.game'},{'package':'org.b.game'}]), \
patch.object(android, '_meta_or_fail', side_effect=android.FrameError('missing')):
patch.object(android, '_meta_or_fail', side_effect=[http.client.RemoteDisconnected('gone'),
AttributeError('odd')]):
result=android.refresh_art()
self.assertEqual(len(result),2)
self.assertTrue(all('error' in a for a in result))
@@ -173,7 +175,7 @@ class SteamGridDB(unittest.TestCase):
def test_exact_match_and_top_votes_per_slot(self):
calls=[]
def get(path,key):
def get(path,key,deadline=None):
calls.append(path)
if 'search' in path: return [{'id':1,'name':'Other Game'},{'id':2,'name':'Game'}]
dims=(600,900) if '600x900' in path else (920,430)
@@ -186,6 +188,17 @@ class SteamGridDB(unittest.TestCase):
self.assertTrue(all(url.endswith('/top.png') for url in images.values()))
self.assertTrue(all('/game/2?' in p for p in calls[1:]))
def test_unicode_titles_match_exactly_and_symbols_never_match_all(self):
self.assertEqual(sgdb._name('ビートセイバー VR!'), 'ビートセイバーvr')
with patch.object(sgdb,'api_key',return_value='test-key'), \
patch.object(sgdb,'_get',return_value=[{'id':1,'name':'Unrelated'},{'id':2,'name':'!!!'}]) as get:
self.assertEqual(sgdb.lookup('★★★'),({},[]))
get.assert_not_called()
self.assertEqual(sgdb.lookup('ビートセイバー'),({},[]))
with patch.object(sgdb,'api_key',return_value='test-key'), \
patch.object(sgdb,'_get',side_effect=AttributeError("'list' object has no attribute 'get'")):
self.assertEqual(sgdb.lookup('Game')[0],{})
def test_wrong_title_and_failed_lookup_fall_back(self):
with patch.object(sgdb,'api_key',return_value='test-key'),patch.object(sgdb,'_get',return_value=[{'id':1,'name':'Unrelated'}]):
self.assertEqual(sgdb.lookup('Game'),({},[]))