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}