feat(details): synopsis and cast on long press, and search by cast - #418
Closed
Pierroons wants to merge 5 commits into
Closed
feat(details): synopsis and cast on long press, and search by cast#418Pierroons wants to merge 5 commits into
Pierroons wants to merge 5 commits into
Conversation
"prenom" returned nothing while "prénom" found "Le Prénom". SQLite's LIKE ignores case for ASCII but never diacritics, so 4 882 of the 40 971 titles in a French catalogue — 12 % — were unreachable to anyone who did not type the accent, which on a TV remote is most of the time. Titles are now stored a second time in a folded form (NFD, combining marks dropped, lowercased with Locale.ROOT) and queries are folded the same way before they reach the DAO. The comparison stays symmetric: accented or not, either side matches. normalizeForSearch is the single definition of that fold. The migration backfills existing rows through it in Kotlin rather than through a stack of SQL REPLACE calls, so the two can never drift — and a drift here is silent, rows simply stop matching. titleNormalized sits outside the constructor on purpose: a data class only copies constructor parameters, so copy(title = …) recomputes it instead of carrying a stale value that would drop the channel out of every search. Folding happens in the repositories rather than at each call site, so every screen searching channels or categories behaves the same way. No index on the column: '%query%' cannot use one, and it would only slow the bulk inserts a resubscription performs on tens of thousands of rows. Verified on a 40 971-row database: migration to v27 completed at startup, every row backfilled, and "prenom" and "AMELIE" now return the accented titles on screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Long-pressing a channel opened a sheet offering Favourite, Hide and Create shortcut, and said nothing about the title itself. Other clients show a synopsis and a cast there. Xtream keeps that apart from the catalogue: get_vod_streams returns names and artwork only, so each description costs a get_vod_info call — which is why it is fetched when the sheet opens rather than up front. Series answer the same shape on get_series_info. Tapping a channel still plays it immediately. The description sits behind a long press so nothing is added between a viewer and their film. Details are cached in channel_details, keyed on (playlist, channel reference) rather than on the channel row id: ids are regenerated on every re-subscription, which is the only way to refresh a catalogue, so keying on them would discard the cache each time. Requests are serialised behind a mutex. These accounts commonly allow a single connection and it is shared with playback — several descriptions fetched at once can cost the viewer their stream. A row is written even when the panel has nothing to say, so "asked, has no description" stays distinguishable from "never asked" and the sheet does not re-ask on every open. The call is issued from this module rather than from the parser submodule, which models the episode list of get_series_info but not the descriptive block. Only its URL builder is reused, so this stays a single-repository change. Panels disagree on their own field types — rating arrives as 5, "5" and 7.4, tmdb_id alternates just as freely, movies say releasedate where series say releaseDate. A lenient reader keeps a sheet from being lost over a field nobody reads as a number; five tests pin those shapes down. Verified on device: long press on a film shows 2017 · Action, Science-Fiction, Thriller ★ 5.6, its cast, its director and its synopsis, and the row lands in the cache with the folded cast ready for search. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Searching an actor returned nothing: cast lists were fetched for the detail
sheet but never looked at by search.
The query now left-joins channel_details, so a catalogue whose descriptions
have not been collected yet searches exactly as before — the actor half
simply matches nothing until they arrive.
Results carry why they matched. A search for "Gyllenhaal" returns films
whose titles share nothing with the query, and a bare list of them reads as
a bug, so the matching cast is shown under the title.
ChannelDetailsWorker collects the rest of the catalogue in the background.
Two things shape it:
- These accounts commonly allow a single connection, shared with
playback. The sweep steps aside the moment something starts playing and
paces itself between requests rather than going as fast as the panel
allows. Collecting metadata should not cost a viewer their stream.
- Progress lives in the database, not in the worker: each batch asks for
channels that still have no description, so an interrupted run resumes
by asking again. Nothing to persist, nothing to lose — which matters
when twenty-eight thousand titles are spread over several nights.
It starts after a subscription completes, when the catalogue is freshly in
and the descriptions behind it are missing.
Work names are hashed rather than raw: a playlist URL carries the account
credentials in its query string and work names surface in diagnostics.
Verified on device with 400 titles collected: "gyllenhaal" returns three
films — Spirit, Night Call, In the Grey — none of which carry the name in
their title, each showing the cast that matched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-subscribing is the only way to refresh an Xtream catalogue, and it writes the playlist row back with INSERT OR REPLACE. SQLite performs a REPLACE as a delete followed by an insert, so the ON DELETE CASCADE on channel_details fired on every refresh. Measured on a real database: 401 rows before, 0 after — and each row costs one network request to earn back, so the table emptied itself precisely when it was most expensive to refill. The foreign key is gone; rows are deleted explicitly where a playlist is genuinely unsubscribed, next to where its channels are. Migration 28 to 29 rebuilds the table without the constraint, keeping what is already cached. The sweep is now one worker for all playlists instead of one per playlist. Two of them running side by side halved the pause between requests — measured at some 247 titles a minute against the 130 intended. The mutex still kept requests strictly serialised, so the single-connection limit was never breached, but the pacing was not what it claimed to be. It is also periodic rather than a single long run. The platform stops any worker after about ten minutes, so one run was never going to walk twenty-eight thousand titles; and since playback deliberately interrupts the sweep, without something bringing it back an evening of watching would leave it stopped for good. The existing migration tests were fixed too: they only supplied migrations up to 26, so raising the schema silently broke all six. They now cover the whole path, and a new test pins down the behaviour this commit is about — cached rows surviving the INSERT OR REPLACE a refresh performs. Verified on device: schema at 29, cache intact through the migration, one worker, 130 titles a minute, and a search for "judor" returning Roulez jeunesse, Les Nouvelles Aventures d'Aladin, H and Platane. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
data/.gitignore excluded /src/test, so the module could not carry unit tests at all. The entry is dropped and five come with it. They cover what panels actually send: a rating as a bare number, tmdb_id alternating between number and string, movies spelling releasedate where series spell releaseDate, cast falling back to actors, plot to description, and unknown fields appearing without warning. Any of those refused outright would cost the sheet its content. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
this PR changes too much codes 😇 |
Contributor
Author
|
Fair, and I should have split it before opening it. Let me close this and send it in pieces:
Much of the bulk is generated Room schema JSON rather than hand-written code, but that doesn't make it easier to review. I'll open the first once #417 is settled, since it builds on it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Long-pressing a channel opens a sheet offering Favourite, Hide and Create
shortcut, and says nothing about the title itself. Other clients show a
synopsis and a cast there.
Where the data comes from
Xtream keeps it apart from the catalogue:
get_vod_streamsreturns names andartwork only, so each description costs its own
get_vod_infocall — measuredat ~0.21 s. Series answer the same shape on
get_series_info.Tapping a channel still plays it immediately. The description sits behind a
long press, so nothing is added between a viewer and their film.
Caching, and what it must survive
Details are cached in
channel_details, keyed on(playlist_url, relation_id)rather than on the channel row id: ids areregenerated on every re-subscription, which is the only way to refresh a
catalogue, so keying on them would discard the cache each time.
That table deliberately carries no foreign key onto
playlists. It hadone, and re-subscribing writes the playlist back with
INSERT OR REPLACE—which SQLite performs as a delete followed by an insert, so
ON DELETE CASCADEemptied the whole table on every refresh. Measured on a real database: 401 rows
before, 0 after, each costing a request to earn back. Rows are now deleted
explicitly where a playlist is genuinely unsubscribed.
Requests are serialised behind a mutex. These accounts commonly allow a single
connection and it is shared with playback — several descriptions fetched at
once can cost the viewer their stream.
A row is written even when the panel has nothing to say, so "asked, has no
description" stays distinguishable from "never asked".
Searching by cast
The query left-joins
channel_details, so a catalogue whose descriptions havenot been collected yet searches exactly as before. Results carry why they
matched: a search for an actor returns films whose titles share nothing with
the query, and a bare list of them reads as a bug, so the matching cast is
shown under the title.
ChannelDetailsWorkercollects the rest in the background — one worker for allplaylists, paced between requests, stepping aside the moment something starts
playing, and periodic because the platform stops any worker after about ten
minutes. Progress lives in the database: each batch asks for channels that
still have no description, so an interrupted run resumes by asking again.
The parser submodule is untouched; only its URL builder is reused, so this
stays a single-repository change.
Verification
On device against a 22 822 film / 5 332 series catalogue, collected in full:
long press shows year, genre, rating, cast, director and synopsis; a search for
an actor returns their films with the matching cast shown; and the cache
survives a full refresh intact.