media_remote/high_level/
now_playing_jxa.rs1use std::{
2 collections::HashMap,
3 sync::{
4 atomic::{AtomicBool, AtomicU64, Ordering},
5 Arc, Mutex, RwLock, RwLockReadGuard,
6 },
7 thread::{self, JoinHandle},
8 time::{Duration, SystemTime},
9};
10
11use crate::{get_bundle_info, get_raw_info, ListenerToken, NowPlayingInfo, Subscription};
12
13use super::controller::Controller;
14
15pub fn get_info() -> Option<NowPlayingInfo> {
16 let raw = get_raw_info()?;
17
18 let mut bundle_id = raw["client"]["parentApplicationBundleIdentifier"].as_str();
19 if bundle_id.is_none() {
20 bundle_id = raw["client"]["bundleIdentifier"].as_str();
21 }
22 let bundle_id = bundle_id;
23
24 let bundle_info = bundle_id.and_then(|bid| get_bundle_info(bid));
25
26 Some(NowPlayingInfo {
27 is_playing: raw["isPlaying"].as_bool(),
28 title: raw["info"]["kMRMediaRemoteNowPlayingInfoTitle"]
29 .as_str()
30 .map(|s| s.to_string()),
31 artist: raw["info"]["kMRMediaRemoteNowPlayingInfoArtist"]
32 .as_str()
33 .map(|s| s.to_string()),
34 album: raw["info"]["kMRMediaRemoteNowPlayingInfoAlbum"]
35 .as_str()
36 .map(|s| s.to_string()),
37 #[cfg(feature = "artwork")]
38 album_cover: None,
39 elapsed_time: raw["info"]["kMRMediaRemoteNowPlayingInfoElapsedTime"].as_f64(),
40 duration: raw["info"]["kMRMediaRemoteNowPlayingInfoDuration"].as_f64(),
41 playback_rate: raw["info"]["kMRMediaRemoteNowPlayingInfoPlaybackRate"].as_f64(),
42 info_update_time: raw["info"]["kMRMediaRemoteNowPlayingInfoTimestamp"]
43 .as_u64()
44 .and_then(|t| Some(SystemTime::UNIX_EPOCH + Duration::from_millis(t)))
45 .or(Some(SystemTime::now())),
46 bundle_id: bundle_id.map(|b| b.to_string()),
47 bundle_name: bundle_info.as_ref().map(|b| b.name.clone()),
48 #[cfg(feature = "artwork")]
49 bundle_icon: bundle_info.map(|b| b.icon),
50 })
51}
52
53pub struct NowPlayingJXA {
54 info: Arc<RwLock<Option<NowPlayingInfo>>>,
55 listeners: Arc<
56 Mutex<
57 HashMap<
58 ListenerToken,
59 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
60 >,
61 >,
62 >,
63 token_counter: Arc<AtomicU64>,
64 stop_flag: Arc<AtomicBool>,
65 handle: Option<JoinHandle<()>>,
66}
67
68impl NowPlayingJXA {
69 fn update(&mut self, update_interval: Duration) {
70 let info_clone = Arc::clone(&self.info);
71 let stop_clone = Arc::clone(&self.stop_flag);
72 let listeners = Arc::clone(&self.listeners);
73
74 self.handle = Some(thread::spawn(move || {
75 while !stop_clone.load(Ordering::Relaxed) {
76 thread::sleep(update_interval);
77 if let Some(new_info) = get_info() {
78 let mut current = info_clone.write().unwrap();
79 if current.as_ref() != Some(&new_info) {
80 *current = Some(new_info);
81 drop(current);
82
83 for (_, listener) in listeners.clone().lock().unwrap().iter() {
84 listener(info_clone.read().unwrap());
85 }
86 }
87 }
88 }
89 }));
90 }
91
92 pub fn new(update_interval: Duration) -> Self {
108 let mut new_instance = NowPlayingJXA {
109 info: Arc::new(RwLock::new(get_info())),
110 listeners: Arc::new(Mutex::new(HashMap::new())),
111 token_counter: Arc::new(AtomicU64::new(0)),
112 stop_flag: Arc::new(AtomicBool::new(false)),
113 handle: None,
114 };
115
116 new_instance.update(update_interval);
117
118 new_instance
119 }
120
121 pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
147 let mut info_guard = self.info.write().unwrap();
148 let info = info_guard.as_mut();
149
150 if info.is_some() {
151 let info = info.unwrap();
152 if info.is_playing.is_some_and(|x| x)
153 && info.elapsed_time.is_some()
154 && info.info_update_time.is_some()
155 {
156 info.elapsed_time = Some(
157 info.elapsed_time.unwrap()
158 + info.info_update_time.unwrap().elapsed().unwrap().as_secs() as f64,
159 );
160 info.info_update_time = Some(SystemTime::now())
161 }
162 }
163
164 drop(info_guard);
165
166 self.info.read().unwrap()
167 }
168}
169
170impl Drop for NowPlayingJXA {
171 fn drop(&mut self) {
172 self.stop_flag.store(true, Ordering::Relaxed);
173 if let Some(handle) = self.handle.take() {
174 let _ = handle.join();
175 }
176 }
177}
178
179impl Controller for NowPlayingJXA {
180 fn is_info_some(&self) -> bool {
181 self.info.read().unwrap().as_ref().is_some()
182 }
183}
184
185impl Subscription for NowPlayingJXA {
186 fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
187 self.get_info()
188 }
189
190 fn get_token_counter(&self) -> Arc<AtomicU64> {
191 self.token_counter.clone()
192 }
193
194 fn get_listeners(
195 &self,
196 ) -> Arc<
197 Mutex<
198 HashMap<
199 super::subscription::ListenerToken,
200 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
201 >,
202 >,
203 > {
204 self.listeners.clone()
205 }
206}