use std::{
collections::HashMap,
io::{BufRead, BufReader, Cursor},
process::{Command, Stdio},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc, Mutex, RwLock, RwLockReadGuard,
},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use base64::{engine::general_purpose, Engine as _};
use flate2::read::GzDecoder;
use image::ImageReader;
use serde_json::Value;
use tar::Archive;
use tempfile::TempDir;
use crate::{Controller, ListenerToken, NowPlayingInfo, Subscription};
const ADAPTER_ASSET: &[u8] = include_bytes!("../../assets/mediaremote-adapter.tar.gz");
pub struct NowPlayingPerl {
info: Arc<RwLock<Option<NowPlayingInfo>>>,
listeners: Arc<
Mutex<
HashMap<
ListenerToken,
Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
>,
>,
>,
token_counter: Arc<AtomicU64>,
_temp_dir: Arc<TempDir>,
running: Arc<AtomicBool>,
}
impl NowPlayingPerl {
pub fn new() -> Self {
let temp_dir = tempfile::Builder::new()
.prefix("mediaremote-adapter")
.tempdir()
.expect("Failed to create temporary directory");
let tar = GzDecoder::new(Cursor::new(ADAPTER_ASSET));
let mut archive = Archive::new(tar);
archive
.unpack(temp_dir.path())
.expect("Failed to unpack adapter assets");
let adapter_script = temp_dir.path().join("mediaremote-adapter.pl");
let framework_path = temp_dir.path().join("MediaRemoteAdapter.framework");
let info = Arc::new(RwLock::new(None));
let listeners = Arc::new(Mutex::new(HashMap::new()));
let token_counter = Arc::new(AtomicU64::new(0));
let running = Arc::new(AtomicBool::new(true));
let info_clone = info.clone();
let listeners_clone = listeners.clone();
let running_clone = running.clone();
thread::spawn(move || {
let mut child = Command::new("/usr/bin/perl")
.arg(&adapter_script)
.arg(&framework_path)
.arg("stream")
.arg("--no-diff")
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start mediaremote-adapter");
let stdout = child.stdout.take().expect("Failed to capture stdout");
let reader = BufReader::new(stdout);
for line in reader.lines() {
if !running_clone.load(Ordering::Relaxed) {
break;
}
if let Ok(line) = line {
if let Ok(json) = serde_json::from_str::<Value>(&line) {
if let Some(payload) = json.get("payload") {
Self::update_info(&info_clone, &listeners_clone, payload);
}
}
}
}
let _ = child.kill();
});
Self {
info,
listeners,
token_counter,
_temp_dir: Arc::new(temp_dir),
running,
}
}
fn update_info(
info: &Arc<RwLock<Option<NowPlayingInfo>>>,
listeners: &Arc<
Mutex<
HashMap<
ListenerToken,
Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
>,
>,
>,
payload: &Value,
) {
let mut new_info = NowPlayingInfo {
is_playing: payload["playing"].as_bool(),
title: payload["title"].as_str().map(|s| s.to_string()),
artist: payload["artist"].as_str().map(|s| s.to_string()),
album: payload["album"].as_str().map(|s| s.to_string()),
album_cover: None,
elapsed_time: payload["elapsedTime"].as_f64(),
duration: payload["duration"].as_f64(),
info_update_time: payload["timestamp"]
.as_f64()
.map(|ts| UNIX_EPOCH + Duration::from_secs_f64(ts)),
bundle_id: payload["bundleIdentifier"].as_str().map(|s| s.to_string()),
bundle_name: None,
bundle_icon: None,
};
if let Some(artwork_base64) = payload["artworkData"].as_str() {
let clean_base64 = artwork_base64.replace("\n", "");
if let Ok(data) = general_purpose::STANDARD.decode(&clean_base64) {
new_info.album_cover = ImageReader::new(Cursor::new(data))
.with_guessed_format()
.ok()
.and_then(|img| img.decode().ok());
}
}
if let Some(bundle_id) = &new_info.bundle_id {
if let Some(bundle_info) = crate::get_bundle_info(bundle_id) {
new_info.bundle_name = Some(bundle_info.name);
new_info.bundle_icon = Some(bundle_info.icon);
}
}
{
let mut info_guard = info.write().unwrap();
*info_guard = Some(new_info);
}
for (_, listener) in listeners.lock().unwrap().iter() {
listener(info.read().unwrap());
}
}
pub fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
let mut info_guard = self.info.write().unwrap();
if let Some(ref mut info) = *info_guard {
if info.is_playing == Some(true) {
if let (Some(elapsed), Some(update_time)) =
(info.elapsed_time, info.info_update_time)
{
if let Ok(duration) = SystemTime::now().duration_since(update_time) {
info.elapsed_time = Some(elapsed + duration.as_secs_f64());
info.info_update_time = Some(SystemTime::now());
}
}
}
}
drop(info_guard);
self.info.read().unwrap()
}
}
impl Drop for NowPlayingPerl {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
impl Controller for NowPlayingPerl {
fn is_info_some(&self) -> bool {
self.info.read().unwrap().as_ref().is_some()
}
}
impl Subscription for NowPlayingPerl {
fn get_info(&self) -> RwLockReadGuard<'_, Option<NowPlayingInfo>> {
self.get_info()
}
fn get_token_counter(&self) -> Arc<AtomicU64> {
self.token_counter.clone()
}
fn get_listeners(
&self,
) -> Arc<
Mutex<
HashMap<
crate::high_level::subscription::ListenerToken,
Box<dyn Fn(RwLockReadGuard<'_, Option<NowPlayingInfo>>) + Send + Sync>,
>,
>,
> {
self.listeners.clone()
}
}