Skip to main content

media_remote/high_level/
now_playing_jxa.rs

1use 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    /// Creates a new instance of `NowPlayingJXA` and registers for playback notifications.
93    ///
94    /// This function initializes a new `NowPlayingJXA` object, sets up necessary observers,
95    /// and ensures that media metadata is updated upon creation.
96    ///
97    /// # Returns
98    /// - `NowPlayingJXA`: A new instance of the `NowPlayingJXA` struct.
99    ///
100    /// # Example
101    /// ```rust
102    /// use media_remote::NowPlayingJXA;
103    /// use std::time::Duration;
104    ///
105    /// let now_playing = NowPlayingJXA::new(Duration::from_secs(3));
106    /// ```
107    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    /// Retrieves the latest now playing information.
122    ///
123    /// This function provides a read-locked view of the current playing media metadata.
124    ///
125    /// # Note
126    /// - The lock should be released as soon as possible to minimize blocking time.
127    ///
128    /// # Returns
129    /// - `RwLockReadGuard<'_, Option<NowPlayingInfo>>`: A guard to the now playing metadata.
130    ///
131    /// # Example
132    /// ```rust
133    /// use media_remote::NowPlayingJXA;
134    /// use std::time::Duration;
135    ///
136    /// let now_playing = NowPlayingJXA::new(Duration::from_secs(3));
137    /// let guard = now_playing.get_info();
138    /// let info = guard.as_ref();
139    ///
140    /// if let Some(info) = info {
141    ///     println!("Currently playing: {:?}", info.title);
142    /// }
143    ///
144    /// drop(guard);
145    /// ```
146    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}