Lumora v6.4
Catalogue
- A small playlist never goes through the database at all. A catalogue whose enabled playlists measure 40 MB or less now stays resident: the import collects the rows in memory instead of writing them through the fifteen-index catalogue table and reading them back, and the flat
channels_cache.txtis the persistence. That copy is read on the next cold start, so the catalogue paints from RAM without a SQL query and without waiting for a provider. The 40 MB decision is re-made from the sizes this load actually measured, so adding/removing a provider moves the setup between RAM and Room on the very next load instead of a restart later; a build that cannot fit (or an out-of-memory on a TV stick) falls back to Room for the rest of the process and re-imports once, and anything over the cutoff keeps using Room exactly as before. The cache also carries the fields added since it was last written - season, IMDb rating, plugin token and plugin id - so media-server episodes and anime streams survive a cold start instead of coming back blank. - Switching a source off no longer leaves its rows in someone else's cap. A one-provider load (a plugin re-add, a provider switched back on) that has to fall back to Room merges the refreshed rows into the resident list instead of replacing it, so the providers that live only in RAM are still there afterwards.
Settings
- The rail groups its 18 sections by task, and stays readable translated. Your library, Browsing, Connections and System headings replace one long flat list, the rail is 168dp wide so "Recording Storage" and "Backup & Restore" stop being cut to an ellipsis, and the collapse control is an icon-only button rather than a labelled pill that squeezed the localised "Settings" title out of its row. Rows are 48dp on a phone (Android's touch floor) and 36dp on a TV, focus rings stay inside the rail, and group headings are accessibility headings.
- Settings opens where the user actually is. A configured install lands on General (the whole-app switches, then Language) instead of starting on Providers, and a fresh install still lands on Providers where adding one is the only useful action.
- Recording Storage is its own pane, and a NAS can be set up without a phone. Connect NAS (QR) and Add NAS manually are now real rows with captions instead of being hidden inside the destination picker; the manual dialog takes SMB/WebDAV type, name, address, credentials and recording folder on the TV remote, saves the share, points recordings at it and verifies it the same way the QR path does. The rail's section picker also no longer relies on hardcoded indices (the plugin row opened section 7 by number), so a section can be added without silently renumbering the one below it.
- The classic-layout toggle keeps the Regions selection. Re-deriving live through
deriveLiveHalfinstead of dropping back to the raw list means the region filter and its untagged/per-provider fallbacks survive the toggle, andENis no longer treated as a United Kingdom alias - it is a language tag, not England.
Popups
- Every dialog is Lumora's own. The platform
AlertDialogwas whatever the ROM shipped - on Fire OS a message silently dropped the choice list, buttons ignored the app's colours and type, and the panel looked foreign on every device. All 83 popup call sites now go throughcom.lumora.ui.dialog.LumoraDialogand its builder: the app's glass panel, its focus ring, its D-pad rules and its own button row, with single-choice, multi-choice, menu and form layouts built fromdialog_lumora.xml. The toolbar refresh is a real picker now (every source selected by default, a master "All providers" row, an empty selection kept open with a message), and a Gradle task (verifyNoPlatformDialogs) fails the build if a platformAlertDialogimport comes back. - Dialogs fit the device. Width is 92% of the window capped at 680dp, so a phone gets a usable column instead of a 245dp strip while a TV stays a dialog; the choice list reserves room for the title, message and buttons (a landscape phone no longer pushes its actions off the bottom edge), and the plugin stream-search and scraper source pickers finally have a result list with height - their scrolled rows were a weighted
0dpchild of awrap_contentdialog, which measures to exactly zero.
Search
- Arabic titles are searchable however they are spelled. Both the catalogue's search key and the typed query now fold the common Arabic variants before matching:
أ,إ,آandٱall read asا; Tashkeel (harakat, tanween, shadda, sukun, Quranic marks), Tatweel/Kashida and invisible joiners are ignored; Alef Maqsura reads as Yeh; Latin case and repeated whitespace are folded too. Titles stay exactly as the provider spelled them on screen - only the matching key changes - and the same fold covers live channels, films, series, EPG results and the Discover title fallback. The key is stored on the catalogue row (searchName) so a 300k-item panel is not re-normalising every title on every keystroke; rows written by an older build are backfilled in the background, and keep matching by their original spelling until their turn comes.
Providers
- A rate-limited panel is left alone instead of asked again. Every request the app makes to one host now shares a small brake: starts are spaced, and a 429/503 parks that host for the server's
Retry-After(or a growing default) so the next guide row, poster tile or catalogue refresh fails fast instead of adding to the pile. The shared client also runs at most three calls per host at once, which is what turns a screenful of artwork and guide fetches into the burst panels answer with HTTP 429 in the first place. A guide channel also stops trying every one of its quality/source variants, and a channel whose guide could not be fetched is now rested with the same failure backoff the guide and menu already use.
Performance
- The storage breakdown no longer runs on the way to the first frame. The diagnostic walk over the app's data directory, cache, databases and external volumes was synchronous in
onCreate; on a package with hundreds of megabytes it delayed the first screen and made the launch look frozen. It stays a diagnostic line, but on the IO dispatcher it can no longer sit between the user and the catalogue. The search-key backfill runs in its own coroutine for the same reason, so it cannot hold the update-installer sweep behind a six-figure row pass. - A recording no longer occupies a metadata request slot. OkHttp keeps a dispatcher slot for the whole life of a call, body included, and the shared client allows three calls per host. A recording stays open for the length of the programme, so three simultaneous recordings from one provider would have filled that budget and queued every guide/artwork/catalogue request behind them. Capture now uses its own dispatcher and stays out of the shared one.
Android 7.1
- API 25 is clean.
lintDebugreports zeroNewApifindings for the new work: the settings headings use the API-28accessibilityHeadingattribute only where the platform understands it (marked withtools:targetApi, ignored below 28), and every other addition - the search fold, the request pacer, the dialogs, the resident catalogue, the Room v2 migration - uses APIs available on Android 7.1. The catalogue migration addssearchNamewith a default in oneALTER TABLE, so the upgrade opens without a full-table Kotlin pass.
Installation
Download the APK below and install on your Android TV, Fire TV Stick, or phone.