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 info_update_time: raw["info"]["kMRMediaRemoteNowPlayingInfoTimestamp"]
42 .as_u64()
43 .and_then(|t| Some(SystemTime::UNIX_EPOCH + Duration::from_millis(t)))
44 .or(Some(SystemTime::now())),
45 bundle_id: bundle_id.map(|b| b.to_string()),
46 bundle_name: bundle_info.as_ref().map(|b| b.name.clone()),
47 #[cfg(feature = "artwork")]
48 bundle_icon: bundle_info.map(|b| b.icon),
49 })
50}
51
52pub struct NowPlayingJXA {
53 info: Arc<RwLock<Option<NowPlayingInfo>>>,
54 listeners: Arc<
55 Mutex<
56 HashMap<
57 ListenerToken,
58 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
59 >,
60 >,
61 >,
62 token_counter: Arc<AtomicU64>,
63 stop_flag: Arc<AtomicBool>,
64 handle: Option<JoinHandle<()>>,
65}
66
67impl NowPlayingJXA {
68 fn update(&mut self, update_interval: Duration) {
69 let info_clone = Arc::clone(&self.info);
70 let stop_clone = Arc::clone(&self.stop_flag);
71 let listeners = Arc::clone(&self.listeners);
72
73 self.handle = Some(thread::spawn(move || {
74 while !stop_clone.load(Ordering::Relaxed) {
75 thread::sleep(update_interval);
76 if let Some(new_info) = get_info() {
77 let mut current = info_clone.write().unwrap();
78 if current.as_ref() != Some(&new_info) {
79 *current = Some(new_info);
80 drop(current);
81
82 for (_, listener) in listeners.clone().lock().unwrap().iter() {
83 listener(info_clone.read().unwrap());
84 }
85 }
86 }
87 }
88 }));
89 }
90
91 pub fn new(update_interval: Duration) -> Self {
107 let mut new_instance = NowPlayingJXA {
108 info: Arc::new(RwLock::new(get_info())),
109 listeners: Arc::new(Mutex::new(HashMap::new())),
110 token_counter: Arc::new(AtomicU64::new(0)),
111 stop_flag: Arc::new(AtomicBool::new(false)),
112 handle: None,
113 };
114
115 new_instance.update(update_interval);
116
117 new_instance
118 }
119
120 pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
146 let mut info_guard = self.info.write().unwrap();
147 let info = info_guard.as_mut();
148
149 if info.is_some() {
150 let info = info.unwrap();
151 if info.is_playing.is_some_and(|x| x)
152 && info.elapsed_time.is_some()
153 && info.info_update_time.is_some()
154 {
155 info.elapsed_time = Some(
156 info.elapsed_time.unwrap()
157 + info.info_update_time.unwrap().elapsed().unwrap().as_secs() as f64,
158 );
159 info.info_update_time = Some(SystemTime::now())
160 }
161 }
162
163 drop(info_guard);
164
165 self.info.read().unwrap()
166 }
167}
168
169impl Drop for NowPlayingJXA {
170 fn drop(&mut self) {
171 self.stop_flag.store(true, Ordering::Relaxed);
172 if let Some(handle) = self.handle.take() {
173 let _ = handle.join();
174 }
175 }
176}
177
178impl Controller for NowPlayingJXA {
179 fn is_info_some(&self) -> bool {
180 self.info.read().unwrap().as_ref().is_some()
181 }
182}
183
184impl Subscription for NowPlayingJXA {
185 fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
186 self.get_info()
187 }
188
189 fn get_token_counter(&self) -> Arc<AtomicU64> {
190 self.token_counter.clone()
191 }
192
193 fn get_listeners(
194 &self,
195 ) -> Arc<
196 Mutex<
197 HashMap<
198 super::subscription::ListenerToken,
199 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
200 >,
201 >,
202 > {
203 self.listeners.clone()
204 }
205}