use anyhow::Result;
#[cfg(feature = "download_ffmpeg")]
use std::path::{Path, PathBuf};
#[cfg(feature = "download_ffmpeg")]
use tokio::fs::File;
pub const UNPACK_DIRNAME: &str = "ffmpeg_release_temp";
pub fn ffmpeg_manifest_url() -> Result<&'static str> {
if cfg!(not(target_arch = "x86_64")) {
anyhow::bail!("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 {
anyhow::bail!("Unsupported platform")
}
}
pub fn ffmpeg_download_url() -> Result<&'static str> {
if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
Ok("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip")
} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
Ok("https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz")
} else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
Ok("https://evermeet.cx/ffmpeg/getrelease/zip")
} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
Ok("https://www.osxexperts.net/ffmpeg7arm.zip") } else {
anyhow::bail!("Unsupported platform; you can provide your own URL instead and call download_ffmpeg_package directly.")
}
}
#[cfg(feature = "download_ffmpeg")]
pub async fn auto_download() -> Result<()> {
use crate::{command::ffmpeg_is_installed, paths::sidecar_dir};
if ffmpeg_is_installed().await {
return Ok(());
}
let download_url = ffmpeg_download_url()?;
let destination = sidecar_dir()?;
tokio::fs::create_dir_all(&destination).await?;
let archive_path = download_ffmpeg_package(download_url, &destination).await?;
unpack_ffmpeg(&archive_path, &destination).await?;
if !(ffmpeg_is_installed().await) {
anyhow::bail!("Ffmpeg failed to install, please install manually")
}
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)?
.split_whitespace()
.next()
.map(|s| s.to_string())
}
#[cfg(feature = "download_ffmpeg")]
pub async fn check_latest_version() -> Result<String> {
use anyhow::Context;
if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
return Ok("7.0".to_string());
}
let manifest_url = ffmpeg_manifest_url()?;
let version_string = reqwest::get(manifest_url)
.await?
.error_for_status()?
.text()
.await?;
if cfg!(target_os = "windows") {
Ok(version_string)
} else if cfg!(target_os = "macos") {
parse_macos_version(&version_string).context("failed to parse version number (macos variant)")
} else if cfg!(target_os = "linux") {
parse_linux_version(&version_string).context("failed to parse version number (linux variant)")
} else {
anyhow::bail!("unsupported platform")
}
}
#[cfg(feature = "download_ffmpeg")]
pub async fn download_ffmpeg_package(url: &str, download_dir: &Path) -> Result<PathBuf> {
use anyhow::Context;
use futures_util::StreamExt;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
let filename = Path::new(url)
.file_name()
.context("Failed to get filename")?;
let archive_path = download_dir.join(filename);
let response = reqwest::get(url)
.await
.context("failed to download ffmpeg")?
.error_for_status()
.context("server returned error")?;
let mut file = File::create(&archive_path)
.await
.context("failed to create file for ffmpeg download")?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let data = chunk?;
file.write_all(&data).await?
}
Ok(archive_path)
}
#[cfg(feature = "download_ffmpeg")]
pub async fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()> {
use anyhow::Context;
use tokio::fs::{create_dir_all, read_dir, remove_dir_all, remove_file, File};
let temp_folder = binary_folder.join(UNPACK_DIRNAME);
create_dir_all(&temp_folder)
.await
.context("failed creating temp dir")?;
let file = File::open(from_archive)
.await
.context("failed to open archive")?;
#[cfg(target_os = "linux")]
{
untar_file(file, &temp_folder).await?
}
#[cfg(not(target_os = "linux"))]
{
unzip_file(file, &temp_folder).await?
}
let inner_folder = read_dir(&temp_folder)
.await?
.next_entry()
.await
.context("Failed to get inner folder")?
.unwrap();
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!(target_os = "linux") {
(
inner_folder.path().join("./ffmpeg"),
inner_folder.path().join("./ffplay"), inner_folder.path().join("./ffprobe"),
)
} else if cfg!(target_os = "macos") {
(
temp_folder.join("ffmpeg"),
temp_folder.join("ffplay"), temp_folder.join("ffprobe"), )
} else {
anyhow::bail!("Unsupported platform");
};
if ffmpeg.exists() {
set_executable_permission(&ffmpeg).await?;
move_bin(&ffmpeg, binary_folder).await?;
}
if ffprobe.exists() {
set_executable_permission(&ffprobe).await?;
move_bin(&ffprobe, binary_folder).await?;
}
if ffplay.exists() {
set_executable_permission(&ffplay).await?;
move_bin(&ffplay, binary_folder).await?;
}
if temp_folder.exists() && temp_folder.is_dir() {
remove_dir_all(&temp_folder).await?;
}
if from_archive.exists() {
remove_file(from_archive).await?;
}
Ok(())
}
#[cfg(feature = "download_ffmpeg")]
async fn move_bin(path: &Path, binary_folder: &Path) -> Result<()> {
use anyhow::Context;
use tokio::fs::rename;
let file_name = binary_folder.join(
path
.file_name()
.with_context(|| format!("Path {} does not have a file_name", path.to_string_lossy()))?,
);
rename(path, file_name).await?;
anyhow::Ok(())
}
#[cfg(all(feature = "download_ffmpeg", target_family = "unix"))]
async fn set_executable_permission(path: &Path) -> Result<()> {
#[cfg(target_family = "unix")]
{
use tokio::fs::set_permissions;
use std::os::unix::fs::PermissionsExt;
let mut perms = path.metadata()?.permissions();
perms.set_mode(perms.mode() | 0o100);
set_permissions(path, perms).await?;
}
Ok(())
}
#[cfg(all(feature = "download_ffmpeg", not(target_family = "unix")))]
async fn set_executable_permission(_path: &Path) -> Result<()> {
Ok(())
}
#[cfg(all(feature = "download_ffmpeg", not(target_os = "linux")))]
async fn unzip_file(archive: File, out_dir: &Path) -> Result<()> {
use anyhow::Context;
use async_zip::base::read::seek::ZipFileReader;
use tokio::fs::create_dir_all;
use tokio::fs::OpenOptions;
use tokio::io::BufReader;
use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
let archive = BufReader::new(archive).compat();
let mut reader = ZipFileReader::new(archive)
.await
.context("Failed to read zip file")?;
for index in 0..reader.file().entries().len() {
let entry = reader.file().entries().get(index).unwrap();
let path = out_dir.join(sanitize_file_path(entry.filename().as_str()?));
let entry_is_dir = entry.dir()?;
let mut entry_reader = reader
.reader_without_entry(index)
.await
.expect("Failed to read ZipEntry");
if entry_is_dir {
if !path.exists() {
create_dir_all(&path)
.await
.expect("Failed to create extracted directory");
}
} else {
let parent = path
.parent()
.expect("A file entry should have parent directories");
if !parent.is_dir() {
create_dir_all(parent)
.await
.expect("Failed to create parent directories");
}
let writer = OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.await
.expect("Failed to create extracted file");
futures_util::io::copy(&mut entry_reader, &mut writer.compat_write())
.await
.expect("Failed to copy to extracted file");
}
}
Ok(())
}
#[cfg(all(feature = "download_ffmpeg", not(target_os = "linux")))]
fn sanitize_file_path(path: &str) -> PathBuf {
path
.replace('\\', "/")
.split('/')
.map(sanitize_filename::sanitize)
.collect()
}
#[cfg(all(feature = "download_ffmpeg", target_os = "linux"))]
async fn untar_file(archive: File, out_dir: &Path) -> Result<()> {
use async_compression::tokio::bufread::XzDecoder;
use tokio::io::BufReader;
use tokio_tar::Archive;
let archive = BufReader::new(archive);
let archive = XzDecoder::new(archive);
let mut archive = Archive::new(archive);
archive.unpack(out_dir).await?;
Ok(())
}