1use std::{
2 collections::HashMap,
3 io::{BufRead, BufReader, Cursor},
4 path::PathBuf,
5 process::{Command, Stdio},
6 sync::{
7 atomic::{AtomicBool, AtomicU64, Ordering},
8 Arc, Mutex, RwLock, RwLockReadGuard,
9 },
10 thread,
11 time::{Duration, SystemTime, UNIX_EPOCH},
12};
13
14#[cfg(feature = "artwork")]
15use base64::{engine::general_purpose, Engine as _};
16use flate2::read::GzDecoder;
17#[cfg(feature = "artwork")]
18use image::ImageReader;
19use serde_json::Value;
20use tar::Archive;
21use tempfile::TempDir;
22
23use crate::{Command as MediaCommand, Controller, ListenerToken, NowPlayingInfo, Subscription};
24
25const ADAPTER_ASSET: &[u8] = include_bytes!("../../assets/mediaremote-adapter.tar.gz");
26
27pub struct NowPlayingPerl {
28 info: Arc<RwLock<Option<NowPlayingInfo>>>,
29 listeners: Arc<
30 Mutex<
31 HashMap<
32 ListenerToken,
33 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
34 >,
35 >,
36 >,
37 token_counter: Arc<AtomicU64>,
38 _temp_dir: Arc<TempDir>,
39 running: Arc<AtomicBool>,
40 adapter_script: PathBuf,
41 framework_path: PathBuf,
42}
43
44impl NowPlayingPerl {
45 pub fn new() -> Self {
46 let temp_dir = tempfile::Builder::new()
47 .prefix("mediaremote-adapter")
48 .tempdir()
49 .expect("Failed to create temporary directory");
50
51 let tar = GzDecoder::new(Cursor::new(ADAPTER_ASSET));
52 let mut archive = Archive::new(tar);
53 archive
54 .unpack(temp_dir.path())
55 .expect("Failed to unpack adapter assets");
56
57 let adapter_script = temp_dir.path().join("mediaremote-adapter.pl");
58 let framework_path = temp_dir.path().join("MediaRemoteAdapter.framework");
59
60 let adapter_script_thread = adapter_script.clone();
62 let framework_path_thread = framework_path.clone();
63
64 let info = Arc::new(RwLock::new(None));
65 let listeners = Arc::new(Mutex::new(HashMap::new()));
66 let token_counter = Arc::new(AtomicU64::new(0));
67 let running = Arc::new(AtomicBool::new(true));
68
69 let info_clone = info.clone();
70 let listeners_clone = listeners.clone();
71 let running_clone = running.clone();
72
73 thread::spawn(move || {
75 let mut command = Command::new("/usr/bin/perl");
76 command
77 .arg(&adapter_script_thread)
78 .arg(&framework_path_thread)
79 .arg("stream")
80 .arg("--no-diff");
81
82 #[cfg(not(feature = "artwork"))]
83 command.arg("--no-artwork");
84
85 let mut child = command
86 .stdout(Stdio::piped())
87 .stderr(Stdio::null())
88 .spawn()
89 .expect("Failed to start mediaremote-adapter");
90
91 let stdout = child.stdout.take().expect("Failed to capture stdout");
92 let reader = BufReader::new(stdout);
93
94 for line in reader.lines() {
95 if !running_clone.load(Ordering::Relaxed) {
96 break;
97 }
98
99 if let Ok(line) = line {
100 if let Ok(json) = serde_json::from_str::<Value>(&line) {
101 if let Some(payload) = json.get("payload") {
102 Self::update_info(&info_clone, &listeners_clone, payload);
103 }
104 }
105 }
106 }
107
108 let _ = child.kill();
109 });
110
111 Self {
112 info,
113 listeners,
114 token_counter,
115 _temp_dir: Arc::new(temp_dir),
116 running,
117 adapter_script,
118 framework_path,
119 }
120 }
121
122 fn update_info(
123 info: &Arc<RwLock<Option<NowPlayingInfo>>>,
124 listeners: &Arc<
125 Mutex<
126 HashMap<
127 ListenerToken,
128 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
129 >,
130 >,
131 >,
132 payload: &Value,
133 ) {
134 let mut new_info = NowPlayingInfo {
135 is_playing: payload["playing"].as_bool(),
136 title: payload["title"].as_str().map(|s| s.to_string()),
137 artist: payload["artist"].as_str().map(|s| s.to_string()),
138 album: payload["album"].as_str().map(|s| s.to_string()),
139 #[cfg(feature = "artwork")]
140 album_cover: None,
141 elapsed_time: payload["elapsedTime"].as_f64(),
142 duration: payload["duration"].as_f64(),
143 playback_rate: payload["playbackRate"].as_f64(),
144 info_update_time: payload["timestamp"]
145 .as_str()
146 .and_then(|s| speedate::DateTime::parse_str(s).ok())
147 .and_then(|dt| {
148 u64::try_from(dt.timestamp())
149 .ok()
150 .and_then(|secs| UNIX_EPOCH.checked_add(Duration::from_secs(secs)))
151 })
152 .or(Some(SystemTime::now())),
153 bundle_id: {
154 let mut bid = payload["parentApplicationBundleIdentifier"].as_str();
155 if bid.is_none() {
156 bid = payload["bundleIdentifier"].as_str();
157 }
158 bid.map(|s| s.to_string())
159 },
160 bundle_name: None,
161 #[cfg(feature = "artwork")]
162 bundle_icon: None,
163 };
164
165 #[cfg(feature = "artwork")]
167 if let Some(artwork_base64) = payload["artworkData"].as_str() {
168 let clean_base64 = artwork_base64.replace("\n", "");
170 if let Ok(data) = general_purpose::STANDARD.decode(&clean_base64) {
171 new_info.album_cover = ImageReader::new(Cursor::new(data))
172 .with_guessed_format()
173 .ok()
174 .and_then(|img| img.decode().ok());
175 }
176 }
177
178 if let Some(bundle_id) = &new_info.bundle_id {
179 if let Some(bundle_info) = crate::get_bundle_info(bundle_id) {
180 new_info.bundle_name = Some(bundle_info.name);
181 #[cfg(feature = "artwork")]
182 {
183 new_info.bundle_icon = Some(bundle_info.icon);
184 }
185 }
186 }
187
188 {
189 let mut info_guard = info.write().unwrap();
190 *info_guard = Some(new_info);
191 }
192
193 for (_, listener) in listeners.lock().unwrap().iter() {
195 listener(info.read().unwrap());
196 }
197 }
198
199 pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
200 let mut info_guard = self.info.write().unwrap();
201
202 if let Some(ref mut info) = *info_guard {
204 if info.is_playing == Some(true) {
205 if let (Some(elapsed), Some(update_time)) =
206 (info.elapsed_time, info.info_update_time)
207 {
208 if let Ok(duration) = SystemTime::now().duration_since(update_time) {
209 info.elapsed_time = Some(elapsed + duration.as_secs_f64());
210 info.info_update_time = Some(SystemTime::now());
211 }
212 }
213 }
214 }
215 drop(info_guard);
216
217 self.info.read().unwrap()
218 }
219}
220
221impl Drop for NowPlayingPerl {
222 fn drop(&mut self) {
223 self.running.store(false, Ordering::Relaxed);
224 }
225}
226
227impl NowPlayingPerl {
228 fn run_send(&self, command: MediaCommand) -> bool {
231 let status = Command::new("/usr/bin/perl")
232 .arg(&self.adapter_script)
233 .arg(&self.framework_path)
234 .arg("send")
235 .arg((command as i32).to_string())
236 .stdin(Stdio::null())
237 .stdout(Stdio::null())
238 .stderr(Stdio::null())
239 .status();
240 matches!(status, Ok(s) if s.success())
241 }
242
243 fn run_seek(&self, position_micros: u64) -> bool {
245 let status = Command::new("/usr/bin/perl")
246 .arg(&self.adapter_script)
247 .arg(&self.framework_path)
248 .arg("seek")
249 .arg(position_micros.to_string())
250 .stdin(Stdio::null())
251 .stdout(Stdio::null())
252 .stderr(Stdio::null())
253 .status();
254 matches!(status, Ok(s) if s.success())
255 }
256
257 fn run_speed(&self, speed: i32) -> bool {
259 let status = Command::new("/usr/bin/perl")
260 .arg(&self.adapter_script)
261 .arg(&self.framework_path)
262 .arg("speed")
263 .arg(speed.to_string())
264 .stdin(Stdio::null())
265 .stdout(Stdio::null())
266 .stderr(Stdio::null())
267 .status();
268 matches!(status, Ok(s) if s.success())
269 }
270}
271
272impl Controller for NowPlayingPerl {
273 fn is_info_some(&self) -> bool {
274 self.info.read().unwrap().as_ref().is_some()
275 }
276
277 fn toggle(&self) -> bool {
278 self.run_send(MediaCommand::TogglePlayPause)
279 }
280
281 fn play(&self) -> bool {
282 self.run_send(MediaCommand::Play)
283 }
284
285 fn pause(&self) -> bool {
286 self.run_send(MediaCommand::Pause)
287 }
288
289 fn next(&self) -> bool {
290 self.run_send(MediaCommand::NextTrack)
291 }
292
293 fn previous(&self) -> bool {
294 self.run_send(MediaCommand::PreviousTrack)
295 }
296
297 fn toggle_shuffle(&self) -> bool {
298 self.run_send(MediaCommand::ToggleShuffle)
299 }
300
301 fn toggle_repeat(&self) -> bool {
302 self.run_send(MediaCommand::ToggleRepeat)
303 }
304
305 fn start_forward_seek(&self) -> bool {
306 self.run_send(MediaCommand::StartForwardSeek)
307 }
308
309 fn end_forward_seek(&self) -> bool {
310 self.run_send(MediaCommand::EndForwardSeek)
311 }
312
313 fn start_backward_seek(&self) -> bool {
314 self.run_send(MediaCommand::StartBackwardSeek)
315 }
316
317 fn end_backward_seek(&self) -> bool {
318 self.run_send(MediaCommand::EndBackwardSeek)
319 }
320
321 fn go_back_fifteen_seconds(&self) -> bool {
322 self.run_send(MediaCommand::GoBackFifteenSeconds)
323 }
324
325 fn skip_fifteen_seconds(&self) -> bool {
326 self.run_send(MediaCommand::SkipFifteenSeconds)
327 }
328
329 fn set_playback_speed(&self, speed: i32) {
330 self.run_speed(speed);
331 }
332
333 fn set_elapsed_time(&self, elapsed_time: f64) {
334 let position_micros = (elapsed_time.max(0.0) * 1_000_000.0) as u64;
336 self.run_seek(position_micros);
337 }
338}
339
340impl Subscription for NowPlayingPerl {
341 fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
342 self.get_info()
343 }
344
345 fn get_token_counter(&self) -> Arc<AtomicU64> {
346 self.token_counter.clone()
347 }
348
349 fn get_listeners(
350 &self,
351 ) -> Arc<
352 Mutex<
353 HashMap<
354 crate::high_level::subscription::ListenerToken,
355 Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
356 >,
357 >,
358 > {
359 self.listeners.clone()
360 }
361}