use std::{
fs::{create_dir_all, read_dir, remove_dir_all, remove_file, rename},
io::Read,
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
};
use crate::{
command::ffmpeg_is_installed,
error::{Error, Result},
paths::sidecar_dir,
};
pub const UNPACK_DIRNAME: &str = "ffmpeg_release_temp";
pub fn ffmpeg_manifest_url() -> Result<&'static str> {
if cfg!(not(target_arch = "x86_64")) {
return Err(Error::msg(
"Downloads must be manually provided for non-x86_64 architectures",
));
}
if cfg!(target_os = "windows") {
Ok("https://www.gyan.dev/ffmpeg/builds/release-version")
} else if cfg!(target_os = "macos") {
Ok("https://evermeet.cx/ffmpeg/info/ffmpeg/release")
} else if cfg!(target_os = "linux") {
Ok("https://johnvansickle.com/ffmpeg/release-readme.txt")
} else {
Err(Error::msg("Unsupported platform"))
}
}
pub fn ffmpeg_download_url() -> Result<&'static str> {
if cfg!(not(target_arch = "x86_64")) {
return Err(Error::msg(
"Downloads must be manually provided for non-x86_64 architectures",
));
}
if cfg!(target_os = "windows") {
Ok("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip")
} else if cfg!(target_os = "macos") {
Ok("https://evermeet.cx/ffmpeg/getrelease")
} else if cfg!(target_os = "linux") {
Ok("https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz")
} else {
Err(Error::msg("Unsupported platform"))
}
}
pub fn auto_download() -> Result<()> {
if ffmpeg_is_installed() {
return Ok(());
}
let download_url = ffmpeg_download_url()?;
let destination = sidecar_dir()?;
let archive_path = download_ffmpeg_package(download_url, &destination)?;
unpack_ffmpeg(&archive_path, &destination)?;
match ffmpeg_is_installed() {
false => Err(Error::msg(
"FFmpeg failed to install, please install manually.",
)),
true => Ok(()),
}
}
pub fn parse_macos_version(version: &str) -> Option<String> {
version
.split("\"version\":")
.nth(1)?
.trim()
.split("\"")
.nth(1)
.map(|s| s.to_string())
}
pub fn parse_linux_version(version: &str) -> Option<String> {
version
.split("version:")
.nth(1)?
.trim_start()
.split_whitespace()
.next()
.map(|s| s.to_string())
}
pub fn curl(url: &str) -> Result<String> {
let mut child = Command::new("curl")
.args(["-L", url])
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()?;
let stdout = child
.stdout
.take()
.ok_or(Error::msg("Failed to get stdout"))?;
let mut string = String::new();
std::io::BufReader::new(stdout).read_to_string(&mut string)?;
Ok(string)
}
pub fn curl_to_file(url: &str, destination: &str) -> Result<ExitStatus> {
Command::new("curl")
.args(["-L", url])
.args(["-o", destination])
.status()
.map_err(Error::from)
}
pub fn check_latest_version() -> Result<String> {
let string = curl(ffmpeg_manifest_url()?)?;
if cfg!(target_os = "windows") {
Ok(string)
} else if cfg!(target_os = "macos") {
Ok(parse_macos_version(&string).ok_or("failed to parse version number (macos variant)")?)
} else if cfg!(target_os = "linux") {
Ok(parse_linux_version(&string).ok_or("failed to parse version number (linux variant)")?)
} else {
Err(Error::msg("Unsupported platform"))
}
}
pub fn download_ffmpeg_package(url: &str, download_dir: &PathBuf) -> Result<PathBuf> {
let filename = Path::new(url)
.file_name()
.ok_or(Error::msg("Failed to get filename"))?;
let archive_path = download_dir.join(filename);
let archive_filename = archive_path.to_str().ok_or("invalid download path")?;
let exit_status = curl_to_file(url, archive_filename)?;
if !exit_status.success() {
return Err(Error::msg("Failed to download ffmpeg"));
}
Ok(archive_path)
}
pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &PathBuf) -> Result<()> {
let temp_dirname = UNPACK_DIRNAME;
let temp_folder = binary_folder.join(temp_dirname);
create_dir_all(&temp_folder)?;
Command::new("tar")
.arg("-xf")
.arg(from_archive)
.current_dir(&temp_folder)
.status()?
.success()
.then_some(())
.ok_or("Failed to unpack ffmpeg")?;
let inner_folder = read_dir(&temp_folder)?
.next()
.ok_or("Failed to get inner folder")??;
if !inner_folder.file_type()?.is_dir() {
return Err(Error::msg("No top level directory inside archive"));
}
let (ffmpeg, ffplay, ffprobe) = if cfg!(target_os = "windows") {
(
(&inner_folder).path().join("bin/ffmpeg.exe"),
(&inner_folder).path().join("bin/ffplay.exe"),
(&inner_folder).path().join("bin/ffprobe.exe"),
)
} else if cfg!(any(target_os = "linux", target_os = "macos")) {
(
(&inner_folder).path().join("./ffmpeg"),
(&inner_folder).path().join("./ffplay"), (&inner_folder).path().join("./ffprobe"),
)
} else {
return Err(Error::msg("Unsupported platform"));
};
rename(&ffmpeg, binary_folder.join(ffmpeg.file_name().ok_or(())?))?;
if ffprobe.exists() {
rename(&ffprobe, binary_folder.join(ffprobe.file_name().ok_or(())?))?;
}
if ffplay.exists() {
rename(&ffplay, binary_folder.join(ffplay.file_name().ok_or(())?))?;
}
if temp_folder.exists() {
remove_dir_all(&temp_folder)?;
}
if from_archive.exists() {
remove_file(from_archive)?;
}
Ok(())
}