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 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 #[cfg(feature = "artwork")]
152 if let Some(artwork_base64) = payload["artworkData"].as_str() {
153 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 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 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}