feat(search): answer search from a local index; tier downloads by lifetime
Search's instant leg read only downloaded items, so with no downloads it returned nothing and every keystroke fell through to a full Recursive=true server query. It now reads the whole synced catalog through the same availability CTE get_items uses, gated on the same include_catalog_browse flag so search and browse cannot diverge. (UR-065, DR-108) Also fixes three defects found while confirming that: - items_fts grew by a full duplicate index every catalog pass. INSERT OR REPLACE fires no AFTER DELETE trigger without recursive_triggers, so the old index row was orphaned, and a TEXT PRIMARY KEY meant the replacement took a fresh rowid and inserted a second entry. Now a real upsert, with migration 021 rebuilding existing indexes. (DR-110) - DELETE FROM items existed nowhere, so server-side deletions never propagated. Adds a post-crawl mark-and-sweep, scoped to crawled types, skipping downloaded items, and refusing to run after a partial crawl because items.parent_id cascades. (DR-110) - The index omitted MusicArtist, Playlist and People, which search groups results by. Adds them plus people_fts (migration 022). (DR-111) Re-indexing moves from a frontend startup call to a Rust background task with a 6h TTL, so a long session no longer searches a stale catalog and a restart no longer forces a crawl regardless of freshness. (DR-109, IR-030) Downloads gain a lifetime tier. Eviction selected every completed row by age with no download_source filter, so hitting the storage limit deleted the oldest download -- typically one saved deliberately for offline -- to make room for a precached track. It now reclaims only 'auto' rows, and expired ones are reclaimed first, before live cache is evicted. (DR-126, DR-127) Downloaded video and audio-only handoffs now play from disk instead of streaming; the video path had never consulted downloads at all. No transcode is involved: MPV runs video=no and ExoPlayer has no surface for an Audio item. (DR-123 in part, DR-128) FTS queries are built as quoted phrases so apostrophes, hyphens and slashes are data rather than operator syntax, and the item-type filter is bound rather than interpolated. Specs: docs/specs/catalog-index-search.md, docs/specs/read-through-media-cache.md Includes concurrently-developed favourites browsing and background-audio stream-end handling; the two workstreams share offline.rs, lib.rs and online.rs, so no subset of files builds independently.
This commit is contained in:
+126
-2
@@ -634,7 +634,7 @@ episode strip.
|
||||
│ │ S2E4 • 48m • ★8.1 │ │
|
||||
│ │ Overview… │ │
|
||||
│ │ ▓▓▓▓▓░░░░░ 32m left │ │
|
||||
│ │ [▶ Play] │ │
|
||||
│ │ [▶ Play] [⬇] [♡] │ │
|
||||
│ └───────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ More Episodes │ ← 2. EPISODE STRIP
|
||||
@@ -688,7 +688,7 @@ A movie has no continuation set, so cast follows the hero directly.
|
||||
### 5B.4 Series detail — section order
|
||||
|
||||
```
|
||||
Hero (poster, title, metadata, Resume SxEy / Download / Clear history)
|
||||
Hero (poster, title, metadata, Resume SxEy / Download / Favorite / Clear history)
|
||||
→ Crew links
|
||||
→ Genre tags
|
||||
→ Seasons (collapsible; only the current season expanded)
|
||||
@@ -757,6 +757,130 @@ opt-in. Grids and other surfaces keep tap-to-open with no long-press.
|
||||
|
||||
---
|
||||
|
||||
## 5C. Favourites
|
||||
|
||||
Favouriting is a two-sided promise: the heart takes the input, and the app must
|
||||
be able to give it back. This section covers both sides — where you can mark a
|
||||
favourite, and where marked favourites resurface.
|
||||
|
||||
See [specs/favorites-browsing.md](specs/favorites-browsing.md) for the layer
|
||||
assignment and wire shapes.
|
||||
|
||||
### 5C.1 The heart appears wherever an item does
|
||||
|
||||
A favourite is a property of an *item*, so the affordance follows the item
|
||||
rather than living on one privileged screen. Any surface that shows a whole
|
||||
item shows its heart.
|
||||
|
||||
| Surface | Heart position | Notes |
|
||||
|---------|----------------|-------|
|
||||
| Movie / Series detail hero | In the button row, after Play and Download | §5B.3, §5B.4 |
|
||||
| Episode Focus View hero | Same row as Play / Download | §5B.2 |
|
||||
| Album, Artist, Playlist detail | In the header button row | §5.2 |
|
||||
| Media card (any grid or carousel) | Top-right overlay on the artwork | Hidden on server-only (greyed) cards |
|
||||
| Mini player | Right of the track metadata | Existing behaviour, unchanged |
|
||||
| Full player | Secondary controls row | §3.2 — **not yet built**, see §5C.5 |
|
||||
|
||||
Rules:
|
||||
|
||||
- **The heart never competes with the card.** On a media card it is its own
|
||||
button and swallows the tap, so hearting an item never also opens or plays
|
||||
it, and never triggers the §5B.5 long-press.
|
||||
- **State is shown, not guessed.** A filled heart means the *server* considers
|
||||
the item a favourite (or you just tapped it). An item favourited in Jellyfin
|
||||
Web, on another device, or by another client renders filled here without
|
||||
being touched in JellyTau.
|
||||
- **Feedback is immediate.** The heart fills on tap and a toast confirms;
|
||||
neither waits for the server round-trip.
|
||||
|
||||
### 5C.2 Three ways back to what you favourited
|
||||
|
||||
Favourites are not one destination — they are a lens, and the right surface
|
||||
depends on whether the user is *browsing*, *deciding*, or *hunting*.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
User[User wants their favourites] --> How{Intent}
|
||||
|
||||
How -->|Passive: show me something| Home[Home carousels<br/>Favourite Movies / Shows / Music]
|
||||
How -->|Deliberate: my whole collection| Page[Favourites page<br/>/library/favorites]
|
||||
How -->|Narrowing: within this library| Filter[Favourites filter<br/>on a library page]
|
||||
|
||||
Home -->|See all| Page
|
||||
Page --> Detail[Item detail page]
|
||||
Filter --> Detail
|
||||
```
|
||||
|
||||
**Home carousels.** Rows for favourite movies, shows and music sit below
|
||||
*Recently Added*. A row with nothing in it **does not render** — a fresh install
|
||||
shows no empty favourite rows. Each row ends with *See all*, landing on the
|
||||
matching tab of the Favourites page.
|
||||
|
||||
**The Favourites page** (`/library/favorites`) is the complete collection,
|
||||
scoped by tabs:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ [←] Favourites │
|
||||
│ ┌─────┬────────┬───────┬───────┐ │
|
||||
│ │ All │ Movies │ Shows │ Music │ ← scope tabs │
|
||||
│ └─────┴────────┴───────┴───────┘ │
|
||||
│ │
|
||||
│ ┌────┐┌────┐┌────┐┌────┐┌────┐ │
|
||||
│ │ ♥ ││ ♥ ││ ♥ ││ ♥ ││ ♥ │ grid/list │
|
||||
│ └────┘└────┘└────┘└────┘└────┘ per §5A │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Cards obey §5A in full — shape follows the media, so a mixed *All* tab reads
|
||||
as posters, squares and thumbnails side by side rather than one forced shape.
|
||||
- Reached from a card on the library overview (`/library`) and from *See all*
|
||||
on any home favourites row.
|
||||
- Sorted by name. Jellyfin does not record *when* an item was favourited, so
|
||||
"recently favourited" is not offerable — see §5C.5.
|
||||
- Empty state, per tab: *"Nothing favourited yet — tap the heart on anything
|
||||
you like."*
|
||||
|
||||
**The in-library filter** is for narrowing where the user already is: a
|
||||
favourites toggle in the header of the Movies, TV and Music browse pages,
|
||||
filtering the current list in place. It is **session-scoped and not persisted** —
|
||||
a sticky filter that silently hides most of a library reads as data loss on the
|
||||
next launch.
|
||||
|
||||
### 5C.3 Removing a favourite removes it everywhere, at once
|
||||
|
||||
Un-hearting an item on the Favourites page removes its card from the grid
|
||||
immediately; the same item disappears from the home rows and shows an empty
|
||||
heart on its detail page without a manual refresh. The reverse holds for
|
||||
favouriting. There is no confirmation prompt — the action is one tap to undo.
|
||||
|
||||
### 5C.4 Offline
|
||||
|
||||
- **Marking works offline.** The heart fills, the toast confirms, and the change
|
||||
is held locally.
|
||||
- **It reaches the server on reconnect**, without the user returning to the
|
||||
screen where they made it.
|
||||
- **Browsing offline shows favourites among media on the device**, subject to
|
||||
the same "Show all server media" gate as every other browse surface (§7.2) —
|
||||
with the gate off, an empty Favourites tab means *nothing favourited is
|
||||
downloaded*, and the page does not quietly fall back to the server catalog.
|
||||
|
||||
### 5C.5 Known deviations
|
||||
|
||||
- **The full player has no heart.** §3.2 and §3.3 list a Favorite button among
|
||||
the full player's secondary controls; it was never built, and this pass does
|
||||
not add it. The mini player heart above it is the only in-player affordance.
|
||||
*(UR-067)*
|
||||
- **No "recently favourited" sort.** Jellyfin's API does not expose a favourite
|
||||
timestamp, so favourites can only be ordered by name. Recording the
|
||||
timestamp locally at toggle time would order *this device's* favourites only,
|
||||
which is worse than a consistent name sort.
|
||||
- **Music is one tab, not three.** The Music scope mixes albums, artists and
|
||||
tracks in a single grid rather than offering sub-tabs. Acceptable while
|
||||
favourite counts are small; revisit if the tab becomes unscannable.
|
||||
|
||||
---
|
||||
|
||||
## 6. Search Flow
|
||||
|
||||
Search is **context-scoped**: what you are looking at when you start a search
|
||||
|
||||
Reference in New Issue
Block a user