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