0.2.76 - Icon Packs
This commit is contained in:
@@ -315,6 +315,56 @@ household, so watched/favourite/resume state must never be cached there and stil
|
||||
from Emby live. A full import mark-and-sweeps on `synced_at`; incremental uses
|
||||
`MinDateLastSaved` with a minute of overlap.
|
||||
|
||||
**But asking Emby is no longer how the gateway finds out.** Sonarr and Radarr are the
|
||||
things that put files on disk, so they are what the catalogue learns from: both post to
|
||||
`/hooks/sonarr` and `/hooks/radarr`, `internal/library/events.go` turns a notification into
|
||||
a piece of work, and `ingest.go`'s single worker reads that one title out of Emby a minute
|
||||
later. An episode imported at 19:05 is searchable at 19:06 rather than as late as 20:00,
|
||||
and the hourly sweep becomes reconciliation for what the *arrs do not manage — a file
|
||||
dropped in by hand, a title edited in Emby, a webhook that arrived while the container was
|
||||
down. Things to preserve:
|
||||
|
||||
- **`library_ingest_queue` is durable, and it is the only queue in the schema that is.** A
|
||||
Tracearr-derived credits candidate is rebuilt from one query on restart; "Sonarr imported
|
||||
this at 19:05" cannot be rederived from anything, so a container restarted during the
|
||||
settle delay must still read the file. The key names the **file** rather than the
|
||||
delivery, which is what makes `ON CONFLICT` the whole of the repeat-delivery defence —
|
||||
both *arrs re-notify on retry — while a file deleted and re-imported is a different file
|
||||
and its own work.
|
||||
- **Both hooks sit outside the quiet-time gate**, which the Radarr one previously sat
|
||||
inside. That gate answers 503 and neither *arr re-delivers, so a quiet hour silently
|
||||
discarded every import that happened during it. The hook records at any hour and the
|
||||
*worker* is where quiet time is honoured — which is only possible because the queue is
|
||||
durable.
|
||||
- **An upgrade is silent as news and still refreshes the row.** Those are two judgements
|
||||
made in two places: `AnnounceLibraryIngest` refuses to announce anything that is not
|
||||
`ReasonImport` — the title was already there — and the catalogue re-reads it because the
|
||||
file genuinely changed. A rename is a
|
||||
refresh and never an invalidation — the Emby item id survives a move, and so does the
|
||||
credits marker measured against it. A delete only counts when the media went with it: a
|
||||
series unfollowed in Sonarr with its files left on disk is still in the library.
|
||||
- **Emby not having scanned yet is the expected first answer**, not a fault. One
|
||||
`RefreshItem` nudge at the parent, then a widening backoff (`IngestRetryDelay`) out to an
|
||||
attempt limit — because past the last step the cause is not timing, and a row retrying for
|
||||
ever is one nobody looks at.
|
||||
- **The lookup asks for `syncFields`**, the scheduled import's own set, for the reason
|
||||
`Syncer.Find` does: a thinner query leaves an event-imported title without People,
|
||||
MediaStreams or ProviderIds — no cast, no ratings lookup, no format badges — until Emby
|
||||
next reports it changed, which for a film nobody edits again is never.
|
||||
- **A refresh resolves through Emby; a delete resolves through the catalogue.** That
|
||||
asymmetry is deliberate. The local series index answers for every show ever imported and
|
||||
Emby is asked only when it misses, which is exactly the case the feature exists for — a
|
||||
brand-new show whose first episode has just landed, whose series row is then written
|
||||
beside its episode. A delete is the other way round because the file is gone and Emby is
|
||||
the least likely thing to still be able to name it.
|
||||
- **The sweep interval is an operator override** (`librarySyncMinutes`, the
|
||||
`embyHealthInterval` pattern) and `Syncer.Schedule` takes a *function* rather than a
|
||||
value, because a setting read once at start-up is not a setting: lengthening the sweep to
|
||||
6h after wiring the webhooks up must not need a restart.
|
||||
- **`store.DeleteLibraryItem` takes the credits marker with the row.** `credits_markers` is
|
||||
keyed on the item id and nothing else prunes it, so a deleted title would otherwise leave
|
||||
a Skip Credits position behind for a file that no longer exists.
|
||||
|
||||
**External ratings** (MDBList) are bought by the day, not by the request, so the design
|
||||
question is never "how fast can we fetch" but "how few times must we ever ask". The answer
|
||||
is that a title is fetched once and kept: `external_media_ratings` holds the raw provider
|
||||
@@ -365,12 +415,41 @@ window closes — a list rather than a push because the gateway holds no connect
|
||||
television, and a window is what lets a set that was off or in the screensaver at the time
|
||||
still hear the news. Four publishers today:
|
||||
|
||||
- `api/radarr_alerts.go` — a film Radarr just imported. `POST /hooks/radarr` is the "On
|
||||
Import" webhook and the one thing that pushes *into* the gateway, guarded by
|
||||
`MEMBY_RADARR_WEBHOOK_TOKEN` (unset ⇒ 404, the stance `/admin` takes) and mounted
|
||||
outside both the auth middleware and the maintenance gate, because an event dropped
|
||||
during maintenance is lost rather than delayed. A quality upgrade is deliberately
|
||||
silent: the film was already there.
|
||||
- `AnnounceLibraryIngest` in `api/ingest_alerts.go` — a film or an episode whose scan has
|
||||
**finished**. `POST /hooks/radarr` and `POST /hooks/sonarr` are what push *into* the
|
||||
gateway, guarded by `MEMBY_RADARR_WEBHOOK_TOKEN` / `MEMBY_SONARR_WEBHOOK_TOKEN` (unset
|
||||
⇒ 404, the stance `/admin` takes) and mounted outside both the auth middleware and the
|
||||
maintenance gate, because an event dropped during maintenance is lost rather than
|
||||
delayed — but the *hook* no longer announces anything. It records, and the announcement
|
||||
is hung off `Ingester.Announce` in `main.go`, the `SetAfterSync` arrangement, so
|
||||
`library` stays ignorant of what an alert is. Things to preserve:
|
||||
- **The webhook is not the news.** Both *arrs fire the moment they have moved a file and
|
||||
Emby has not scanned it in yet, which is why the banner published from the hook could
|
||||
only ever promise a film would be available "shortly" and why an episode could not be
|
||||
announced at all — there was nothing true to say about one until it was there. Behind
|
||||
the scan the banner says the title is **ready to watch**, and a title Emby never
|
||||
manages to scan is never announced, which is the right way round.
|
||||
- **A quality upgrade is still silent, and so are a rename and a delete.** Only
|
||||
`ReasonImport` is news; the file genuinely changed, so the row is still re-read. That
|
||||
judgement lives in `api` rather than in the worker: the worker's business is that the
|
||||
row moved, this is the separate question of whether anybody should be told.
|
||||
- **A season pack is one banner.** `ingestRuns` tallies a season's arrivals within
|
||||
`ingestRunWindow` and every later one replaces the same alert, because the id is
|
||||
anchored on the run's **first** episode. The anchor is what makes both halves work:
|
||||
within the window a burst collapses, and next week's episode — arriving after it has
|
||||
closed — starts a run of its own rather than reusing an id every television in the
|
||||
house has already dismissed as seen. Bounded and lossy in memory, the `playbackTitles`
|
||||
arrangement; a gateway restarted mid-pack announces the rest as a second run.
|
||||
- **One arrival is named, several are counted.** Naming the last of six would be
|
||||
arbitrary — nothing makes it the one worth mentioning — where the count is what the
|
||||
viewer wants. `episodeSummary` drops the episode title when Emby has recorded it as
|
||||
the show's own name, since "S03E05 — The Bear" reads as a mistake.
|
||||
- **Emby's names outrank the *arr's.** The result carries what was actually written to
|
||||
the catalogue, so the banner and the card underneath it cannot name one thing two ways.
|
||||
- **The two windows still switch their own half off**: films answer to
|
||||
`MEMBY_RADARR_ALERT_WINDOW`, episodes to `MEMBY_SONARR_ALERT_WINDOW`. `sonarr-import`
|
||||
is its own kind, distinct from `sonarr-aired` — one says an episode has been broadcast
|
||||
and is *not* here, the other that it is.
|
||||
- `AnnounceLibrarySync` in `api/server_alerts.go`, hung off `syncer.SetAfterSync` in
|
||||
`main.go` — "24 titles added or updated". Only a run that *changed* something is
|
||||
announced; the import is scheduled, most passes find nothing, and an hourly "no news"
|
||||
@@ -2199,6 +2278,56 @@ which is the same as the feature not existing. The only switch is the operator's
|
||||
from its own clock that it is Halloween, while the household's gateway has seasons switched
|
||||
off, would be the feature failing rather than degrading. `data/Themes.kt` is only hex
|
||||
parsing, and it refuses anything it cannot read so the app's own token stands in.
|
||||
**Icons are the server's answer too.** `ui/theme/MembyIcons.kt` is `DesignTokens.kt` for
|
||||
marks: an enum of ~70 **slots** named for what they mean (`Search`, `Drama`, `Sparkle`), one
|
||||
process-wide `mutableStateOf(MembyIconPack)`, and `applyMembyIconPack` to repaint. Before it
|
||||
the app held seventy literal `Icons.Default.*` across nineteen files, which put icons exactly
|
||||
where the palette was before its own work: unreachable from the gateway, because the server
|
||||
can only change what the television has a slot for. The packs are
|
||||
`ui/theme/MembyIconPack*.kt` — Material (what the app shipped with), Lucide and Font Awesome
|
||||
Solid, from `com.composables:icons-*-cmp` — and the gateway names one with `iconSet` on the
|
||||
theme document. Things to preserve:
|
||||
|
||||
- **A slot is named for the job, never for the mark that fills it today.** Filing the
|
||||
recommendation slot under `AutoAwesome` would describe Material's four stars, and a pack
|
||||
whose answer is a wand would then sit under a name that lies about it.
|
||||
- **Nothing may hold a resolved mark.** `MembyIcon.mark` reads process-wide state, so a mark
|
||||
captured in an `enum` constant or a top-level `val` freezes whichever pack was loaded when
|
||||
that class initialised — the same `val`-versus-`get()` trap that made `SettingsSheet` the
|
||||
one screen a palette could never reach. `BrowseDestination`, `SettingsPage` and
|
||||
`RequestCardAction` therefore carry the **slot** and resolve it where they draw.
|
||||
- **The marks are lambdas, not vectors.** An `ImageVector` is built when it is first read, so
|
||||
a map of them would build all seventy on the first frame that touched a pack — on the cold
|
||||
start, which is the one thing in this app nothing may cost.
|
||||
- **A pack may be partial, and an absent slot falls back to Material.** Lucide is stroke-only
|
||||
and has no filled heart, so mapping `Favourite` and `FavouriteOutline` to one glyph would
|
||||
make "this is a favourite" and "this is not" identical on screen — a pack must never cost
|
||||
the app a distinction. Font Awesome Solid declines the outline halves for the same reason
|
||||
from the other side. The mixture is small and is the honest answer.
|
||||
- **The wire carries a slug and never geometry**, the line the palette already draws: a
|
||||
gateway that could send paths could draw an unreadable rail, where the worst a pack slug
|
||||
does is look unchanged. An unknown slug resolves to Material at *both* ends —
|
||||
`membyIconPackFor` on the television and `knownIconPack` on the gateway, which refuses to
|
||||
echo a pack nothing can draw.
|
||||
- **It rides the theme revision**, so it costs no new field on the status poll and no second
|
||||
sync loop: `ThemeSync` already refetches on a revision it does not hold. The slug is cached
|
||||
beside the palette and applied *before* any request, and on its own evidence — a set whose
|
||||
stored palette will not parse still opens wearing the marks it was told to wear.
|
||||
- **Where it is chosen is the `iconSet` preference**, beside `themeId`, so an operator sets it
|
||||
per viewer from the console's existing catalogue-driven editor and no admin page was needed.
|
||||
A **season may replace the marks; a selectable theme may not** (`IconSet` on
|
||||
`themeDefinition`, the `Decoration` shape) — a season is a look, where a scheme somebody
|
||||
picked to live with all year taking their marks away leaves no way to tell which of the two
|
||||
choices did it.
|
||||
- **R8 is what makes three packs affordable.** Only the slots named in the maps survive out of
|
||||
packs holding a thousand icons each: measured, two complete packs cost **+16 KB** on the
|
||||
release APK. A slot nothing draws costs three vectors for nothing.
|
||||
- **`IconPackScreenshotTest` is the only test that can judge this** (`build/screenshots/
|
||||
icon-packs/`), the point `ThemeScreenshotTest` makes about palettes. A unit test can check
|
||||
that a pack names a mark for a slot; it cannot check whether that mark *means* the slot —
|
||||
it is what caught Lucide's Action genre drawing an award ribbon. Both sizes are captured
|
||||
because a stroke set has least to spare at the 21dp the rail draws.
|
||||
|
||||
**Seasonal decorations** are `ui/seasonal/SeasonalDecorations.kt`: snow, bats or blossom
|
||||
drifting over the launcher for the few days a season is on. A palette on its own is a thin
|
||||
idea of Christmas — the colours change and nothing says why — and this is the half that
|
||||
|
||||
Reference in New Issue
Block a user