Three fixes. The pulse ring never showed. It was drawn on a 30dp canvas inside a box sized to the 20dp crest, so the parent's constraints shrank the canvas to 20dp and the crest image covered what was left. The box is now wider than the crest and the ring has room. "läuft" was drawn straight onto the row, and a focused row is filled with the accent colour — red on turquoise. The signal now sits on a dark chip, which reads on an unfocused row, a selected one and a focused one alike. The updater handed the installer whatever had been written so far. It now checks the response code, downloads to a .part file, compares the byte count against Content-Length before renaming, and refuses a second concurrent attempt instead of letting two writers share one file. A half-written APK leaves the installer spinning with nothing to report, which is what a hang looks like from the sofa. Crests are also resampled to 128px with a proper filter when fetched; Android's 8x downscale from the 330px originals left ragged edges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Downloads club crests into the APK's assets at build time.
|
|
|
|
The badges are trademarks: they are fetched into a generated, git-ignored
|
|
folder so the repository stays free of them while the app ships with them
|
|
and needs no network at runtime.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
OUT = sys.argv[1] if len(sys.argv) > 1 else "app/src/main/assets/crests"
|
|
URLS = "tools/crest-sources.json"
|
|
SRC = "app/src/main/java/dev/castarr/tv/data/TeamFilters.kt"
|
|
SUMMARY = "https://de.wikipedia.org/api/rest_v1/page/summary/"
|
|
UA = {"User-Agent": "Castarr build script (private use)"}
|
|
|
|
os.makedirs(OUT, exist_ok=True)
|
|
# Image URLs are plain text and safe to commit; the artwork itself is not.
|
|
sources = {}
|
|
if os.path.exists(URLS):
|
|
sources = json.load(open(URLS, encoding="utf-8"))
|
|
kotlin = open(SRC, encoding="utf-8").read()
|
|
entries = re.findall(r'club\((.*?)\)\s*,\s*(?://.*)?$', kotlin, re.M | re.S)
|
|
clubs = []
|
|
# Wikipedia serves ~330px thumbnails. The rail draws them at 20dp, which on a
|
|
# 1080p TV is 40px -- an 8x downscale that Android does with a cheap filter,
|
|
# and the edges come out ragged. Resampling once here with a proper filter
|
|
# fixes that and shrinks the APK.
|
|
CREST_PX = 128
|
|
|
|
|
|
def downscale(data: bytes) -> bytes:
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
return data
|
|
import io
|
|
|
|
image = Image.open(io.BytesIO(data)).convert("RGBA")
|
|
if max(image.size) <= CREST_PX:
|
|
return data
|
|
scale = CREST_PX / max(image.size)
|
|
size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
|
|
out = io.BytesIO()
|
|
image.resize(size, Image.LANCZOS).save(out, format="PNG", optimize=True)
|
|
return out.getvalue()
|
|
|
|
|
|
for raw in re.findall(r'club\(\s*"([^"]+)",\s*"[^"]*",\s*"([^"]+)"[^\n]*', kotlin):
|
|
clubs.append(raw)
|
|
# an explicit article = "..." wins over the club name
|
|
overrides = dict(re.findall(r'club\(\s*"([^"]+)"[^\n]*article = "([^"]+)"', kotlin))
|
|
clubs = [(k, overrides.get(k, n)) for k, n in clubs]
|
|
|
|
fetched = skipped = failed = 0
|
|
for key, full_name in clubs:
|
|
target = os.path.join(OUT, f"{key}.png")
|
|
if os.path.exists(target) and os.path.getsize(target) > 0:
|
|
skipped += 1
|
|
continue
|
|
try:
|
|
thumb = sources.get(key)
|
|
if not thumb:
|
|
url = SUMMARY + urllib.parse.quote(full_name)
|
|
with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=20) as r:
|
|
thumb = json.load(r).get("thumbnail", {}).get("source")
|
|
if not thumb:
|
|
raise ValueError("no thumbnail")
|
|
sources[key] = thumb.split("?")[0]
|
|
thumb = sources[key]
|
|
with urllib.request.urlopen(urllib.request.Request(thumb, headers=UA), timeout=20) as r:
|
|
data = r.read()
|
|
with open(target, "wb") as f:
|
|
f.write(downscale(data))
|
|
fetched += 1
|
|
except Exception as exc: # noqa: BLE001 - best effort, shield is the fallback
|
|
print(f" {key}: {exc}", file=sys.stderr)
|
|
failed += 1
|
|
# Wikipedia rate-limits bursts; this runs rarely and caches.
|
|
time.sleep(1.2)
|
|
|
|
with open(URLS, "w", encoding="utf-8") as f:
|
|
json.dump(dict(sorted(sources.items())), f, indent=2, ensure_ascii=False)
|
|
f.write("\n")
|
|
|
|
print(f"crests: {fetched} geladen, {skipped} vorhanden, {failed} fehlgeschlagen")
|