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
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 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 if let Some(artwork_base64) = payload["artworkData"].as_str() {
135 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 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 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}