jellytau_lib/session_poller/
mod.rs1use crate::utils::lock::{MutexSafe, RwLockSafe};
9use log::{debug, info, warn};
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11use std::sync::{Arc, Mutex, RwLock};
12use std::thread;
13use std::time::Duration;
14
15use crate::jellyfin::JellyfinClient;
16use crate::playback_mode::{PlaybackMode, PlaybackModeManager};
17use crate::player::PlayerEventEmitter;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PollingHint {
22 CastActive,
24 CastDiscovery,
26 Normal,
28}
29
30pub struct SessionPollerManager {
32 jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
33 playback_mode_manager: Arc<PlaybackModeManager>,
34 event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
35 connectivity_reporter: Arc<Mutex<Option<crate::connectivity::ConnectivityReporter>>>,
42
43 is_running: Arc<AtomicBool>,
45 current_hint: Arc<RwLock<PollingHint>>,
46 current_interval_ms: Arc<AtomicU64>,
47
48 thread_handle: Arc<Mutex<Option<thread::JoinHandle<()>>>>,
50}
51
52impl SessionPollerManager {
53 pub fn new(
55 jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
56 playback_mode_manager: Arc<PlaybackModeManager>,
57 ) -> Self {
58 Self {
59 jellyfin_client,
60 playback_mode_manager,
61 event_emitter: Arc::new(Mutex::new(None)),
62 connectivity_reporter: Arc::new(Mutex::new(None)),
63 is_running: Arc::new(AtomicBool::new(false)),
64 current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
65 current_interval_ms: Arc::new(AtomicU64::new(10000)), thread_handle: Arc::new(Mutex::new(None)),
67 }
68 }
69
70 pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
72 *self.event_emitter.lock_safe() = Some(emitter);
73 }
74
75 pub fn set_connectivity_reporter(&self, reporter: crate::connectivity::ConnectivityReporter) {
79 *self.connectivity_reporter.lock_safe() = Some(reporter);
80 }
81
82 pub fn start(&self) {
84 if self.is_running.swap(true, Ordering::Relaxed) {
85 warn!("[SessionPoller] Already running, ignoring start request");
86 return;
87 }
88
89 info!("[SessionPoller] Starting session polling");
90
91 let client = self.jellyfin_client.clone();
93 let mode_manager = self.playback_mode_manager.clone();
94 let emitter = self.event_emitter.clone();
95 let connectivity_reporter = self.connectivity_reporter.clone();
96 let is_running = self.is_running.clone();
97 let hint = self.current_hint.clone();
98 let interval_ms = self.current_interval_ms.clone();
99
100 let handle = thread::spawn(move || {
101 let rt = tokio::runtime::Runtime::new().unwrap();
103
104 while is_running.load(Ordering::Relaxed) {
105 let new_interval =
107 Self::calculate_interval(&mode_manager.get_mode(), *hint.read_safe());
108
109 interval_ms.store(new_interval, Ordering::Relaxed);
110
111 debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
112
113 let (sessions_result, had_client) = rt.block_on(async {
117 let client_opt = client.lock_safe().clone();
118 match client_opt {
119 Some(c) => (c.get_sessions().await, true),
120 None => {
121 debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
122 (Ok(Vec::new()), false)
123 }
124 }
125 });
126
127 if had_client {
132 if let Some(reporter) = connectivity_reporter.lock_safe().clone() {
133 rt.block_on(async {
134 match &sessions_result {
135 Ok(_) => reporter.report_success().await,
136 Err(e) => reporter.report_network_failure(Some(e.clone())).await,
137 }
138 });
139 }
140 }
141
142 match sessions_result {
144 Ok(sessions) => {
145 debug!("[SessionPoller] Fetched {} sessions", sessions.len());
146
147 if let PlaybackMode::Remote { session_id } = mode_manager.get_mode() {
155 Self::push_remote_lockscreen(&sessions, &session_id);
156 }
157
158 if let Some(em) = emitter.lock_safe().as_ref() {
159 em.emit(crate::player::PlayerStatusEvent::SessionsUpdated { sessions });
160 }
161 }
162 Err(e) => {
163 warn!("[SessionPoller] Failed to fetch sessions: {}", e);
164 }
165 }
166
167 thread::sleep(Duration::from_millis(new_interval));
169 }
170
171 info!("[SessionPoller] Polling thread stopped");
172 });
173
174 *self.thread_handle.lock_safe() = Some(handle);
175 }
176
177 pub fn stop(&self) {
179 info!("[SessionPoller] Stopping session polling");
180 self.is_running.store(false, Ordering::Relaxed);
181
182 if let Some(handle) = self.thread_handle.lock_safe().take() {
184 let _ = handle.join();
185 }
186 }
187
188 pub fn set_polling_hint(&self, hint: PollingHint) {
190 debug!("[SessionPoller] Setting polling hint: {:?}", hint);
191 *self.current_hint.write_safe() = hint;
192 }
193
194 fn push_remote_lockscreen(sessions: &[crate::jellyfin::client::SessionInfo], session_id: &str) {
201 const TICKS_PER_MS: i64 = 10_000;
203
204 let Some(session) = sessions
205 .iter()
206 .find(|s| s.id.as_deref() == Some(session_id))
207 else {
208 return;
209 };
210
211 let Some(now_playing) = session.now_playing_item.as_ref() else {
212 return;
213 };
214
215 let title = now_playing.name.clone().unwrap_or_default();
216 let artist = now_playing
217 .artists
218 .as_ref()
219 .map(|a| a.join(", "))
220 .filter(|s| !s.is_empty())
221 .or_else(|| now_playing.album_artist.clone())
222 .unwrap_or_default();
223 let album = now_playing.album.clone();
224 let duration_ms = now_playing.run_time_ticks.unwrap_or(0) / TICKS_PER_MS;
225
226 let (position_ms, is_playing) = session
227 .play_state
228 .as_ref()
229 .map(|ps| {
230 (
231 ps.position_ticks.unwrap_or(0) / TICKS_PER_MS,
232 !ps.is_paused.unwrap_or(false),
233 )
234 })
235 .unwrap_or((0, false));
236
237 let meta = crate::player::LockscreenMetadata {
238 title,
239 artist,
240 album,
241 duration_ms,
242 position_ms,
243 is_playing,
244 };
245
246 if let Err(e) = crate::player::update_lockscreen_metadata(&meta) {
247 warn!(
248 "[SessionPoller] Failed to update lockscreen metadata: {}",
249 e
250 );
251 }
252 }
253
254 fn calculate_interval(mode: &PlaybackMode, hint: PollingHint) -> u64 {
256 match hint {
257 PollingHint::CastActive => 500, PollingHint::CastDiscovery => 15000, PollingHint::Normal => {
260 match mode {
261 PlaybackMode::Remote { .. } => 2000, PlaybackMode::Local | PlaybackMode::Idle => 10000, }
264 }
265 }
266 }
267
268 pub async fn poll_now(&self) -> Result<Vec<crate::jellyfin::client::SessionInfo>, String> {
270 let client = self
271 .jellyfin_client
272 .lock_safe()
273 .clone()
274 .ok_or("Jellyfin client not configured")?;
275
276 client.get_sessions().await
277 }
278}
279
280impl Drop for SessionPollerManager {
281 fn drop(&mut self) {
282 self.stop();
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
292 fn test_calculate_interval() {
293 assert_eq!(
295 SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::CastActive),
296 500
297 );
298 assert_eq!(
299 SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::CastActive),
300 500
301 );
302 assert_eq!(
303 SessionPollerManager::calculate_interval(
304 &PlaybackMode::Remote {
305 session_id: "test".to_string()
306 },
307 PollingHint::CastActive
308 ),
309 500
310 );
311
312 assert_eq!(
314 SessionPollerManager::calculate_interval(
315 &PlaybackMode::Idle,
316 PollingHint::CastDiscovery
317 ),
318 15000
319 );
320 assert_eq!(
321 SessionPollerManager::calculate_interval(
322 &PlaybackMode::Local,
323 PollingHint::CastDiscovery
324 ),
325 15000
326 );
327 assert_eq!(
328 SessionPollerManager::calculate_interval(
329 &PlaybackMode::Remote {
330 session_id: "test".to_string()
331 },
332 PollingHint::CastDiscovery
333 ),
334 15000
335 );
336
337 assert_eq!(
340 SessionPollerManager::calculate_interval(&PlaybackMode::Idle, PollingHint::Normal),
341 10000
342 );
343 assert_eq!(
344 SessionPollerManager::calculate_interval(&PlaybackMode::Local, PollingHint::Normal),
345 10000
346 );
347 assert_eq!(
349 SessionPollerManager::calculate_interval(
350 &PlaybackMode::Remote {
351 session_id: "test".to_string()
352 },
353 PollingHint::Normal
354 ),
355 2000
356 );
357 }
358
359 #[test]
361 fn test_polling_hint_equality() {
362 assert_eq!(PollingHint::Normal, PollingHint::Normal);
363 assert_eq!(PollingHint::CastActive, PollingHint::CastActive);
364 assert_eq!(PollingHint::CastDiscovery, PollingHint::CastDiscovery);
365
366 assert_ne!(PollingHint::Normal, PollingHint::CastActive);
367 assert_ne!(PollingHint::CastActive, PollingHint::CastDiscovery);
368 }
369}