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 transport: None,
203 id: track.id.clone(),
204 title: track.name.clone(),
205 name: Some(track.name.clone()), artist: track
207 .album_artist
208 .clone()
209 .or_else(|| track.artists.as_ref().and_then(|a| a.first().cloned())),
210 album: track.album_name.clone(),
211 album_name: track.album_name.clone(), album_id: track.album_id.clone(),
213 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(),
217 item_type: Some(track.item_type.clone()), playlist_id: None,
219 duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
220 artwork_url: primary_image_tag_for_url.and_then(|tag| {
221 track.album_id.as_ref().map(|album_id| {
222 repository.get_image_url(
223 album_id,
224 ImageType::Primary,
225 Some(ImageOptions {
226 max_width: Some(300),
227 tag: Some(tag),
228 ..Default::default()
229 }),
230 )
231 })
232 }),
233 media_type: MediaType::Audio,
234 source,
235 video_codec: None,
236 needs_transcoding: false,
237 video_width: None,
238 video_height: None,
239 subtitles: vec![],
240 series_id: None,
241 server_id: None,
242 };
243
244 let position = match request.position.as_str() {
246 "next" => AddPosition::Next,
247 _ => AddPosition::End,
248 };
249
250 let controller = player.0.lock().await;
251 let queue = controller.queue();
252 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
253
254 queue_lock.add(vec![media_item], position);
255
256 let result = get_queue_status(&controller);
257 drop(queue_lock);
258 controller.emit_queue_changed();
259
260 info!("Successfully added track {} to queue", request.track_id);
261 Ok(result)
262}
263
264#[tauri::command]
266#[specta::specta]
267pub async fn player_add_tracks_by_ids(
268 player: State<'_, PlayerStateWrapper>,
269 db: State<'_, DatabaseWrapper>,
270 repository_manager: State<'_, RepositoryManagerWrapper>,
271 repository_handle: String,
272 request: AddTracksByIdsRequest,
273) -> Result<QueueStatus, String> {
274 use crate::player::queue::AddPosition;
275
276 info!(
277 "player_add_tracks_by_ids called: {} tracks, position={}",
278 request.track_ids.len(),
279 request.position
280 );
281
282 let repository = repository_manager
284 .0
285 .get(&repository_handle)
286 .ok_or("Repository not found - user may need to log in")?;
287
288 let mut media_items = Vec::new();
290 for track_id in &request.track_ids {
291 info!("Fetching metadata for track {} via repository", track_id);
292 let track = repository
293 .get_item(track_id)
294 .await
295 .map_err(|e| format!("Failed to fetch track metadata for {}: {}", track_id, e))?;
296
297 let local_path = check_for_local_download(&db, track_id).await?;
299
300 let source = if let Some(path) = local_path {
301 info!("Using local download for track {}", track_id);
302 MediaSource::Local {
303 file_path: PathBuf::from(path),
304 jellyfin_item_id: Some(track.id.clone()),
305 }
306 } else {
307 let stream_url = repository
309 .get_audio_stream_url(&track.id)
310 .await
311 .map_err(|e| format!("Failed to get stream URL for {}: {}", track.name, e))?;
312
313 MediaSource::Remote {
314 stream_url,
315 jellyfin_item_id: track.id.clone(),
316 }
317 };
318
319 let primary_image_tag_for_url = track.primary_image_tag.clone();
321 let media_item = MediaItem {
322 transport: None,
324 id: track.id.clone(),
325 title: track.name.clone(),
326 name: Some(track.name.clone()), artist: track
328 .album_artist
329 .clone()
330 .or_else(|| track.artists.as_ref().and_then(|a| a.first().cloned())),
331 album: track.album_name.clone(),
332 album_name: track.album_name.clone(), album_id: track.album_id.clone(),
334 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(),
338 item_type: Some(track.item_type.clone()), playlist_id: None,
340 duration: track.runtime_ticks.map(|t| t as f64 / 10_000_000.0),
341 artwork_url: primary_image_tag_for_url.and_then(|tag| {
342 track.album_id.as_ref().map(|album_id| {
343 repository.get_image_url(
344 album_id,
345 ImageType::Primary,
346 Some(ImageOptions {
347 max_width: Some(300),
348 tag: Some(tag),
349 ..Default::default()
350 }),
351 )
352 })
353 }),
354 media_type: MediaType::Audio,
355 source,
356 video_codec: None,
357 needs_transcoding: false,
358 video_width: None,
359 video_height: None,
360 subtitles: vec![],
361 series_id: None,
362 server_id: None,
363 };
364
365 media_items.push(media_item);
366 }
367
368 let position = match request.position.as_str() {
370 "next" => AddPosition::Next,
371 _ => AddPosition::End,
372 };
373
374 let controller = player.0.lock().await;
375 let queue = controller.queue();
376 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
377
378 queue_lock.add(media_items, position);
379
380 let result = get_queue_status(&controller);
381 drop(queue_lock);
382 controller.emit_queue_changed();
383
384 info!(
385 "Successfully added {} tracks to queue",
386 request.track_ids.len()
387 );
388 Ok(result)
389}
390
391#[tauri::command]
392#[specta::specta]
393pub async fn player_skip_to(
394 player: State<'_, PlayerStateWrapper>,
395 db: State<'_, DatabaseWrapper>,
396 index: usize,
397) -> Result<PlayerStatus, String> {
398 let controller = player.0.lock().await;
399
400 if let Err(e) = super::refresh_queue_local_sources(&controller, &db).await {
402 log::warn!("[player_skip_to] Failed to refresh local sources: {}", e);
403 }
404
405 let item = {
407 let queue = controller.queue();
408 let mut queue_lock = queue.lock().map_err(|e| e.to_string())?;
409 queue_lock.skip_to(index).cloned().ok_or("Invalid index")?
410 };
411
412 controller.load_and_play(&item).map_err(|e| e.to_string())?;
414
415 controller.emit_queue_changed();
417
418 Ok(get_player_status(&controller))
419}