media_remote/high_level/
now_playing_perl.rs1use 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 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 #[cfg(feature = "artwork")]
145 if let Some(artwork_base64) = payload["artworkData"].as_str() {
146 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 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 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}