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