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            info_update_time: payload["timestamp"]
135                .as_f64()
136                .map(|ts| UNIX_EPOCH + Duration::from_secs_f64(ts)),
137            bundle_id: payload["bundleIdentifier"].as_str().map(|s| s.to_string()),
138            bundle_name: None,
139            #[cfg(feature = "artwork")]
140            bundle_icon: None,
141        };
142
143        // Handle artwork
144        #[cfg(feature = "artwork")]
145        if let Some(artwork_base64) = payload["artworkData"].as_str() {
146            // Clean up main string which might have newlines
147            let clean_base64 = artwork_base64.replace("\n", "");
148            if let Ok(data) = general_purpose::STANDARD.decode(&clean_base64) {
149                new_info.album_cover = ImageReader::new(Cursor::new(data))
150                    .with_guessed_format()
151                    .ok()
152                    .and_then(|img| img.decode().ok());
153            }
154        }
155
156        if let Some(bundle_id) = &new_info.bundle_id {
157            if let Some(bundle_info) = crate::get_bundle_info(bundle_id) {
158                new_info.bundle_name = Some(bundle_info.name);
159                #[cfg(feature = "artwork")]
160                {
161                    new_info.bundle_icon = Some(bundle_info.icon);
162                }
163            }
164        }
165
166        {
167            let mut info_guard = info.write().unwrap();
168            *info_guard = Some(new_info);
169        }
170
171        // Notify listeners
172        for (_, listener) in listeners.lock().unwrap().iter() {
173            listener(info.read().unwrap());
174        }
175    }
176
177    pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
178        let mut info_guard = self.info.write().unwrap();
179
180        // Logic to update elapsed time estimation if playing
181        if let Some(ref mut info) = *info_guard {
182            if info.is_playing == Some(true) {
183                if let (Some(elapsed), Some(update_time)) =
184                    (info.elapsed_time, info.info_update_time)
185                {
186                    if let Ok(duration) = SystemTime::now().duration_since(update_time) {
187                        info.elapsed_time = Some(elapsed + duration.as_secs_f64());
188                        info.info_update_time = Some(SystemTime::now());
189                    }
190                }
191            }
192        }
193        drop(info_guard);
194
195        self.info.read().unwrap()
196    }
197}
198
199impl Drop for NowPlayingPerl {
200    fn drop(&mut self) {
201        self.running.store(false, Ordering::Relaxed);
202    }
203}
204
205impl Controller for NowPlayingPerl {
206    fn is_info_some(&self) -> bool {
207        self.info.read().unwrap().as_ref().is_some()
208    }
209}
210
211impl Subscription for NowPlayingPerl {
212    fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
213        self.get_info()
214    }
215
216    fn get_token_counter(&self) -> Arc<AtomicU64> {
217        self.token_counter.clone()
218    }
219
220    fn get_listeners(
221        &self,
222    ) -> Arc<
223        Mutex<
224            HashMap<
225                crate::high_level::subscription::ListenerToken,
226                Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
227            >,
228        >,
229    > {
230        self.listeners.clone()
231    }
232}