0.2.81
This commit is contained in:
@@ -809,6 +809,53 @@ at a time. Things to preserve:
|
||||
`HomeViewModel.favoriteChanges` so a Favourite press and its possible rollback reach the
|
||||
paged copy immediately. Its `type=Movie` / `type=Series` query is still enforced by the
|
||||
gateway and on the direct Emby path, so no category can mix the two grids.
|
||||
- **The rail is ordered by what this viewer watches, and the catalogue itself never
|
||||
changes.** `GET /v1/genres/affinity` (`server/internal/api/genre_affinity.go` →
|
||||
`store.TracearrGenreAffinity` → `ui/genre/GenrePersonalisation.kt`) is a weight per Emby
|
||||
genre label, and `personaliseGenreCategories` is what the television does with it. Things
|
||||
to preserve:
|
||||
- **The evidence is the server's and the catalogue is the set's.** Nothing is stored:
|
||||
Tracearr's sessions and the imported catalogue's genres are already in Postgres, so the
|
||||
reading is a join over two tables the gateway keeps for other reasons — the trade
|
||||
`watchedMsExpr` makes for watch time, and for the same reason (a per-user genre table
|
||||
would be a copy of both, wrong the moment either changed). The gateway sends Emby's own
|
||||
spellings verbatim and never a category: folding "Science Fiction", "Sci-Fi" and
|
||||
"Sci-Fi & Fantasy" into one shelf needs the alias table, which lives on the television,
|
||||
and a second copy of it on the server would disagree the first time a category gained a
|
||||
spelling — which is the failure the aliases exist to fix.
|
||||
- **The order is settled before the rail is drawn and never re-ordered under a viewer.**
|
||||
`EmbyRepository.genreAffinitySnapshot()` is a volatile read, the `snapshot` arrangement,
|
||||
consulted once in `GenreBrowseViewModel`'s constructor; `warmGenreAffinity()` is what
|
||||
fills it in, from `HomeViewModel`'s init, because the browser is never the first screen
|
||||
a television draws. A set whose reading has not landed gets the catalogue order and is
|
||||
personalised the next time the page is opened, which is the right way round: a rail that
|
||||
reshuffled under a D-pad already travelling it is worse than one that never personalised.
|
||||
- **Nothing is ever hidden, and the unpersonalised half stays in product order.** The sort
|
||||
is stable, so a category with no evidence scores zero and keeps its place below the ones
|
||||
the viewer watches. `MIN_GENRE_AFFINITY_SESSIONS` refuses to personalise at all below a
|
||||
floor — one Western on a wet Sunday is not a taste — and `GENRE_AFFINITY_FLOOR` is a
|
||||
*share* of the top genre rather than a count, so the long tail cannot reshuffle on noise
|
||||
and a household that watches nightly and one that watches on Sundays order the same way.
|
||||
- **An episode counts as its series, and an abandoned title counts as nothing.** The
|
||||
session joins on `emby_series_id` before `emby_item_id`, so a household watching one
|
||||
crime drama nightly reads as watching crime; `genreAffinityEngagement` requires half the
|
||||
title, because a session that stopped four minutes in is evidence somebody did *not*
|
||||
want it and counting those is how a rail comes to lead with what a viewer keeps
|
||||
abandoning. Recency is three flat bands over 180 days rather than a decay — a half-life
|
||||
nobody could defend would also make the answer move between two readings taken the same
|
||||
evening.
|
||||
- **Every failure is the default order.** The direct path has nobody to ask, a gateway
|
||||
predating the route answers 404, a household running no Tracearr has no history, and the
|
||||
operator's Tracearr switch is honoured even though the rows are already in Postgres —
|
||||
switched off means Memby may not read their viewing, and old rows are still their
|
||||
viewing. There is deliberately no second implementation on the direct path, the stance
|
||||
the TV calendar takes: the answer is Tracearr's, which a television holds no credential
|
||||
for, and the catalogue's own order is a perfectly good rail.
|
||||
- **Six hours on both sides.** `genreAffinityTTL` and `GENRE_AFFINITY_REFRESH_MS` are
|
||||
matched so a set refetching past it asks a question that has actually been recomputed
|
||||
rather than paying a round trip for a cached repeat; an empty reading is kept for an
|
||||
hour instead, because a new account genuinely acquires a history and a six-hour no would
|
||||
hide their first evening of watching until the morning.
|
||||
- **The gateway path degrades to a keyword search on the *first* page only**, so a set on a
|
||||
new build talking to a gateway that predates the route still shows something. A later page
|
||||
does not: a gateway that answered page one and failed on page two is having trouble, not
|
||||
@@ -2513,6 +2560,40 @@ frame. Don't construct a bare `OkHttpClient.Builder()` — derive from `HttpStac
|
||||
stream's derived client raises the read timeout and sets **no call timeout**, which would
|
||||
cap the length of a film.
|
||||
|
||||
**A logo is fetched once and judged once.** `ui/TitleLogo.kt` holds both halves, and both
|
||||
were being repeated on every screen that drew a title treatment — visible to a viewer as a
|
||||
detail page returned to a second time showing the show's *name* and then swapping to its
|
||||
logo. Things to preserve:
|
||||
|
||||
- **The darkness probe must not use Coil's memory cache.** That cache is keyed on the
|
||||
image's URL and **not** on the size it was decoded at, so the 64×64 inspection copy was
|
||||
written over the full-size bitmap the launcher had just cached: every page that probed a
|
||||
logo threw away the copy the previous page had paid for, and the next display request
|
||||
decoded it again. `probeLogoDarkness` sets `memoryCachePolicy(CachePolicy.DISABLED)` and
|
||||
leaves the *disk* cache on deliberately — the bytes are worth keeping, and they are what
|
||||
the display request reads instead of the network.
|
||||
- **The verdict is remembered, and `useTextTitleForLogo` reads it as its initial value.**
|
||||
`produceState` starting from "use the text title" is the right default the first time a
|
||||
title is seen and a visible flash every time after it, since the answer cannot change.
|
||||
`TitleLogoCache` is process-scoped, access-ordered and capped; concurrent asks for one
|
||||
logo share a single probe on the cache's *own* scope, because the callers are a detail
|
||||
page's `produceState` and a focus warm and both die when the D-pad moves.
|
||||
- **"Could not tell" is not remembered.** A failed or cancelled probe returns null and is
|
||||
not recorded — a hiccup must not condemn a title to its text heading for the life of the
|
||||
process. `TitleLogoCacheTest` pins that, the single flight and the eviction.
|
||||
- **One width, `data/LOGO_MAX_WIDTH` (720).** It was four — 800 in the screensaver, 720 on
|
||||
the detail page and in the player, 640 on the home hero, 420 in the TV calendar — and a
|
||||
width is part of the URL, so one show's logo was four cache keys, four fetches and four
|
||||
verdicts. Ask for the default; a new surface that names its own width quietly opts out of
|
||||
every cache the others share.
|
||||
- **Invalidation is free and must stay that way.** An Emby logo URL carries the item id
|
||||
*and* the image tag, so artwork replaced on the server is a different key in the verdict
|
||||
cache and in both of Coil's. Nothing is told about it and nothing needs to be.
|
||||
- **It is warmed on focus** (`HomeViewModel.warmDetailPage`), which is where the bytes and
|
||||
the verdict both come from by the time a press composes the page. Skipped outright when
|
||||
the viewer has logos switched off, and `TitleLogoCache.install` in `MembyApp` is what
|
||||
gives that warm an application context rather than making the ViewModel hold one.
|
||||
|
||||
## Language
|
||||
|
||||
**Everything a person reads is New Zealand English.** No American spellings: `-ise`/
|
||||
@@ -2939,6 +3020,41 @@ returns nothing rather than throwing. Things to preserve:
|
||||
constantly behind it, which is why one slot must always yield the same draw.
|
||||
The rotation further down belongs to the direct path, which has no merit to rank by.
|
||||
|
||||
**An operator's hero change is live, and it rides the status poll to get there.**
|
||||
`heroRevision` (`server/internal/api/hero_revision.go`) is the `themeRevision` shape — a
|
||||
hash rather than a counter, because what a hero resolves to changes when nobody has
|
||||
written anything: a schedule window opens, the rotation slot turns over. `/v1/status`
|
||||
carries it as `hero: {revision}`, `MaintenanceMonitor.heroRevision` publishes it, and the
|
||||
launcher refetches `/v1/heroes/active` when it moves. Before it, a pin reached a set only
|
||||
at its next cold start: the section heroes were fetched once per session and never again,
|
||||
and Home's hero arrives with rows the launcher does not re-request while somebody is
|
||||
sitting on them. Things to preserve:
|
||||
|
||||
- **The revision hashes what would be *resolved*, not the stored document.** `UpdatedAt`
|
||||
moves on every write, so hashing the row would repaint every launcher in the house for a
|
||||
save that changed nothing. For the same reason a schedule added for tomorrow evening does
|
||||
not move it — only the schedules `activeHeroScheduleIDs` says are in force now do, which
|
||||
is also what makes a window opening at 20:00 arrive by itself.
|
||||
- **It is the same value in the cache key as on the poll**, in `handleHome` (`:hr`) and in
|
||||
`hero:active:v2:`. That is what leaves no state in which the poll says something moved and
|
||||
the fetch that follows returns what the set already had — and it replaced the cache
|
||||
*sweep*: saving a hero used to call `InvalidateUser` for every account in the house,
|
||||
throwing away every cached item lookup, row and image the household held in order to
|
||||
change four cards. Nothing is invalidated now; the entries built under the old policy are
|
||||
simply no longer named, and age out on their own TTL.
|
||||
- **The policy is read through `currentHeroPolicy`**, never `store.HeroPolicy` directly.
|
||||
The status poll is every open television every ten seconds, so this document needed the
|
||||
`featurePolicyCache` treatment; the admin save invalidates that copy, which is the only
|
||||
thing that write now does besides writing.
|
||||
- **Home is not refetched at launch, only when the revision has moved under a set already
|
||||
looking at it.** Its hero arrives with the home payload, and fetching it separately on
|
||||
every launch would replace a hero already on screen with a second composition of the same
|
||||
thing. The refetch replaces one placement at a time and never clears first, so unchanged
|
||||
cards keep the artwork already decoded for them and nothing blanks between the two states.
|
||||
- **The revision is collected inside the effect, not read as state in the launcher's body.**
|
||||
A number arriving on a poll must never be a reason the launcher recomposes — the same rule
|
||||
the animation and flow-collection conventions state.
|
||||
|
||||
**The reason sits above the ratings strip**, and that order is load-bearing. The featured
|
||||
card's text column is what gives way when a title wraps onto two lines, so whatever is last
|
||||
in it is cut — with the reason below the strip, the one line explaining why this card leads
|
||||
|
||||
Reference in New Issue
Block a user