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