Skip to main content

media_remote/high_level/
now_playing_perl.rs

1use std::{
2    collections::HashMap,
3    io::{BufRead, BufReader, Cursor},
4    process::{Command, Stdio},
5    sync::{
6        atomic::{AtomicBool, AtomicU64, Ordering},
7        Arc, Mutex, RwLock, RwLockReadGuard,
8    },
9    thread,
10    time::{Duration, SystemTime, UNIX_EPOCH},
11};
12
13#[cfg(feature = "artwork")]
14use base64::{engine::general_purpose, Engine as _};
15use flate2::read::GzDecoder;
16#[cfg(feature = "artwork")]
17use image::ImageReader;
18use serde_json::Value;
19use tar::Archive;
20use tempfile::TempDir;
21
22use crate::{Controller, ListenerToken, NowPlayingInfo, Subscription};
23
24const ADAPTER_ASSET: &[u8] = include_bytes!("../../assets/mediaremote-adapter.tar.gz");
25
26pub struct NowPlayingPerl {
27    info: Arc<RwLock<Option<NowPlayingInfo>>>,
28    listeners: Arc<
29        Mutex<
30            HashMap<
31                ListenerToken,
32                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
33            >,
34        >,
35    >,
36    token_counter: Arc<AtomicU64>,
37    _temp_dir: Arc<TempDir>,
38    running: Arc<AtomicBool>,
39}
40
41impl NowPlayingPerl {
42    pub fn new() -> Self {
43        let temp_dir = tempfile::Builder::new()
44            .prefix("mediaremote-adapter")
45            .tempdir()
46            .expect("Failed to create temporary directory");
47
48        let tar = GzDecoder::new(Cursor::new(ADAPTER_ASSET));
49        let mut archive = Archive::new(tar);
50        archive
51            .unpack(temp_dir.path())
52            .expect("Failed to unpack adapter assets");
53
54        let adapter_script = temp_dir.path().join("mediaremote-adapter.pl");
55        let framework_path = temp_dir.path().join("MediaRemoteAdapter.framework");
56
57        let info = Arc::new(RwLock::new(None));
58        let listeners = Arc::new(Mutex::new(HashMap::new()));
59        let token_counter = Arc::new(AtomicU64::new(0));
60        let running = Arc::new(AtomicBool::new(true));
61
62        let info_clone = info.clone();
63        let listeners_clone = listeners.clone();
64        let running_clone = running.clone();
65
66        // Spawn reading thread
67        thread::spawn(move || {
68            let mut command = Command::new("/usr/bin/perl");
69            command
70                .arg(&adapter_script)
71                .arg(&framework_path)
72                .arg("stream")
73                .arg("--no-diff");
74
75            #[cfg(not(feature = "artwork"))]
76            command.arg("--no-artwork");
77
78            let mut child = command
79                .stdout(Stdio::piped())
80                .stderr(Stdio::null())
81                .spawn()
82                .expect("Failed to start mediaremote-adapter");
83
84            let stdout = child.stdout.take().expect("Failed to capture stdout");
85            let reader = BufReader::new(stdout);
86
87            for line in reader.lines() {
88                if !running_clone.load(Ordering::Relaxed) {
89                    break;
90                }
91
92                if let Ok(line) = line {
93                    if let Ok(json) = serde_json::from_str::<Value>(&line) {
94                        if let Some(payload) = json.get("payload") {
95                            Self::update_info(&info_clone, &listeners_clone, payload);
96                        }
97                    }
98                }
99            }
100
101            let _ = child.kill();
102        });
103
104        Self {
105            info,
106            listeners,
107            token_counter,
108            _temp_dir: Arc::new(temp_dir),
109            running,
110        }
111    }
112
113    fn update_info(
114        info: &Arc<RwLock<Option<NowPlayingInfo>>>,
115        listeners: &Arc<
116            Mutex<
117                HashMap<
118                    ListenerToken,
119                    Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
120                >,
121            >,
122        >,
123        payload: &Value,
124    ) {
125        let mut new_info = NowPlayingInfo {
126            is_playing: payload["playing"].as_bool(),
127            title: payload["title"].as_str().map(|s| s.to_string()),
128            artist: payload["artist"].as_str().map(|s| s.to_string()),
129            album: payload["album"].as_str().map(|s| s.to_string()),
130            #[cfg(feature = "artwork")]
131            album_cover: None,
132            elapsed_time: payload["elapsedTime"].as_f64(),
133            duration: payload["duration"].as_f64(),
134            playback_rate: payload["playbackRate"].as_f64(),
135            info_update_time: payload["timestamp"]
136                .as_str()
137                .and_then(|s| speedate::DateTime::parse_str(s).ok())
138                .and_then(|dt| {
139                    u64::try_from(dt.timestamp())
140                        .ok()
141                        .and_then(|secs| UNIX_EPOCH.checked_add(Duration::from_secs(secs)))
142                })
143                .or(Some(SystemTime::now())),
144            bundle_id: payload["bundleIdentifier"].as_str().map(|s| s.to_string()),
145            bundle_name: None,
146            #[cfg(feature = "artwork")]
147            bundle_icon: None,
148        };
149
150        // Handle artwork
151        #[cfg(feature = "artwork")]
152        if let Some(artwork_base64) = payload["artworkData"].as_str() {
153            // Clean up main string which might have newlines
154            let clean_base64 = artwork_base64.replace("\n", "");
155            if let Ok(data) = general_purpose::STANDARD.decode(&clean_base64) {
156                new_info.album_cover = ImageReader::new(Cursor::new(data))
157                    .with_guessed_format()
158                    .ok()
159                    .and_then(|img| img.decode().ok());
160            }
161        }
162
163        if let Some(bundle_id) = &new_info.bundle_id {
164            if let Some(bundle_info) = crate::get_bundle_info(bundle_id) {
165                new_info.bundle_name = Some(bundle_info.name);
166                #[cfg(feature = "artwork")]
167                {
168                    new_info.bundle_icon = Some(bundle_info.icon);
169                }
170            }
171        }
172
173        {
174            let mut info_guard = info.write().unwrap();
175            *info_guard = Some(new_info);
176        }
177
178        // Notify listeners
179        for (_, listener) in listeners.lock().unwrap().iter() {
180            listener(info.read().unwrap());
181        }
182    }
183
184    pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
185        let mut info_guard = self.info.write().unwrap();
186
187        // Logic to update elapsed time estimation if playing
188        if let Some(ref mut info) = *info_guard {
189            if info.is_playing == Some(true) {
190                if let (Some(elapsed), Some(update_time)) =
191                    (info.elapsed_time, info.info_update_time)
192                {
193                    if let Ok(duration) = SystemTime::now().duration_since(update_time) {
194                        info.elapsed_time = Some(elapsed + duration.as_secs_f64());
195                        info.info_update_time = Some(SystemTime::now());
196                    }
197                }
198            }
199        }
200        drop(info_guard);
201
202        self.info.read().unwrap()
203    }
204}
205
206impl Drop for NowPlayingPerl {
207    fn drop(&mut self) {
208        self.running.store(false, Ordering::Relaxed);
209    }
210}
211
212impl Controller for NowPlayingPerl {
213    fn is_info_some(&self) -> bool {
214        self.info.read().unwrap().as_ref().is_some()
215    }
216}
217
218impl Subscription for NowPlayingPerl {
219    fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
220        self.get_info()
221    }
222
223    fn get_token_counter(&self) -> Arc<AtomicU64> {
224        self.token_counter.clone()
225    }
226
227    fn get_listeners(
228        &self,
229    ) -> Arc<
230        Mutex<
231            HashMap<
232                crate::high_level::subscription::ListenerToken,
233                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
234            >,
235        >,
236    > {
237        self.listeners.clone()
238    }
239}