use anyhow::Result;
use reqwest::Client;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::process::{Command, Child};
use std::path::Path;
use base64::{Engine as _, engine::general_purpose};
use rand::Rng;
use crate::types::{MediaSource, PlayerState, PlaybackState, ConnectionStatus};
use crate::utils;
pub struct VlcController {
client: Client,
base_url: String,
auth_header: Option<String>,
state: Arc<Mutex<PlayerState>>,
_host: String,
_port: u16,
_password: String,
vlc_process: Option<Child>,
}
impl VlcController {
pub fn new(host: Option<&str>, port: Option<u16>) -> Result<Self> {
let host = host.unwrap_or("127.0.0.1").to_string();
let port = port.unwrap_or(8080);
let base_url = format!("http://{}:{}", host, port);
let password = Self::generate_password();
let vlc_path = utils::find_vlc_executable()?;
let vlc_process = Self::start_vlc(&vlc_path, &password, port)?;
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()?;
let connection_status = ConnectionStatus {
host: host.clone(),
port,
password_set: true,
password: password.clone(),
..Default::default()
};
let state = PlayerState {
connection_status,
..Default::default()
};
let credentials = format!(":{}", password);
let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
let auth_header = Some(format!("Basic {}", encoded));
let controller = Self {
client,
base_url,
auth_header,
state: Arc::new(Mutex::new(state)),
_host: host,
_port: port,
_password: password,
vlc_process: Some(vlc_process),
};
std::thread::sleep(Duration::from_millis(2000));
Ok(controller)
}
fn generate_password() -> String {
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const PASSWORD_LEN: usize = 16;
let mut rng = rand::thread_rng();
(0..PASSWORD_LEN)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}
fn start_vlc(vlc_path: &Path, password: &str, port: u16) -> Result<Child> {
let child = Command::new(vlc_path)
.args([
"--intf", "http",
"--http-password", password,
"--http-port", &port.to_string(),
"--http-host", "127.0.0.1",
"--extraintf", "dummy",
"--no-video-title-show",
"--quiet",
"--fullscreen",
"--loop"
])
.spawn()
.map_err(|e| anyhow::anyhow!("Failed to start VLC: {}", e))?;
Ok(child)
}
pub async fn test_connection(&mut self) -> Result<bool> {
const MAX_RETRIES: u32 = 3;
let mut last_error = None;
for attempt in 1..=MAX_RETRIES {
let url = format!("{}/requests/status.xml", self.base_url);
let mut request = self.client.get(&url);
if let Some(ref auth) = self.auth_header {
request = request.header("Authorization", auth);
}
match request.send().await {
Ok(resp) => {
let success = resp.status().is_success();
{
let mut state = self.state.lock().unwrap();
state.connection_status.connected = success;
if success {
state.connection_status.last_error = None;
} else {
state.connection_status.last_error = Some(format!("HTTP status: {}", resp.status()));
}
}
if success {
return Ok(true);
} else {
last_error = Some(anyhow::anyhow!("HTTP status: {}", resp.status()));
}
}
Err(e) => {
last_error = Some(anyhow::anyhow!("Connection failed: {}", e));
}
}
if attempt < MAX_RETRIES {
tokio::time::sleep(Duration::from_millis(200 * attempt as u64)).await;
}
}
{
let mut state = self.state.lock().unwrap();
state.connection_status.connected = false;
state.connection_status.last_error = last_error.as_ref().map(|e| e.to_string());
}
Ok(false)
}
async fn send_command(&self, command: &str, params: Option<&[(&str, &str)]>) -> Result<String> {
self.send_command_with_retry(command, params, 3).await
}
async fn send_command_with_retry(&self, command: &str, params: Option<&[(&str, &str)]>, max_retries: u32) -> Result<String> {
let mut last_error = None;
for attempt in 1..=max_retries {
let mut url = format!("{}/requests/status.xml", self.base_url);
if !command.is_empty() {
url.push_str(&format!("?command={}", urlencoding::encode(command)));
if let Some(params) = params {
for (key, value) in params {
url.push_str(&format!("&{}={}",
urlencoding::encode(key),
urlencoding::encode(value)
));
}
}
}
let mut request = self.client.get(&url);
if let Some(ref auth) = self.auth_header {
request = request.header("Authorization", auth);
}
match request.send().await {
Ok(response) => {
if response.status().is_success() {
match response.text().await {
Ok(text) => return Ok(text),
Err(e) => {
last_error = Some(anyhow::anyhow!("Failed to read response text: {}", e));
}
}
} else {
last_error = Some(anyhow::anyhow!("HTTP request failed with status: {}", response.status()));
}
}
Err(e) => {
last_error = Some(anyhow::anyhow!("Request failed: {}", e));
}
}
if attempt < max_retries {
let delay = Duration::from_millis(100 * (2_u64.pow(attempt - 1)));
tokio::time::sleep(delay).await;
}
}
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("All retry attempts failed")))
}
pub async fn load_media(&mut self, source: &MediaSource) -> Result<()> {
let path = match source {
MediaSource::LocalFile(path) => path.clone(),
MediaSource::Url(url) => url.clone(),
};
let mut last_error = None;
const MAX_RETRIES: u32 = 3;
for attempt in 1..=MAX_RETRIES {
match self.send_command_with_retry("in_play", Some(&[("input", &path)]), 2).await {
Ok(_) => {
{
let mut state = self.state.lock().unwrap();
state.state = PlaybackState::Loading;
state.media_info.path = path.clone();
state.media_info.filename = std::path::Path::new(&path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(&path)
.to_string();
}
tokio::time::sleep(Duration::from_millis(1000)).await;
let _ = self.send_command_with_retry("loop", None, 2).await;
{
let mut state = self.state.lock().unwrap();
state.fullscreen = true;
state.media_info.loop_enabled = true;
}
match self.update_state_from_vlc().await {
Ok(_) => {
let state = self.state.lock().unwrap();
if !matches!(state.state, PlaybackState::Stopped) || state.media_info.duration.is_some() {
return Ok(());
} else {
last_error = Some(anyhow::anyhow!("Media failed to load - VLC shows stopped state"));
}
}
Err(e) => {
last_error = Some(anyhow::anyhow!("Failed to update state after loading: {}", e));
}
}
}
Err(e) => {
last_error = Some(anyhow::anyhow!("Failed to send load command: {}", e));
}
}
if attempt < MAX_RETRIES {
tokio::time::sleep(Duration::from_millis(500 * attempt as u64)).await;
}
}
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("Failed to load media after all retry attempts")))
}
pub async fn play_pause(&mut self) -> Result<()> {
match self.send_command_with_retry("pl_pause", None, 3).await {
Ok(_) => {
tokio::time::sleep(Duration::from_millis(200)).await;
self.update_state_from_vlc().await?;
Ok(())
}
Err(e) => {
match self.test_connection().await {
Ok(true) => {
Err(anyhow::anyhow!("Play/pause command failed: {}", e))
}
Ok(false) => {
Err(anyhow::anyhow!("Play/pause failed - VLC connection lost: {}", e))
}
Err(conn_err) => {
Err(anyhow::anyhow!("Play/pause failed and connection test failed: {} (original: {})", conn_err, e))
}
}
}
}
}
pub async fn stop(&mut self) -> Result<()> {
match self.send_command_with_retry("pl_stop", None, 3).await {
Ok(_) => {
{
let mut state = self.state.lock().unwrap();
state.state = PlaybackState::Stopped;
state.media_info.position = 0.0;
}
Ok(())
}
Err(e) => {
{
let mut state = self.state.lock().unwrap();
state.state = PlaybackState::Stopped;
state.media_info.position = 0.0;
}
Err(anyhow::anyhow!("Stop command failed (state updated anyway): {}", e))
}
}
}
pub async fn set_volume(&mut self, volume: f64) -> Result<()> {
let volume = volume.clamp(0.0, 100.0) as u32;
self.send_command("volume", Some(&[("val", &volume.to_string())])).await?;
{
let mut state = self.state.lock().unwrap();
state.media_info.volume = volume as f64;
}
Ok(())
}
pub async fn mute(&mut self) -> Result<()> {
let current_volume = {
let state = self.state.lock().unwrap();
state.media_info.volume
};
if current_volume > 0.0 {
self.set_volume(0.0).await?;
let mut state = self.state.lock().unwrap();
state.media_info.muted = true;
} else {
self.set_volume(50.0).await?;
let mut state = self.state.lock().unwrap();
state.media_info.muted = false;
}
Ok(())
}
pub async fn seek(&mut self, seconds: f64) -> Result<()> {
let seek_value = if seconds >= 0.0 {
format!("+{}s", seconds as u32)
} else {
format!("{}s", seconds as i32)
};
self.send_command("seek", Some(&[("val", &seek_value)])).await?;
{
let mut state = self.state.lock().unwrap();
state.media_info.position = (state.media_info.position + seconds).max(0.0);
}
Ok(())
}
pub async fn set_speed(&mut self, speed: f64) -> Result<()> {
let speed = speed.clamp(0.1, 10.0);
let mut state = self.state.lock().unwrap();
state.media_info.speed = speed;
Ok(())
}
pub async fn toggle_fullscreen(&mut self) -> Result<()> {
self.send_command("fullscreen", None).await?;
{
let mut state = self.state.lock().unwrap();
state.fullscreen = !state.fullscreen;
}
Ok(())
}
pub async fn toggle_loop(&mut self) -> Result<()> {
self.send_command("loop", None).await?;
{
let mut state = self.state.lock().unwrap();
state.media_info.loop_enabled = !state.media_info.loop_enabled;
}
Ok(())
}
pub async fn get_state(&mut self) -> PlayerState {
let _ = self.update_state_from_vlc().await;
let state = self.state.lock().unwrap();
state.clone()
}
pub async fn quit(&mut self) -> Result<()> {
let _ = self.send_command("quit", None).await;
tokio::time::sleep(Duration::from_millis(500)).await;
if let Some(mut process) = self.vlc_process.take() {
match process.try_wait() {
Ok(Some(_)) => {
}
Ok(None) => {
let _ = process.kill();
let _ = process.wait();
}
Err(_) => {
let _ = process.kill();
}
}
}
Ok(())
}
async fn update_state_from_vlc(&mut self) -> Result<()> {
match self.send_command_with_retry("", None, 2).await {
Ok(response) => {
let mut state = self.state.lock().unwrap();
if response.contains("<state>playing</state>") {
state.state = PlaybackState::Playing;
} else if response.contains("<state>paused</state>") {
state.state = PlaybackState::Paused;
} else if response.contains("<state>stopped</state>") {
state.state = PlaybackState::Stopped;
}
if let Some(volume_start) = response.find("<volume>") {
if let Some(volume_end) = response[volume_start..].find("</volume>") {
let volume_str = &response[volume_start + 8..volume_start + volume_end];
if let Ok(volume) = volume_str.parse::<f64>() {
state.media_info.volume = volume;
}
}
}
if let Some(pos_start) = response.find("<time>") {
if let Some(pos_end) = response[pos_start..].find("</time>") {
let pos_str = &response[pos_start + 6..pos_start + pos_end];
if let Ok(position) = pos_str.parse::<f64>() {
state.media_info.position = position;
}
}
}
if let Some(dur_start) = response.find("<length>") {
if let Some(dur_end) = response[dur_start..].find("</length>") {
let dur_str = &response[dur_start + 8..dur_start + dur_end];
if let Ok(duration) = dur_str.parse::<f64>() {
if duration > 0.0 {
state.media_info.duration = Some(duration);
}
}
}
}
state.connection_status.connected = true;
state.connection_status.last_error = None;
Ok(())
}
Err(e) => {
let mut state = self.state.lock().unwrap();
state.connection_status.last_error = Some(format!("State update failed: {}", e));
Err(e)
}
}
}
}