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        album_cover: None,
38        elapsed_time: raw["info"]["kMRMediaRemoteNowPlayingInfoElapsedTime"].as_f64(),
39        duration: raw["info"]["kMRMediaRemoteNowPlayingInfoDuration"].as_f64(),
40        info_update_time: raw["info"]["kMRMediaRemoteNowPlayingInfoTimestamp"]
41            .as_u64()
42            .and_then(|t| Some(SystemTime::UNIX_EPOCH + Duration::from_millis(t)))
43            .or(Some(SystemTime::now())),
44        bundle_id: bundle_id.map(|b| b.to_string()),
45        bundle_name: bundle_info.as_ref().map(|b| b.name.clone()),
46        bundle_icon: bundle_info.map(|b| b.icon),
47    })
48}
49
50pub struct NowPlayingJXA {
51    info: Arc<RwLock<Option<NowPlayingInfo>>>,
52    listeners: Arc<
53        Mutex<
54            HashMap<
55                ListenerToken,
56                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
57            >,
58        >,
59    >,
60    token_counter: Arc<AtomicU64>,
61    stop_flag: Arc<AtomicBool>,
62    handle: Option<JoinHandle<()>>,
63}
64
65impl NowPlayingJXA {
66    fn update(&mut self, update_interval: Duration) {
67        let info_clone = Arc::clone(&self.info);
68        let stop_clone = Arc::clone(&self.stop_flag);
69        let listeners = Arc::clone(&self.listeners);
70
71        self.handle = Some(thread::spawn(move || {
72            while !stop_clone.load(Ordering::Relaxed) {
73                thread::sleep(update_interval);
74                if let Some(new_info) = get_info() {
75                    let mut current = info_clone.write().unwrap();
76                    if current.as_ref() != Some(&new_info) {
77                        *current = Some(new_info);
78                        drop(current);
79
80                        for (_, listener) in listeners.clone().lock().unwrap().iter() {
81                            listener(info_clone.read().unwrap());
82                        }
83                    }
84                }
85            }
86        }));
87    }
88
89    /// Creates a new instance of `NowPlayingJXA` and registers for playback notifications.
90    ///
91    /// This function initializes a new `NowPlayingJXA` object, sets up necessary observers,
92    /// and ensures that media metadata is updated upon creation.
93    ///
94    /// # Returns
95    /// - `NowPlayingJXA`: A new instance of the `NowPlayingJXA` struct.
96    ///
97    /// # Example
98    /// ```rust
99    /// use media_remote::NowPlayingJXA;
100    /// use std::time::Duration;
101    ///
102    /// let now_playing = NowPlayingJXA::new(Duration::from_secs(3));
103    /// ```
104    pub fn new(update_interval: Duration) -> Self {
105        let mut new_instance = NowPlayingJXA {
106            info: Arc::new(RwLock::new(get_info())),
107            listeners: Arc::new(Mutex::new(HashMap::new())),
108            token_counter: Arc::new(AtomicU64::new(0)),
109            stop_flag: Arc::new(AtomicBool::new(false)),
110            handle: None,
111        };
112
113        new_instance.update(update_interval);
114
115        new_instance
116    }
117
118    /// Retrieves the latest now playing information.
119    ///
120    /// This function provides a read-locked view of the current playing media metadata.
121    ///
122    /// # Note
123    /// - The lock should be released as soon as possible to minimize blocking time.
124    ///
125    /// # Returns
126    /// - `RwLockReadGuard<'_, Option<NowPlayingInfo>>`: A guard to the now playing metadata.
127    ///
128    /// # Example
129    /// ```rust
130    /// use media_remote::NowPlayingJXA;
131    /// use std::time::Duration;
132    ///
133    /// let now_playing = NowPlayingJXA::new(Duration::from_secs(3));
134    /// let guard = now_playing.get_info();
135    /// let info = guard.as_ref();
136    ///
137    /// if let Some(info) = info {
138    ///     println!("Currently playing: {:?}", info.title);
139    /// }
140    ///
141    /// drop(guard);
142    /// ```
143    pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
144        let mut info_guard = self.info.write().unwrap();
145        let info = info_guard.as_mut();
146
147        if info.is_some() {
148            let info = info.unwrap();
149            if info.is_playing.is_some_and(|x| x)
150                && info.elapsed_time.is_some()
151                && info.info_update_time.is_some()
152            {
153                info.elapsed_time = Some(
154                    info.elapsed_time.unwrap()
155                        + info.info_update_time.unwrap().elapsed().unwrap().as_secs() as f64,
156                );
157                info.info_update_time = Some(SystemTime::now())
158            }
159        }
160
161        drop(info_guard);
162
163        self.info.read().unwrap()
164    }
165}
166
167impl Drop for NowPlayingJXA {
168    fn drop(&mut self) {
169        self.stop_flag.store(true, Ordering::Relaxed);
170        if let Some(handle) = self.handle.take() {
171            let _ = handle.join();
172        }
173    }
174}
175
176impl Controller for NowPlayingJXA {
177    fn is_info_some(&self) -> bool {
178        self.info.read().unwrap().as_ref().is_some()
179    }
180}
181
182impl Subscription for NowPlayingJXA {
183    fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
184        self.get_info()
185    }
186
187    fn get_token_counter(&self) -> Arc<AtomicU64> {
188        self.token_counter.clone()
189    }
190
191    fn get_listeners(
192        &self,
193    ) -> Arc<
194        Mutex<
195            HashMap<
196                super::subscription::ListenerToken,
197                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
198            >,
199        >,
200    > {
201        self.listeners.clone()
202    }
203}