Files
jellytau/src-tauri/src/repository/mod.rs
T
dtourolle 62873cab3d 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.
2026-08-04 17:35:17 +02:00

317 lines
11 KiB
Rust

pub mod hybrid;
pub mod offline;
pub mod online;
pub mod series_progress;
pub mod types;
pub use hybrid::HybridRepository;
pub use offline::OfflineRepository;
pub use online::{JRayActor, OnlineRepository};
pub use types::*;
use async_trait::async_trait;
/// Repository trait for media access (online, offline, or hybrid)
///
/// @req: UR-002 - Access media when online or offline
/// @req: UR-007 - Navigate media in library
/// @req: UR-008 - Search media across libraries
/// @req: IR-010 - Jellyfin API client for library browsing
/// @req: DR-012 - Local database for media metadata cache
/// @req: DR-013 - Repository pattern for online/offline data access
#[async_trait]
pub trait MediaRepository: Send + Sync {
/// Get all libraries
///
/// @req: UR-007 - Navigate media in library
/// @req: JA-003 - Get user library views
async fn get_libraries(&self) -> Result<Vec<Library>, RepoError>;
/// Get items in a library or parent
///
/// @req: UR-007 - Navigate media in library
/// @req: JA-004 - Get library items (paginated)
async fn get_items(
&self,
parent_id: &str,
options: Option<GetItemsOptions>,
) -> Result<SearchResult, RepoError>;
/// Get a single item by ID
///
/// @req: UR-007 - Navigate media in library
/// @req: JA-005 - Get item details and metadata
async fn get_item(&self, item_id: &str) -> Result<MediaItem, RepoError>;
/// Get latest items in a library
///
/// @req: UR-024 - View recently added content on server
/// @req: JA-016 - Get recently added items
async fn get_latest_items(
&self,
parent_id: &str,
limit: Option<usize>,
) -> Result<Vec<MediaItem>, RepoError>;
/// Get resume items (continue watching/listening)
///
/// @req: UR-019 - Resume playback from where you left off
/// @req: UR-023 - View "Next Up" / Continue Watching on home screen
/// @req: JA-015 - Get "Continue Watching" items
async fn get_resume_items(
&self,
parent_id: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<MediaItem>, RepoError>;
/// Get next up episodes
///
/// @req: UR-023 - View "Next Up" / Continue Watching; auto-play next episode
/// @req: JA-014 - Get "Next Up" items
async fn get_next_up_episodes(
&self,
series_id: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<MediaItem>, RepoError>;
/// Get recently played audio
async fn get_recently_played_audio(
&self,
limit: Option<usize>,
) -> Result<Vec<MediaItem>, RepoError>;
/// Get albums the user has played, but not recently ("rediscover" / haven't
/// listened to in a while). Returns albums sorted by least-recently played
/// first, optionally restricted to a parent library.
async fn get_rediscover_albums(
&self,
parent_id: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<MediaItem>, RepoError>;
/// Get resume movies
async fn get_resume_movies(&self, limit: Option<usize>) -> Result<Vec<MediaItem>, RepoError>;
/// Get genres
async fn get_genres(&self, parent_id: Option<&str>) -> Result<Vec<Genre>, RepoError>;
/// Search for items
///
/// @req: UR-008 - Search media across libraries
/// @req: JA-006 - Search across libraries
async fn search(
&self,
query: &str,
options: Option<SearchOptions>,
) -> Result<SearchResult, RepoError>;
/// Get playback info for streaming
///
/// @req: UR-003 - Play videos
/// @req: UR-004 - Play audio uninterrupted
/// @req: JA-007 - Get playback info and stream URL
async fn get_playback_info(&self, item_id: &str) -> Result<PlaybackInfo, RepoError>;
/// Get audio stream URL for a track
///
/// @req: UR-004 - Play audio uninterrupted
/// @req: JA-007 - Get playback info and stream URL
async fn get_audio_stream_url(&self, item_id: &str) -> Result<String, RepoError>;
/// Get an audio-only stream URL for a *video* item (background-audio handoff).
///
/// Used when autoplay advances to the next episode while the app is playing a
/// video in audio-only mode in the background: the backend needs the next
/// episode's audio-only URL without any frontend round-trip. Online-only;
/// offline/cache repositories return an error.
///
/// TRACES: UR-040 | JA-032
async fn get_audio_only_stream_url_for_video(
&self,
item_id: &str,
media_source_id: Option<&str>,
start_time_seconds: Option<f64>,
audio_stream_index: Option<i32>,
) -> Result<String, RepoError>;
/// Get Live TV channels (broadcast / IPTV) for browsing.
async fn get_live_tv_channels(&self) -> Result<Vec<MediaItem>, RepoError>;
/// Get the root list of plugin "Channels" (Jellyfin Channels feature).
/// Drill-down into a channel reuses `get_items(channel_id, ...)`.
async fn get_channels(&self) -> Result<SearchResult, RepoError>;
/// Open a live stream (Live TV channel or live channel item) for playback.
///
/// Returns the server transcoding URL plus identifiers needed to manage the
/// stream. Required before a live channel can be played over HLS.
async fn open_live_stream(&self, item_id: &str) -> Result<LiveStreamInfo, RepoError>;
/// Report playback start
///
/// @req: UR-025 - Sync watch history and progress back to Jellyfin
/// @req: JA-010 - Report playback start
async fn report_playback_start(
&self,
item_id: &str,
position_ticks: i64,
) -> Result<(), RepoError>;
/// Report playback progress
///
/// @req: UR-025 - Sync watch history and progress back to Jellyfin
/// @req: JA-011 - Report playback progress (periodic)
async fn report_playback_progress(
&self,
item_id: &str,
position_ticks: i64,
) -> Result<(), RepoError>;
/// Report playback stopped
///
/// @req: UR-025 - Sync watch history and progress back to Jellyfin
/// @req: JA-012 - Report playback stopped
async fn report_playback_stopped(
&self,
item_id: &str,
position_ticks: i64,
) -> Result<(), RepoError>;
/// Get image URL (synchronous - just constructs URL)
fn get_image_url(
&self,
item_id: &str,
image_type: ImageType,
options: Option<ImageOptions>,
) -> String;
/// Get subtitle URL (synchronous - just constructs URL)
/// Called by frontend via Tauri invoke (getSubtitleUrl in VideoPlayer.svelte)
#[allow(dead_code)]
fn get_subtitle_url(
&self,
item_id: &str,
media_source_id: &str,
stream_index: i32,
format: &str,
) -> String;
/// Get video download URL (synchronous - just constructs URL)
/// Called by frontend via Tauri invoke (getVideoDownloadUrl in VideoDownloadButton.svelte)
#[allow(dead_code)]
fn get_video_download_url(
&self,
item_id: &str,
quality: &str,
media_source_id: Option<&str>,
) -> String;
/// Mark item as favorite
async fn mark_favorite(&self, item_id: &str) -> Result<(), RepoError>;
/// Unmark item as favorite
async fn unmark_favorite(&self, item_id: &str) -> Result<(), RepoError>;
/// Everything the viewer has favourited, across every library.
///
/// Separate from `get_items` because favourites span libraries and
/// `get_items` is `ParentId`-shaped. `scope` is the opaque enum the
/// frontend sends; this layer expands it to item types (DR-063) so no
/// Jellyfin taxonomy is needed on the other side of the IPC boundary.
///
/// TRACES: UR-067 | DR-115, JA-033 | UT-100, UT-101
async fn get_favorites(
&self,
scope: SearchScope,
options: Option<GetItemsOptions>,
) -> Result<SearchResult, RepoError>;
/// Erase the viewer's watch history for an item: clear its played flag and
/// its resume position. On a container (series, season) this applies to
/// everything inside it, so a series is returned to "never watched" and
/// reopens on its premiere.
///
/// TRACES: UR-064 | DR-106
async fn clear_watch_history(&self, item_id: &str) -> Result<(), RepoError>;
/// Get person details
async fn get_person(&self, person_id: &str) -> Result<MediaItem, RepoError>;
/// Get items by person (filmography)
async fn get_items_by_person(
&self,
person_id: &str,
options: Option<GetItemsOptions>,
) -> Result<SearchResult, RepoError>;
/// Get similar/related items for a movie or show
///
/// @req: UR-009 - Discover similar content based on current item
async fn get_similar_items(
&self,
item_id: &str,
limit: Option<usize>,
) -> Result<SearchResult, RepoError>;
// ===== Playlist Methods =====
/// Create a new playlist on the server
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-019 - Get/create/update playlists
async fn create_playlist(
&self,
name: &str,
item_ids: &[String],
) -> Result<PlaylistCreatedResult, RepoError>;
/// Delete a playlist
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-019 - Get/create/update playlists
async fn delete_playlist(&self, playlist_id: &str) -> Result<(), RepoError>;
/// Rename a playlist
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-019 - Get/create/update playlists
async fn rename_playlist(&self, playlist_id: &str, name: &str) -> Result<(), RepoError>;
/// Get playlist items with PlaylistItemId (needed for remove/reorder)
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-019 - Get/create/update playlists
async fn get_playlist_items(&self, playlist_id: &str) -> Result<Vec<PlaylistEntry>, RepoError>;
/// Add items to a playlist
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-020 - Add/remove items from playlist
async fn add_to_playlist(
&self,
playlist_id: &str,
item_ids: &[String],
) -> Result<(), RepoError>;
/// Remove items from a playlist using entry IDs (PlaylistItemId, NOT media item IDs)
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-020 - Add/remove items from playlist
async fn remove_from_playlist(
&self,
playlist_id: &str,
entry_ids: &[String],
) -> Result<(), RepoError>;
/// Move a playlist item to a new position
///
/// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
/// @req: JA-020 - Add/remove items from playlist
async fn move_playlist_item(
&self,
playlist_id: &str,
item_id: &str,
new_index: u32,
) -> Result<(), RepoError>;
}