Skip to main content

#
jellytau_lib/repository/
mod.rs

    1pub mod device_profile;
    2/// User-chosen browsing exclusions (UR-076 / DR-209).
    3pub mod exclusions;
    4pub mod hybrid;
    5pub mod offline;
    6pub mod online;
    7pub mod series_progress;
    8/// Backend-owned stream selection (UR-079 / DR-225).
    9pub mod stream_selection;
    10pub mod types;
    11
    12pub use hybrid::HybridRepository;
    13pub use offline::OfflineRepository;
    14pub use online::{JRayActor, OnlineRepository};
    15pub use stream_selection::{StreamSelection, Transport};
    16pub use types::*;
    17
    18use async_trait::async_trait;
    19
    20/// Repository trait for media access (online, offline, or hybrid)
    21///
    22/// @req: UR-002 - Access media when online or offline
    23/// @req: UR-007 - Navigate media in library
    24/// @req: UR-008 - Search media across libraries
    25/// @req: IR-010 - Jellyfin API client for library browsing
    26/// @req: DR-012 - Local database for media metadata cache
    27/// @req: DR-013 - Repository pattern for online/offline data access
    28#[async_trait]
    29pub trait MediaRepository: Send + Sync {
    30    /// Get all libraries
    31    ///
    32    /// @req: UR-007 - Navigate media in library
    33    /// @req: JA-003 - Get user library views
    34    async fn get_libraries(&self) -> Result<Vec<Library>, RepoError>;
    35
    36    /// Get items in a library or parent
    37    ///
    38    /// @req: UR-007 - Navigate media in library
    39    /// @req: JA-004 - Get library items (paginated)
    40    async fn get_items(
    41        &self,
    42        parent_id: &str,
    43        options: Option<GetItemsOptions>,
    44    ) -> Result<SearchResult, RepoError>;
    45
    46    /// Get a single item by ID
    47    ///
    48    /// @req: UR-007 - Navigate media in library
    49    /// @req: JA-005 - Get item details and metadata
    50    async fn get_item(&self, item_id: &str) -> Result<MediaItem, RepoError>;
    51
    52    /// Get latest items in a library
    53    ///
    54    /// @req: UR-024 - View recently added content on server
    55    /// @req: JA-016 - Get recently added items
    56    async fn get_latest_items(
    57        &self,
    58        parent_id: &str,
    59        limit: Option<usize>,
    60    ) -> Result<Vec<MediaItem>, RepoError>;
    61
    62    /// Get resume items (continue watching/listening)
    63    ///
    64    /// @req: UR-019 - Resume playback from where you left off
    65    /// @req: UR-023 - View "Next Up" / Continue Watching on home screen
    66    /// @req: JA-015 - Get "Continue Watching" items
    67    async fn get_resume_items(
    68        &self,
    69        parent_id: Option<&str>,
    70        limit: Option<usize>,
    71    ) -> Result<Vec<MediaItem>, RepoError>;
    72
    73    /// Get next up episodes
    74    ///
    75    /// @req: UR-023 - View "Next Up" / Continue Watching; auto-play next episode
    76    /// @req: JA-014 - Get "Next Up" items
    77    async fn get_next_up_episodes(
    78        &self,
    79        series_id: Option<&str>,
    80        limit: Option<usize>,
    81    ) -> Result<Vec<MediaItem>, RepoError>;
    82
    83    /// Get recently played audio
    84    async fn get_recently_played_audio(
    85        &self,
    86        limit: Option<usize>,
    87    ) -> Result<Vec<MediaItem>, RepoError>;
    88
    89    /// Get albums the user has played, but not recently ("rediscover" / haven't
    90    /// listened to in a while). Returns albums sorted by least-recently played
    91    /// first, optionally restricted to a parent library.
    92    async fn get_rediscover_albums(
    93        &self,
    94        parent_id: Option<&str>,
    95        limit: Option<usize>,
    96    ) -> Result<Vec<MediaItem>, RepoError>;
    97
    98    /// Get resume movies
    99    async fn get_resume_movies(&self, limit: Option<usize>) -> Result<Vec<MediaItem>, RepoError>;
    100
    101    /// Get genres
    102    async fn get_genres(&self, parent_id: Option<&str>) -> Result<Vec<Genre>, RepoError>;
    103
    104    /// Search for items
    105    ///
    106    /// @req: UR-008 - Search media across libraries
    107    /// @req: JA-006 - Search across libraries
    108    async fn search(
    109        &self,
    110        query: &str,
    111        options: Option<SearchOptions>,
    112    ) -> Result<SearchResult, RepoError>;
    113
    114    /// Get playback info for streaming
    115    ///
    116    /// @req: UR-003 - Play videos
    117    /// @req: UR-004 - Play audio uninterrupted
    118    /// @req: JA-007 - Get playback info and stream URL
    119    async fn get_playback_info(&self, item_id: &str) -> Result<PlaybackInfo, RepoError>;
    120
    121    /// Get audio stream URL for a track
    122    ///
    123    /// @req: UR-004 - Play audio uninterrupted
    124    /// @req: JA-007 - Get playback info and stream URL
    125    async fn get_audio_stream_url(&self, item_id: &str) -> Result<String, RepoError>;
    126
    127    /// Get an audio-only stream URL for a *video* item (background-audio handoff).
    128    ///
    129    /// Used when autoplay advances to the next episode while the app is playing a
    130    /// video in audio-only mode in the background: the backend needs the next
    131    /// episode's audio-only URL without any frontend round-trip. Online-only;
    132    /// offline/cache repositories return an error.
    133    ///
    134    /// TRACES: UR-040 | JA-032
    135    async fn get_audio_only_stream_url_for_video(
    136        &self,
    137        item_id: &str,
    138        media_source_id: Option<&str>,
    139        start_time_seconds: Option<f64>,
    140        audio_stream_index: Option<i32>,
    141    ) -> Result<String, RepoError>;
    142
    143    /// Get Live TV channels (broadcast / IPTV) for browsing.
    144    async fn get_live_tv_channels(&self) -> Result<Vec<MediaItem>, RepoError>;
    145
    146    /// Get the root list of plugin "Channels" (Jellyfin Channels feature).
    147    /// Drill-down into a channel reuses `get_items(channel_id, ...)`.
    148    async fn get_channels(&self) -> Result<SearchResult, RepoError>;
    149
    150    /// Open a live stream (Live TV channel or live channel item) for playback.
    151    ///
    152    /// Returns the server transcoding URL plus identifiers needed to manage the
    153    /// stream. Required before a live channel can be played over HLS.
    154    async fn open_live_stream(&self, item_id: &str) -> Result<LiveStreamInfo, RepoError>;
    155
    156    /// Report playback start
    157    ///
    158    /// @req: UR-025 - Sync watch history and progress back to Jellyfin
    159    /// @req: JA-010 - Report playback start
    160    async fn report_playback_start(
    161        &self,
    162        item_id: &str,
    163        position_ticks: i64,
    164    ) -> Result<(), RepoError>;
    165
    166    /// Report playback progress
    167    ///
    168    /// @req: UR-025 - Sync watch history and progress back to Jellyfin
    169    /// @req: JA-011 - Report playback progress (periodic)
    170    async fn report_playback_progress(
    171        &self,
    172        item_id: &str,
    173        position_ticks: i64,
    174    ) -> Result<(), RepoError>;
    175
    176    /// Report playback stopped
    177    ///
    178    /// @req: UR-025 - Sync watch history and progress back to Jellyfin
    179    /// @req: JA-012 - Report playback stopped
    180    async fn report_playback_stopped(
    181        &self,
    182        item_id: &str,
    183        position_ticks: i64,
    184    ) -> Result<(), RepoError>;
    185
    186    /// Get image URL (synchronous - just constructs URL)
    187    fn get_image_url(
    188        &self,
    189        item_id: &str,
    190        image_type: ImageType,
    191        options: Option<ImageOptions>,
    192    ) -> String;
    193
    194    /// Get subtitle URL (synchronous - just constructs URL)
    195    /// Called by frontend via Tauri invoke (getSubtitleUrl in VideoPlayer.svelte)
    196    #[allow(dead_code)]
    197    fn get_subtitle_url(
    198        &self,
    199        item_id: &str,
    200        media_source_id: &str,
    201        stream_index: i32,
    202        format: &str,
    203    ) -> String;
    204
    205    /// Build the URL a video download is fetched from. Synchronous — it only
    206    /// constructs a URL, so it stays testable without a server. Reach it through
    207    /// [`resolve_video_download_url`] rather than calling it directly.
    208    ///
    209    /// `source_audio_codec` is the codec of the audio track the server would
    210    /// serve (see [`served_audio_codec`]); `None` when it is not known. At
    211    /// `original` quality it decides whether the file can be copied byte-for-byte
    212    /// or has to have its audio re-encoded on the way down — a downloaded file is
    213    /// played back with no server in reach, so it has to be decodable *here*.
    214    ///
    215    /// TRACES: UR-071 | DR-171
    216    #[allow(dead_code)]
    217    fn get_video_download_url(
    218        &self,
    219        item_id: &str,
    220        quality: &str,
    221        media_source_id: Option<&str>,
    222        source_audio_codec: Option<&str>,
    223    ) -> String;
    224
    225    /// Mark item as favorite
    226    async fn mark_favorite(&self, item_id: &str) -> Result<(), RepoError>;
    227
    228    /// Unmark item as favorite
    229    async fn unmark_favorite(&self, item_id: &str) -> Result<(), RepoError>;
    230
    231    /// Everything the viewer has favourited, across every library.
    232    ///
    233    /// Separate from `get_items` because favourites span libraries and
    234    /// `get_items` is `ParentId`-shaped. `scope` is the opaque enum the
    235    /// frontend sends; this layer expands it to item types (DR-063) so no
    236    /// Jellyfin taxonomy is needed on the other side of the IPC boundary.
    237    ///
    238    /// TRACES: UR-067 | DR-115, JA-033 | UT-100, UT-101
    239    async fn get_favorites(
    240        &self,
    241        scope: SearchScope,
    242        options: Option<GetItemsOptions>,
    243    ) -> Result<SearchResult, RepoError>;
    244
    245    /// Erase the viewer's watch history for an item: clear its played flag and
    246    /// its resume position. On a container (series, season) this applies to
    247    /// everything inside it, so a series is returned to "never watched" and
    248    /// reopens on its premiere.
    249    ///
    250    /// TRACES: UR-064 | DR-106
    251    async fn clear_watch_history(&self, item_id: &str) -> Result<(), RepoError>;
    252
    253    /// Mark an item played — the inverse of `clear_watch_history`. Needed by the
    254    /// sync-queue drain, which replays `mark_played` rows queued while the
    255    /// server was unreachable; reporting a stop at a made-up position was the
    256    /// previous stand-in and does not set the played flag reliably.
    257    ///
    258    /// TRACES: UR-025 | DR-131 | JA-035
    259    async fn mark_played(&self, item_id: &str) -> Result<(), RepoError>;
    260
    261    /// Get person details
    262    async fn get_person(&self, person_id: &str) -> Result<MediaItem, RepoError>;
    263
    264    /// Get items by person (filmography)
    265    async fn get_items_by_person(
    266        &self,
    267        person_id: &str,
    268        options: Option<GetItemsOptions>,
    269    ) -> Result<SearchResult, RepoError>;
    270
    271    /// Get similar/related items for a movie or show
    272    ///
    273    /// @req: UR-009 - Discover similar content based on current item
    274    async fn get_similar_items(
    275        &self,
    276        item_id: &str,
    277        limit: Option<usize>,
    278    ) -> Result<SearchResult, RepoError>;
    279
    280    // ===== Playlist Methods =====
    281
    282    /// Create a new playlist on the server
    283    ///
    284    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    285    /// @req: JA-019 - Get/create/update playlists
    286    async fn create_playlist(
    287        &self,
    288        name: &str,
    289        item_ids: &[String],
    290    ) -> Result<PlaylistCreatedResult, RepoError>;
    291
    292    /// Delete a playlist
    293    ///
    294    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    295    /// @req: JA-019 - Get/create/update playlists
    296    async fn delete_playlist(&self, playlist_id: &str) -> Result<(), RepoError>;
    297
    298    /// Rename a playlist
    299    ///
    300    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    301    /// @req: JA-019 - Get/create/update playlists
    302    async fn rename_playlist(&self, playlist_id: &str, name: &str) -> Result<(), RepoError>;
    303
    304    /// Get playlist items with PlaylistItemId (needed for remove/reorder)
    305    ///
    306    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    307    /// @req: JA-019 - Get/create/update playlists
    308    async fn get_playlist_items(&self, playlist_id: &str) -> Result<Vec<PlaylistEntry>, RepoError>;
    309
    310    /// Add items to a playlist
    311    ///
    312    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    313    /// @req: JA-020 - Add/remove items from playlist
    314    async fn add_to_playlist(
    315        &self,
    316        playlist_id: &str,
    317        item_ids: &[String],
    318    ) -> Result<(), RepoError>;
    319
    320    /// Remove items from a playlist using entry IDs (PlaylistItemId, NOT media item IDs)
    321    ///
    322    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    323    /// @req: JA-020 - Add/remove items from playlist
    324    async fn remove_from_playlist(
    325        &self,
    326        playlist_id: &str,
    327        entry_ids: &[String],
    328    ) -> Result<(), RepoError>;
    329
    330    /// Move a playlist item to a new position
    331    ///
    332    /// @req: UR-014 - Make and edit playlists of music that sync back to Jellyfin
    333    /// @req: JA-020 - Add/remove items from playlist
    334    async fn move_playlist_item(
    335        &self,
    336        playlist_id: &str,
    337        item_id: &str,
    338        new_index: u32,
    339    ) -> Result<(), RepoError>;
    340}
    341
    342/// The audio codec the server would serve for `item_id` — the default track, or
    343/// the first when none is marked, matching the track Jellyfin picks.
    344///
    345/// `None` when the item has no audio, names no codec, or cannot be fetched. A
    346/// caller must read that as "unknown", never as "fine": it is the input to a
    347/// policy that only *adds* a transcode, so an unknown codec leaves behaviour
    348/// exactly as it was.
    349///
    350/// TRACES: UR-071 | DR-171 | UT-166
    351pub async fn served_audio_codec(repo: &dyn MediaRepository, item_id: &str) -> Option<String> {
    352    let item = repo.get_item(item_id).await.ok()?;
    353    let audio: Vec<(Option<&str>, bool)> = item
    354        .media_streams
    355        .as_deref()
    356        .unwrap_or_default()
    357        .iter()
    358        .filter(|s| s.stream_type == "Audio")
    359        .map(|s| (s.codec.as_deref(), s.is_default))
    360        .collect();
    361
    362    device_profile::served_audio_codec(&audio).map(str::to_string)
    363}
    364
    365/// Resolve the download URL for a video, applying the audio-codec policy that
    366/// keeps the saved file playable offline (DR-171).
    367///
    368/// Every video download goes through here rather than calling the builder
    369/// directly: the builder is pure and cannot look the codec up, and a caller that
    370/// forgets to is exactly how the silent downloads shipped.
    371///
    372/// TRACES: UR-071 | DR-171
    373pub async fn resolve_video_download_url(
    374    repo: &dyn MediaRepository,
    375    item_id: &str,
    376    quality: &str,
    377    media_source_id: Option<&str>,
    378) -> String {
    379    let codec = served_audio_codec(repo, item_id).await;
    380    repo.get_video_download_url(item_id, quality, media_source_id, codec.as_deref())
    381}