jellytau_lib/commands/player/
timers.rs1use std::sync::Arc;
9use tauri::State;
10
11use super::{
12 create_media_item, get_player_status, DatabaseWrapper, PlayItemRequest, PlayerStateWrapper,
13 PlayerStatus,
14};
15use crate::player::{AutoplaySettings, SleepTimerMode, SleepTimerState};
16use crate::storage::db_service::{DatabaseService, Query, QueryParam};
17
18#[tauri::command]
22#[specta::specta]
23pub async fn player_set_sleep_timer(
24 player: State<'_, PlayerStateWrapper>,
25 mode: SleepTimerMode,
26) -> Result<SleepTimerState, String> {
27 let controller = player.0.lock().await;
28 controller.set_sleep_timer(mode);
29 Ok(controller.sleep_timer_state())
30}
31
32#[tauri::command]
34#[specta::specta]
35pub async fn player_cancel_sleep_timer(
36 player: State<'_, PlayerStateWrapper>,
37) -> Result<SleepTimerState, String> {
38 let controller = player.0.lock().await;
39 controller.cancel_sleep_timer();
40 Ok(controller.sleep_timer_state())
41}
42
43#[tauri::command]
45#[specta::specta]
46pub async fn player_get_sleep_timer(
47 player: State<'_, PlayerStateWrapper>,
48) -> Result<SleepTimerState, String> {
49 let controller = player.0.lock().await;
50 Ok(controller.sleep_timer_state())
51}
52
53#[tauri::command]
57#[specta::specta]
58pub async fn player_get_autoplay_settings(
59 player: State<'_, PlayerStateWrapper>,
60) -> Result<AutoplaySettings, String> {
61 let controller = player.0.lock().await;
62 Ok(controller.autoplay_settings())
63}
64
65#[tauri::command]
67#[specta::specta]
68pub async fn player_set_autoplay_settings(
69 player: State<'_, PlayerStateWrapper>,
70 db: State<'_, DatabaseWrapper>,
71 user_id: String,
72 settings: AutoplaySettings,
73) -> Result<AutoplaySettings, String> {
74 let validated = settings.with_validated_countdown();
75
76 {
78 let controller = player.0.lock().await;
79 controller.set_autoplay_settings(validated.clone());
80 }
81
82 let db_service = {
84 let database = db.0.lock().map_err(|e| e.to_string())?;
85 Arc::new(database.service())
86 };
87
88 let query = Query::with_params(
89 "INSERT INTO user_player_settings (user_id, autoplay_next_episode, autoplay_countdown_seconds, autoplay_max_episodes, updated_at)
90 VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
91 ON CONFLICT(user_id) DO UPDATE SET
92 autoplay_next_episode = excluded.autoplay_next_episode,
93 autoplay_countdown_seconds = excluded.autoplay_countdown_seconds,
94 autoplay_max_episodes = excluded.autoplay_max_episodes,
95 updated_at = CURRENT_TIMESTAMP",
96 vec![
97 QueryParam::String(user_id),
98 QueryParam::Int(if validated.enabled { 1 } else { 0 }),
99 QueryParam::Int(validated.countdown_seconds as i32),
100 QueryParam::Int(validated.max_episodes as i32),
101 ],
102 );
103
104 db_service.execute(query).await.map_err(|e| e.to_string())?;
105
106 Ok(validated)
107}
108
109#[tauri::command]
111#[specta::specta]
112pub async fn player_cancel_autoplay_countdown(
113 player: State<'_, PlayerStateWrapper>,
114) -> Result<(), String> {
115 let controller = player.0.lock().await;
116 controller.cancel_autoplay_countdown();
117 Ok(())
118}
119
120#[tauri::command]
122#[specta::specta]
123pub async fn player_play_next_episode(
124 player: State<'_, PlayerStateWrapper>,
125 db: State<'_, DatabaseWrapper>,
126 item: PlayItemRequest,
127) -> Result<PlayerStatus, String> {
128 let media_item = create_media_item(item, Some(&db)).await?;
130
131 let controller = player.0.lock().await;
132 controller
133 .play_item(media_item)
134 .map_err(|e| e.to_string())?;
135
136 Ok(get_player_status(&controller))
137}
138
139#[tauri::command]
147#[specta::specta]
148pub async fn player_on_playback_ended(
149 player: State<'_, PlayerStateWrapper>,
150 repository_manager: State<'_, crate::commands::repository::RepositoryManagerWrapper>,
151 db: State<'_, DatabaseWrapper>,
152 item_id: Option<String>,
153 repository_handle: Option<String>,
154) -> Result<(), String> {
155 use crate::player::autoplay::AutoplayDecision;
156 use crate::player::PlayerStatusEvent;
157
158 let controller_arc = player.0.clone();
159
160 let decision = {
164 let controller = controller_arc.lock().await;
165 if let Some(ref id) = item_id {
166 let repo = repository_handle
168 .as_ref()
169 .and_then(|handle| repository_manager.0.get(handle));
170 if let Some(repo) = repo {
171 controller.on_video_playback_ended(id, repo).await?
172 } else {
173 log::warn!(
174 "[Autoplay] No repository available for video autoplay (itemId: {})",
175 id
176 );
177 AutoplayDecision::Stop
178 }
179 } else {
180 controller.on_playback_ended().await?
181 }
182 };
183
184 match decision {
186 AutoplayDecision::Stop => {
187 log::info!("[Autoplay] Decision: Stop playback");
188 let controller = controller_arc.lock().await;
189 controller.clear_queue();
194 controller.emit_queue_changed();
195 if let Some(emitter) = controller.event_emitter() {
196 emitter.emit(PlayerStatusEvent::StateChanged {
200 state: "idle".to_string(),
201 media_id: None,
202 });
203 }
204 }
205 AutoplayDecision::AdvanceToNext => {
206 log::info!("[Autoplay] Decision: Advance to next track");
207 let controller = controller_arc.lock().await;
209 if let Err(e) = super::refresh_queue_local_sources(&controller, &db).await {
212 log::warn!("[Autoplay] Failed to refresh local sources: {}", e);
213 }
214 if let Err(e) = controller.next() {
215 log::error!("[Autoplay] Failed to advance to next track: {}", e);
216 if let Some(emitter) = controller.event_emitter() {
218 emitter.emit(PlayerStatusEvent::PlaybackEnded);
219 }
220 } else {
221 controller.emit_queue_changed();
223 }
224 }
225 AutoplayDecision::ShowNextEpisodePopup {
226 current_episode,
227 next_episode,
228 countdown_seconds,
229 auto_advance,
230 } => {
231 log::info!(
232 "[Autoplay] Decision: Show next episode popup (countdown: {}s, auto: {})",
233 countdown_seconds,
234 auto_advance
235 );
236
237 if let Some(emitter) = controller_arc.lock().await.event_emitter() {
239 emitter.emit(PlayerStatusEvent::ShowNextEpisodePopup {
240 current_episode: current_episode.clone(),
241 next_episode: next_episode.clone(),
242 countdown_seconds,
243 auto_advance,
244 });
245 }
246
247 if auto_advance {
253 controller_arc
254 .lock()
255 .await
256 .auto_advance_to_next_episode(next_episode, countdown_seconds)
257 .await;
258 }
259 }
260 AutoplayDecision::ResumeStream { position } => {
261 log::info!(
266 "[Autoplay] Decision: Resume truncated stream at {:.1}s",
267 position
268 );
269 let controller = controller_arc.lock().await;
270 if let Err(e) = controller.resume_stream_at(position).await {
271 log::error!("[Autoplay] Failed to resume truncated stream: {}", e);
272 if let Some(emitter) = controller.event_emitter() {
273 emitter.emit(PlayerStatusEvent::PlaybackEnded);
274 }
275 }
276 }
277 }
278
279 Ok(())
280}
281
282#[tauri::command]
301#[specta::specta]
302pub async fn player_recover_stream(player: State<'_, PlayerStateWrapper>) -> Result<bool, String> {
303 let (position, delay_secs) = {
304 let controller = player.0.lock().await;
305 match controller.recoverable_error_resume() {
306 Some(resume) => resume,
307 None => return Ok(false),
308 }
309 };
310
311 log::warn!(
312 "[Recovery] Stream failed — re-opening at {:.1}s in {}s",
313 position,
314 delay_secs
315 );
316 tokio::time::sleep(std::time::Duration::from_secs(delay_secs)).await;
318
319 let controller = player.0.lock().await;
320 match controller.resume_stream_at(position).await {
321 Ok(()) => Ok(true),
322 Err(e) => {
323 log::error!("[Recovery] Failed to re-open stream: {}", e);
324 Ok(false)
325 }
326 }
327}
328
329#[tauri::command]
340#[specta::specta]
341pub async fn player_report_state(
342 player: State<'_, PlayerStateWrapper>,
343 state: String,
344 media_id: Option<String>,
345) -> Result<(), String> {
346 let controller = player.0.lock().await;
347 controller.report_html5_state(state, media_id);
348 Ok(())
349}
350
351#[tauri::command]
354#[specta::specta]
355pub async fn player_report_position(
356 player: State<'_, PlayerStateWrapper>,
357 position: f64,
358 duration: f64,
359) -> Result<(), String> {
360 let controller = player.0.lock().await;
361 controller.report_html5_position(position, duration);
362 Ok(())
363}
364
365#[tauri::command]
367#[specta::specta]
368pub async fn player_report_media_loaded(
369 player: State<'_, PlayerStateWrapper>,
370 duration: f64,
371) -> Result<(), String> {
372 let controller = player.0.lock().await;
373 controller.report_html5_media_loaded(duration);
374 Ok(())
375}