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:
@@ -1,4 +1,4 @@
|
||||
<!-- TRACES: UR-007, UR-029, UR-030 | DR-007, DR-032, DR-033 -->
|
||||
<!-- TRACES: UR-007, UR-029, UR-030, UR-067 | DR-007, DR-032, DR-033, DR-116 -->
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
@@ -56,6 +56,7 @@
|
||||
let gridWrapper = $state<HTMLDivElement | null>(null);
|
||||
let searchQuery = $state("");
|
||||
let debouncedSearchQuery = $state("");
|
||||
let favoritesOnly = $state(false);
|
||||
let sortBy = $state<string>("");
|
||||
let sortOrder = $state<"Ascending" | "Descending">("Ascending");
|
||||
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@@ -140,6 +141,9 @@
|
||||
sortOrder,
|
||||
recursive: true,
|
||||
limit: 10000,
|
||||
// Narrows the listing in place; the backend owns what "favourite"
|
||||
// resolves to online vs offline. TRACES: UR-067 | DR-116
|
||||
favoritesOnly: favoritesOnly ? true : undefined,
|
||||
});
|
||||
items = excludePodcasts(result.items);
|
||||
}
|
||||
@@ -154,6 +158,12 @@
|
||||
searchQuery = query;
|
||||
}
|
||||
|
||||
/// TRACES: UR-067 | DR-116
|
||||
function toggleFavoritesOnly() {
|
||||
favoritesOnly = !favoritesOnly;
|
||||
loadItems();
|
||||
}
|
||||
|
||||
// Debounce search input (300ms delay) - skip initial mount to avoid duplicate load
|
||||
$effect(() => {
|
||||
const _query = searchQuery; // track for reactivity
|
||||
@@ -266,6 +276,33 @@
|
||||
<SearchBar value={searchQuery} placeholder={searchPlaceholder} onInput={handleSearch} />
|
||||
</div>
|
||||
|
||||
<!-- Favourites filter. Session-scoped on purpose: a persisted filter that
|
||||
hides most of a library reads as data loss on the next launch
|
||||
(ux-flows §5C.2). Hidden while searching, which has no favourites
|
||||
filter of its own. TRACES: UR-067 | DR-116 -->
|
||||
{#if !debouncedSearchQuery.trim()}
|
||||
<button
|
||||
onclick={toggleFavoritesOnly}
|
||||
aria-pressed={favoritesOnly}
|
||||
class="px-3 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors
|
||||
{favoritesOnly
|
||||
? 'bg-[var(--color-jellyfin)] text-white'
|
||||
: 'bg-[var(--color-surface)] text-gray-400 hover:text-white'}"
|
||||
title={favoritesOnly ? "Showing favourites only" : "Show favourites only"}
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill={favoritesOnly ? "currentColor" : "none"}
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
Favourites
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Sort (only show if there are sort options) -->
|
||||
{#if config.sortOptions.length > 0}
|
||||
<SortButtonGroup options={config.sortOptions} selected={sortBy} onSelect={handleSort} />
|
||||
|
||||
Reference in New Issue
Block a user