The club chooser was a flat list of 56 names in a generic picker
dialog, which is unreadable from a couch. It now has its own dialog
with a crest per row, the short label on the right, and section
headers per division. Enabled clubs in the settings card show their
crest too.
A search field would have been the obvious alternative, but text
entry on a remote is exactly what the Ten-Foot rule rules out, so
grouping plus crests carries the recognition instead.
Also adds tests/demo, a generator for neutral playlist and EPG data
so screenshots never carry real channel names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolving crests from Wikipedia at runtime never produced a visible badge
on the TV and left nothing in the log to debug, so the shield fallback was
all anyone ever saw. They are now downloaded once at build time by
tools/fetch-crests.py into assets/crests (git-ignored, so no trademarked
artwork is committed) and loaded from there — instant, and independent of
the network.
CI: accepting SDK licences by piping "yes" into sdkmanager died of SIGPIPE
(exit 141) under pipefail; the licence hashes are written directly now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Control server security and robustness:
- Socket read timeout, handshake deadline and a client cap so an idle or
hostile connection can no longer pin threads forever (#1)
- Per-address rate limiting that counts every failed hello, Origin
checking on the upgrade, and a separate revocable session token for
code-authenticated clients so the guessable path no longer yields the
QR credential (#2)
- Credentials excluded from cloud backup and device transfer, constant
time comparisons, and a pairing reset in the settings (#3)
- Playlist fetches restricted to http(s), capped at 24 MB and bounded by
an overall transfer deadline (#4)
- Port conflicts and MediaSession id collisions no longer crash the app;
the remote degrades to unavailable with a plain-language note (#11)
Player:
- Seeking no longer collapses to position 0 when the duration is unknown
(#5)
- Pause acts on playWhenReady, so pausing during a stall works and
playback cannot resume in the background after leaving the app (#6)
- Playback failures stay on screen with a retry action instead of
silently dropping back to the list (#7)
- Reconnects are spaced 1s/3s/8s and re-entering the channel just closed
waits out a short grace period, which is what the provider needs to
release the previous session (#12)
Channel list and remote:
- Leaving playback returns to the channel the viewer came from (#13)
- The remote only rebuilds its list when the data changed, never
overwrites a focused input and carries indices instead of scanning (#8)
- Pairing retry reconnects properly, resets its backoff and validates the
code before spending an attempt (#9)
- M3U parsing keeps commas in names, strips a BOM and rejects payloads
that are not playlists, covered by unit tests under tests/ (#10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Imported the native TV app (Kotlin, ExoPlayer, embedded remote server, QR
pairing) plus CONTEXT.md and ADRs 0001-0005. Rename, Compose for TV UI and
the Dispatcharr integration follow as tracked issues.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>