1use std::path::PathBuf;
6
7use log::info;
8use serde::Deserialize;
9use tauri::State;
10
11use super::{
12 check_for_local_download, create_media_item, get_player_status, get_queue_status,
13 DatabaseWrapper, PlayItemRequest, PlayerStateWrapper, PlayerStatus, QueueStatus,
14};
15use crate::commands::repository::RepositoryManagerWrapper;
16use crate::player::{MediaItem, MediaSource, MediaType};
17use crate::repository::types::{ImageOptions, ImageType};
18use crate::repository::MediaRepository;
19
20#[derive(specta::Type, Debug, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct AddToQueueRequest {
24 pub items: Vec<PlayItemRequest>,
25 pub position: String, }
27
28#[derive(specta::Type, Debug, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct AddTrackByIdRequest {
32 pub track_id: String,
33 pub position: String, }
35
36#[derive(specta::Type, Debug, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct AddTracksByIdsRequest {
40 pub track_ids: Vec<String>,
41 pub position: String, }
43
44#[tauri::command]
45#[specta::specta]
46pub async fn player_add_to_queue(
47 player: State<'_, PlayerStateWrapper>,
48 db: State<'_, DatabaseWrapper>,
49 request: AddToQueueRequest,
50) -> Result<QueueStatus, String> {
51 use crate::player::queue::AddPosition;
52
53 let position = match request.position.as_str() {
54 "next" => AddPosition::Next,
55 _ => AddPosition::End,
56 };
57
58 let mut items: Vec<MediaItem> = Vec::new();
60 for req in request.items {
61 items.push(create_media_item(req, Some(&db)).await?);
62 }
63
64 let controller = player.0.lock().await;
66 let queue = controller.queue();
67 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
68
69 queue_lock.add(items, position);
70
71 let result = QueueStatus {
72 items: queue_lock.items().to_vec(),
73 current_index: queue_lock.current_index(),
74 shuffle: queue_lock.is_shuffle(),
75 repeat: queue_lock.repeat_mode(),
76 has_next: queue_lock.has_next(),
77 has_previous: queue_lock.has_previous(),
78 };
79
80 drop(queue_lock);
81 controller.emit_queue_changed();
82
83 Ok(result)
84}
85
86#[tauri::command]
87#[specta::specta]
88pub async fn player_remove_from_queue(
89 player: State<'_, PlayerStateWrapper>,
90 index: usize,
91) -> Result<QueueStatus, String> {
92 let controller = player.0.lock().await;
93 let queue = controller.queue();
94 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
95
96 queue_lock.remove(index);
97
98 let result = QueueStatus {
99 items: queue_lock.items().to_vec(),
100 current_index: queue_lock.current_index(),
101 shuffle: queue_lock.is_shuffle(),
102 repeat: queue_lock.repeat_mode(),
103 has_next: queue_lock.has_next(),
104 has_previous: queue_lock.has_previous(),
105 };
106
107 drop(queue_lock);
108 controller.emit_queue_changed();
109
110 Ok(result)
111}
112
113#[tauri::command]
114#[specta::specta]
115pub async fn player_move_in_queue(
116 player: State<'_, PlayerStateWrapper>,
117 from_index: usize,
118 to_index: usize,
119) -> Result<QueueStatus, String> {
120 let controller = player.0.lock().await;
121 let queue = controller.queue();
122 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
123
124 if !queue_lock.move_item(from_index, to_index) {
125 return Err("Invalid indices for move operation".to_string());
126 }
127
128 let result = QueueStatus {
129 items: queue_lock.items().to_vec(),
130 current_index: queue_lock.current_index(),
131 shuffle: queue_lock.is_shuffle(),
132 repeat: queue_lock.repeat_mode(),
133 has_next: queue_lock.has_next(),
134 has_previous: queue_lock.has_previous(),
135 };
136
137 drop(queue_lock);
138 controller.emit_queue_changed();
139
140 Ok(result)
141}
142
143#[tauri::command]
145#[specta::specta]
146pub async fn player_add_track_by_id(
147 player: State<'_, PlayerStateWrapper>,
148 db: State<'_, DatabaseWrapper>,
149 repository_manager: State<'_, RepositoryManagerWrapper>,
150 repository_handle: String,
151 request: AddTrackByIdRequest,
152) -> Result<QueueStatus, String> {
153 use crate::player::queue::AddPosition;
154
155 info!(
156 "player_add_track_by_id called: track_id={}, position={}",
157 request.track_id, request.position
158 );
159
160 let repository = repository_manager
162 .0
163 .get(&repository_handle)
164 .ok_or("Repository not found - user may need to log in")?;
165
166 info!(
168 "Fetching metadata for track {} via repository",
169 request.track_id
170 );
171 let track = repository
172 .get_item(&request.track_id)
173 .await
174 .map_err(|e| format!("Failed to fetch track metadata: {}", e))?;
175
176 let local_path = check_for_local_download(&db, &request.track_id).await?;
178
179 let source = if let Some(path) = local_path {
180 info!("Using local download for track {}", request.track_id);
181 MediaSource::Local {
182 file_path: PathBuf::from(path),
183 jellyfin_item_id: Some(track.id.clone()),
184 }
185 } else {
186 let stream_url = repository
188 .get_audio_stream_url(&track.id)
189 .await
190 .map_err(|e| format!("Failed to get stream URL for {}: {}", track.name, e))?;
191
192 MediaSource::Remote {
193 stream_url,
194 jellyfin_item_id: track.id.clone(),
195 }
196 };
197
198 let primary_image_tag_for_url = track.primary_image_tag.clone();
200 let media_item = MediaItem {
201 id: track.id.clone(),
202 title: track.name.clone(),
203 name: Some(track.name.clone()), artist: track
205 .album_artist
206 .clone()
207 .or_else(|| track.artists.as_ref().and_then(|a| a.first().cloned())),
208 album: track.album_name.clone(),
209 album_name: track.album_name.clone(), album_id: track.album_id.clone(),
211 artist_items: track.artist_items.clone(), artists: track.artists.clone(), primary_image_tag: track.primary_image_tag.clone(), image_id: track.primary_image_tag.clone(),
215 item_type: Some(track.item_type.clone()), playlist_id: None,
217 duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
218 artwork_url: primary_image_tag_for_url.and_then(|tag| {
219 track.album_id.as_ref().map(|album_id| {
220 repository.get_image_url(
221 album_id,
222 ImageType::Primary,
223 Some(ImageOptions {
224 max_width: Some(300),
225 tag: Some(tag),
226 ..Default::default()
227 }),
228 )
229 })
230 }),
231 media_type: MediaType::Audio,
232 source,
233 video_codec: None,
234 needs_transcoding: false,
235 video_width: None,
236 video_height: None,
237 subtitles: vec![],
238 series_id: None,
239 server_id: None,
240 };
241
242 let position = match request.position.as_str() {
244 "next" => AddPosition::Next,
245 _ => AddPosition::End,
246 };
247
248 let controller = player.0.lock().await;
249 let queue = controller.queue();
250 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
251
252 queue_lock.add(vec![media_item], position);
253
254 let result = get_queue_status(&controller);
255 drop(queue_lock);
256 controller.emit_queue_changed();
257
258 info!("Successfully added track {} to queue", request.track_id);
259 Ok(result)
260}
261
262#[tauri::command]
264#[specta::specta]
265pub async fn player_add_tracks_by_ids(
266 player: State<'_, PlayerStateWrapper>,
267 db: State<'_, DatabaseWrapper>,
268 repository_manager: State<'_, RepositoryManagerWrapper>,
269 repository_handle: String,
270 request: AddTracksByIdsRequest,
271) -> Result<QueueStatus, String> {
272 use crate::player::queue::AddPosition;
273
274 info!(
275 "player_add_tracks_by_ids called: {} tracks, position={}",
276 request.track_ids.len(),
277 request.position
278 );
279
280 let repository = repository_manager
282 .0
283 .get(&repository_handle)
284 .ok_or("Repository not found - user may need to log in")?;
285
286 let mut media_items = Vec::new();
288 for track_id in &request.track_ids {
289 info!("Fetching metadata for track {} via repository", track_id);
290 let track = repository
291 .get_item(track_id)
292 .await
293 .map_err(|e| format!("Failed to fetch track metadata for {}: {}", track_id, e))?;
294
295 let local_path = check_for_local_download(&db, track_id).await?;
297
298 let source = if let Some(path) = local_path {
299 info!("Using local download for track {}", track_id);
300 MediaSource::Local {
301 file_path: PathBuf::from(path),
302 jellyfin_item_id: Some(track.id.clone()),
303 }
304 } else {
305 let stream_url = repository
307 .get_audio_stream_url(&track.id)
308 .await
309 .map_err(|e| format!("Failed to get stream URL for {}: {}", track.name, e))?;
310
311 MediaSource::Remote {
312 stream_url,
313 jellyfin_item_id: track.id.clone(),
314 }
315 };
316
317 let primary_image_tag_for_url = track.primary_image_tag.clone();
319 let media_item = MediaItem {
320 id: track.id.clone(),
321 title: track.name.clone(),
322 name: Some(track.name.clone()), artist: track
324 .album_artist
325 .clone()
326 .or_else(|| track.artists.as_ref().and_then(|a| a.first().cloned())),
327 album: track.album_name.clone(),
328 album_name: track.album_name.clone(), album_id: track.album_id.clone(),
330 artist_items: track.artist_items.clone(), artists: track.artists.clone(), primary_image_tag: track.primary_image_tag.clone(), image_id: track.primary_image_tag.clone(),
334 item_type: Some(track.item_type.clone()), playlist_id: None,
336 duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
337 artwork_url: primary_image_tag_for_url.and_then(|tag| {
338 track.album_id.as_ref().map(|album_id| {
339 repository.get_image_url(
340 album_id,
341 ImageType::Primary,
342 Some(ImageOptions {
343 max_width: Some(300),
344 tag: Some(tag),
345 ..Default::default()
346 }),
347 )
348 })
349 }),
350 media_type: MediaType::Audio,
351 source,
352 video_codec: None,
353 needs_transcoding: false,
354 video_width: None,
355 video_height: None,
356 subtitles: vec![],
357 series_id: None,
358 server_id: None,
359 };
360
361 media_items.push(media_item);
362 }
363
364 let position = match request.position.as_str() {
366 "next" => AddPosition::Next,
367 _ => AddPosition::End,
368 };
369
370 let controller = player.0.lock().await;
371 let queue = controller.queue();
372 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
373
374 queue_lock.add(media_items, position);
375
376 let result = get_queue_status(&controller);
377 drop(queue_lock);
378 controller.emit_queue_changed();
379
380 info!(
381 "Successfully added {} tracks to queue",
382 request.track_ids.len()
383 );
384 Ok(result)
385}
386
387#[tauri::command]
388#[specta::specta]
389pub async fn player_skip_to(
390 player: State<'_, PlayerStateWrapper>,
391 db: State<'_, DatabaseWrapper>,
392 index: usize,
393) -> Result<PlayerStatus, String> {
394 let controller = player.0.lock().await;
395
396 if let Err(e) = super::refresh_queue_local_sources(&controller, &db).await {
398 log::warn!("[player_skip_to] Failed to refresh local sources: {}", e);
399 }
400
401 let item = {
403 let queue = controller.queue();
404 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
405 queue_lock.skip_to(index).cloned().ok_or("Invalid index")?
406 };
407
408 controller.load_and_play(&item).map_err(|e| e.to_string())?;
410
411 controller.emit_queue_changed();
413
414 Ok(get_player_status(&controller))
415}