mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-06 01:00:18 +02:00
A host that answers 403/429 is left alone until its Retry-After (or GitHub's rate-limit reset; default 10 minutes). Meanwhile cached data is served, or the source reports 'limited' with its own name, e.g. 'GitHub is limiting requests; try again in 10 minutes'. Covers _web reads/downloads and F-Droid fetches. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
73 lines
3.1 KiB
Python
73 lines
3.1 KiB
Python
"""Free Android VR RSS listings. Download pages stay in the publisher's UI."""
|
|
import html, re, urllib.parse, xml.etree.ElementTree as ET
|
|
from email.utils import parsedate_to_datetime
|
|
|
|
from . import SourceError, _web
|
|
|
|
KIND = 'itch'
|
|
FEEDS = ('openxr', 'oculus-quest')
|
|
|
|
|
|
def sources():
|
|
return [{'id': KIND if tag == 'openxr' else 'itch-quest', 'kind': KIND,
|
|
'name': 'itch.io (' + tag + ')', 'url': 'https://itch.io', 'tag': tag,
|
|
'builtin': True, 'enabled': True, 'trust': 'community'} for tag in FEEDS]
|
|
|
|
|
|
def _parse(source, data):
|
|
try:
|
|
root = ET.fromstring(data)
|
|
except ET.ParseError as e:
|
|
raise SourceError('Invalid itch.io RSS feed') from e
|
|
entries = []
|
|
for item in root.findall('./channel/item'):
|
|
page = item.findtext('link') or ''
|
|
p = urllib.parse.urlsplit(page)
|
|
if p.scheme != 'https' or not (p.hostname or '').endswith('.itch.io') or p.username or ':' in p.netloc:
|
|
continue
|
|
if item.findtext('price') != '$0.00' or item.findtext('platforms/android') != 'yes':
|
|
continue
|
|
cover = item.findtext('imageurl') or None
|
|
if cover and not cover.startswith('https://img.itch.zone/'):
|
|
cover = None
|
|
updated = None
|
|
try:
|
|
updated = parsedate_to_datetime(item.findtext('updateDate')).date().isoformat()
|
|
except (ValueError, TypeError, AttributeError):
|
|
pass
|
|
entries.append({'source': source['id'], 'id': page, 'package': None,
|
|
'name': item.findtext('plainTitle') or item.findtext('title') or page,
|
|
'summary': html.unescape(re.sub('<[^>]*>', '', item.findtext('description') or '')).strip(),
|
|
'icon': cover, 'images': {'icon': cover, 'banner': cover, 'screenshots': []},
|
|
'page': page, 'version': None, 'version_code': None, 'min_sdk': None,
|
|
'abis': None, 'vr': True, 'size': None, 'free': True, 'license': None,
|
|
'updated': updated, 'downloadable': False})
|
|
return entries
|
|
|
|
|
|
def search(source, query, limit=50):
|
|
limit = max(0, min(int(limit), 100))
|
|
if not limit:
|
|
return []
|
|
entries = {}
|
|
tag = source.get('tag', 'openxr')
|
|
if tag not in FEEDS:
|
|
raise SourceError('Unsupported itch.io feed')
|
|
url = 'https://itch.io/games/free/platform-android/tag-' + tag + '.xml'
|
|
for entry in _parse(source, _web.read(url, ('itch.io',), name='itch.io')):
|
|
entries.setdefault(entry['id'], entry)
|
|
words = query.lower().split()
|
|
return [e for e in entries.values() if all(w in (e['name'] + ' ' + e['summary']).lower()
|
|
for w in words)][:limit]
|
|
|
|
|
|
def details(source, entry_id):
|
|
entry = next((e for e in search(source, '', 100) if e['id'] == entry_id), None)
|
|
if not entry:
|
|
raise SourceError('Game is not in the current free Android VR feed; open its publisher page')
|
|
return {**entry, 'versions': []}
|
|
|
|
|
|
def download(source, entry_id, version_code=None):
|
|
raise SourceError('Open the itch.io publisher page to download; automated download pages are disallowed by robots rules')
|