From a5442f140179a79a24e8d3800c95421cbdf90c09 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 22 Sep 2024 07:57:09 +0000 Subject: [PATCH] Add support to use ugoira cli --- Cargo.lock | 11 +++ Cargo.toml | 1 + src/data/mod.rs | 1 - src/data/video.rs | 31 ++++++ src/download.rs | 43 +++++++++ src/error.rs | 2 - src/ext/mod.rs | 1 + src/ext/subprocess.rs | 37 ++++++++ src/main.rs | 1 - src/opt/mod.rs | 1 - src/opthelper.rs | 47 ++++++++-- src/opts.rs | 63 +++++++++---- src/settings_list.rs | 12 +-- src/ugoira.rs | 214 ++++++++++++++++++++++++++++++++++++++---- 14 files changed, 406 insertions(+), 59 deletions(-) create mode 100644 src/ext/subprocess.rs diff --git a/Cargo.lock b/Cargo.lock index de221806..164c20fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,6 +1668,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "subprocess", "tokio", "url", "urlparse", @@ -2104,6 +2105,16 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subprocess" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 789b15f2..348e5722 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ RustyXML = "0.3" serde = "1" serde_json = "1" serde_urlencoded = { version = "*", optional = true } +subprocess = "0.2" tokio = { version = "1.27", features = ["rt", "macros", "rt-multi-thread", "time"] } url = "2.3" urlparse = "0.7" diff --git a/src/data/mod.rs b/src/data/mod.rs index 76eacbd3..cf9bdb8b 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -3,5 +3,4 @@ pub mod data; pub mod exif; pub mod fanbox; pub mod json; -#[cfg(feature = "avdict")] pub mod video; diff --git a/src/data/video.rs b/src/data/video.rs index e656630f..64facd81 100644 --- a/src/data/video.rs +++ b/src/data/video.rs @@ -1,8 +1,12 @@ +#[cfg(feature = "avdict")] use crate::avdict::AVDict; +#[cfg(feature = "avdict")] use crate::avdict::AVDictError; use crate::data::data::PixivData; use crate::parser::description::parse_description; +use std::collections::HashMap; +#[cfg(feature = "avdict")] pub fn get_video_metadata(data: &PixivData) -> Result { let mut d = AVDict::new(); if data.title.is_some() { @@ -24,3 +28,30 @@ pub fn get_video_metadata(data: &PixivData) -> Result { } Ok(d) } + +pub fn get_video_metas(data: &PixivData) -> HashMap { + let mut m = HashMap::new(); + match &data.title { + Some(t) => { + m.insert(String::from("title"), t.clone()); + } + None => {} + } + match &data.author { + Some(a) => { + m.insert(String::from("artist"), a.clone()); + } + None => {} + } + match &data.description { + Some(desc) => { + let des = match parse_description(desc) { + Some(desc) => desc, + None => desc.to_owned(), + }; + m.insert(String::from("comment"), des); + } + None => {} + } + m +} diff --git a/src/download.rs b/src/download.rs index 371a9d4c..1b000b62 100644 --- a/src/download.rs +++ b/src/download.rs @@ -8,6 +8,7 @@ use crate::data::fanbox::FanboxData; use crate::data::json::JSONDataFile; #[cfg(feature = "ugoira")] use crate::data::video::get_video_metadata; +use crate::data::video::get_video_metas; #[cfg(feature = "db")] use crate::db::open_and_init_database; use crate::downloader::Downloader; @@ -32,6 +33,7 @@ use crate::pixiv_link::PixivID; use crate::pixiv_web::PixivWebClient; use crate::task_manager::get_progress_bar; use crate::task_manager::TaskManager; +use crate::ugoira::convert_ugoira_to_mp4_subprocess; #[cfg(feature = "ugoira")] use crate::ugoira::{convert_ugoira_to_mp4, UgoiraFrames}; use crate::utils::get_file_name_from_url; @@ -310,6 +312,47 @@ pub async fn download_artwork_ugoira( let task = tasks.get_mut(0).try_err(gettext("No finished task."))?; task.await??; #[cfg(feature = "ugoira")] + let use_cli = helper.ugoira_cli(); + #[cfg(not(feature = "ugoira"))] + let use_cli = true; + if use_cli { + if let Some(ubase) = helper.ugoira() { + let file_name = get_file_name_from_url(src).try_err(format!( + "{} {}", + gettext("Failed to get file name from url:"), + src + ))?; + let file_name = base.join(file_name); + let metadata = get_video_metas(&datas.clone()); + let frames_file_name = base.join(format!("{}_frames.json", id)); + std::fs::write( + &frames_file_name, + json::stringify((&ugoira_data["frames"]).clone()), + ) + .try_err4(gettext("Failed to write frames info to file:"))?; + let output_file_name = base.join(format!("{}.mp4", id)); + convert_ugoira_to_mp4_subprocess( + &ubase, + &file_name, + &output_file_name, + &frames_file_name, + helper.ugoira_max_fps(), + metadata, + helper.force_yuv420p(), + helper.x264_crf(), + Some(helper.x264_profile()), + ) + .await?; + log::info!( + "{}", + gettext("Converted -> ") + .replace("", file_name.to_str().unwrap_or("(null)")) + .replace("", output_file_name.to_str().unwrap_or("(null)")) + .as_str() + ); + } + } + #[cfg(feature = "ugoira")] { let file_name = get_file_name_from_url(src).try_err(format!( "{} {}", diff --git a/src/error.rs b/src/error.rs index a9650aaa..de0115d1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,4 @@ use crate::downloader::DownloaderError; -#[cfg(feature = "ugoira")] use crate::ugoira::UgoiraError; use tokio::task::JoinError; @@ -8,7 +7,6 @@ pub enum PixivDownloaderError { DownloaderError(DownloaderError), String(String), JoinError(JoinError), - #[cfg(feature = "ugoira")] UgoiraError(UgoiraError), #[cfg(feature = "server")] Hyper(hyper::Error), diff --git a/src/ext/mod.rs b/src/ext/mod.rs index 7588f227..92f46a87 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -8,5 +8,6 @@ pub mod json; pub mod rawhandle; pub mod replace; pub mod rw_lock; +pub mod subprocess; pub mod try_err; pub mod use_or_not; diff --git a/src/ext/subprocess.rs b/src/ext/subprocess.rs new file mode 100644 index 00000000..6ea6567c --- /dev/null +++ b/src/ext/subprocess.rs @@ -0,0 +1,37 @@ +use subprocess::{ExitStatus, Popen}; + +pub struct PopenAwait<'a> { + pub p: &'a mut Popen, +} + +impl<'a> std::future::Future for PopenAwait<'a> { + type Output = ExitStatus; + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + match self.get_mut().p.poll() { + Some(e) => std::task::Poll::Ready(e), + None => { + cx.waker().wake_by_ref(); + return std::task::Poll::Pending; + } + } + } +} + +impl<'a> From<&'a mut Popen> for PopenAwait<'a> { + fn from(value: &'a mut Popen) -> PopenAwait<'a> { + Self { p: value } + } +} + +pub trait PopenAsyncExt<'a> { + fn async_wait(&'a mut self) -> PopenAwait<'a>; +} + +impl<'a> PopenAsyncExt<'a> for Popen { + fn async_wait(&'a mut self) -> PopenAwait<'a> { + PopenAwait::from(self) + } +} diff --git a/src/main.rs b/src/main.rs index 171ce39b..f8ccb1b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,7 +53,6 @@ mod settings_list; mod task_manager; #[cfg(feature = "server")] mod tmp_cache; -#[cfg(feature = "ugoira")] mod ugoira; mod utils; mod webclient; diff --git a/src/opt/mod.rs b/src/opt/mod.rs index 39e8250b..530595eb 100644 --- a/src/opt/mod.rs +++ b/src/opt/mod.rs @@ -1,6 +1,5 @@ /// Author name filters pub mod author_name_filter; -#[cfg(feature = "ugoira")] /// libx264 Constant Rate Factor settings pub mod crf; /// HTTP Header Map diff --git a/src/opthelper.rs b/src/opthelper.rs index 29c8c17b..cd3742c2 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -18,7 +18,6 @@ use crate::server::cors::parse_cors_entries; #[cfg(feature = "server")] use crate::server::cors::CorsEntry; use crate::settings::SettingStore; -#[cfg(feature = "ugoira")] use crate::ugoira::X264Profile; use is_terminal::IsTerminal; #[cfg(feature = "server")] @@ -29,7 +28,6 @@ use std::net::Ipv4Addr; use std::net::SocketAddr; use std::ops::Deref; use std::path::PathBuf; -#[cfg(any(feature = "server", feature = "ugoira"))] use std::str::FromStr; use std::sync::Arc; use std::sync::RwLock; @@ -205,7 +203,6 @@ impl OptHelper { self._fanbox_http_headers.get_ref().clone() } - #[cfg(feature = "ugoira")] /// Return whether to force yuv420p as output pixel format when converting ugoira(GIF) to video. pub fn force_yuv420p(&self) -> bool { match self.opt.get_ref().force_yuv420p { @@ -531,7 +528,6 @@ impl OptHelper { false } - #[cfg(feature = "ugoira")] /// The max fps when converting ugoira(GIF) to video. pub fn ugoira_max_fps(&self) -> f32 { match self.opt.get_ref().ugoira_max_fps { @@ -547,7 +543,6 @@ impl OptHelper { 60f32 } - #[cfg(feature = "ugoira")] /// The Constant Rate Factor when converting ugoira(GIF) to video. pub fn x264_crf(&self) -> Option { match self.opt.get_ref().x264_crf { @@ -563,7 +558,6 @@ impl OptHelper { None } - #[cfg(feature = "ugoira")] /// Return the x264 profile when converting ugoira(GIF) to video. pub fn x264_profile(&self) -> X264Profile { match self.opt.get_ref().x264_profile { @@ -656,6 +650,47 @@ impl OptHelper { pub fn log_cfg(&self) -> Option { return self.settings.get_ref().get_str("log-cfg"); } + + /// The path to ugoira cli executable. + pub fn ugoira(&self) -> Option { + match self.opt.get_ref().ugoira.as_ref() { + Some(d) => { + return Some(d.clone()); + } + None => {} + } + match self.settings.get_ref().get_str("ugoira") { + Some(s) => Some(s), + None => { + #[cfg(all(feature = "ugoira", any(feature = "docker", windows)))] + return Some(String::from("ugoira")); + #[cfg(all(feature = "ugoira", not(any(feature = "docker", windows))))] + return Some( + crate::utils::get_exe_path_else_current() + .join("ugoira") + .to_string_lossy() + .into_owned(), + ); + #[cfg(not(feature = "ugoira"))] + return None; + } + } + } + + #[cfg(feature = "ugoira")] + /// Whether to use ugoira cli. + pub fn ugoira_cli(&self) -> bool { + match self.opt.get_ref().ugoira_cli.as_ref() { + Some(d) => { + return d.clone(); + } + None => {} + } + match self.settings.get_ref().get_bool("ugoira-cli") { + Some(d) => d, + None => false, + } + } } impl Default for OptHelper { diff --git a/src/opts.rs b/src/opts.rs index 929e2403..b8cd7cda 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -3,7 +3,6 @@ use crate::gettext; use crate::list::NonTailList; use crate::pixiv_link::PixivID; use crate::retry_interval::parse_retry_interval_from_str; -#[cfg(feature = "ugoira")] use crate::ugoira::X264Profile; use crate::utils::check_file_exists; use crate::utils::get_exe_path_else_current; @@ -12,7 +11,6 @@ use getopts::Options; use std::env; #[cfg(feature = "server")] use std::net::SocketAddr; -#[cfg(feature = "ugoira")] use std::num::ParseFloatError; use std::num::ParseIntError; use std::num::TryFromIntError; @@ -105,10 +103,8 @@ pub struct CommandOpts { pub download_multiple_posts: Option, /// The maximum number of tasks to download posts/artworks at the same time. pub max_download_post_tasks: Option, - #[cfg(feature = "ugoira")] /// Whether to force yuv420p as output pixel format when converting ugoira(GIF) to video. pub force_yuv420p: Option, - #[cfg(feature = "ugoira")] /// The x264 profile when converting ugoira(GIF) to video. pub x264_profile: Option, /// The base directory to save downloaded files. @@ -116,10 +112,8 @@ pub struct CommandOpts { pub user_agent: Option, /// Urls want to download pub urls: Option>, - #[cfg(feature = "ugoira")] /// The Constant Rate Factor when converting ugoira(GIF) to video. pub x264_crf: Option, - #[cfg(feature = "ugoira")] pub ugoira_max_fps: Option, pub fanbox_page_number: Option, /// Pixiv's refresh token. Used to login. @@ -139,6 +133,11 @@ pub struct CommandOpts { #[cfg(feature = "server")] /// Whether to prevent to run push task. pub disable_push_task: bool, + /// The path to ugoira cli executable. + pub ugoira: Option, + #[cfg(feature = "ugoira")] + /// Whether to use ugoira cli. + pub ugoira_cli: Option, } impl CommandOpts { @@ -170,16 +169,12 @@ impl CommandOpts { max_download_tasks: None, download_multiple_posts: None, max_download_post_tasks: None, - #[cfg(feature = "ugoira")] force_yuv420p: None, - #[cfg(feature = "ugoira")] x264_profile: None, download_base: None, user_agent: None, urls: None, - #[cfg(feature = "ugoira")] x264_crf: None, - #[cfg(feature = "ugoira")] ugoira_max_fps: None, fanbox_page_number: None, refresh_token: None, @@ -192,6 +187,9 @@ impl CommandOpts { push_task_max_push_count: None, #[cfg(feature = "server")] disable_push_task: false, + ugoira: None, + #[cfg(feature = "ugoira")] + ugoira_cli: None, } } @@ -308,7 +306,6 @@ pub fn parse_bool>(s: Option) -> Result, String> { } } -#[cfg(feature = "ugoira")] /// Parse [f32] from string pub fn parse_f32>(s: Option) -> Result, ParseFloatError> { match s { @@ -371,7 +368,6 @@ pub fn parse_nonempty_usize>(s: Option) -> Result } } -#[cfg(feature = "ugoira")] pub fn parse_x264_profile>( s: Option, ) -> Result, &'static str> { @@ -558,7 +554,6 @@ pub fn parse_cmd() -> Option { HasArg::Maybe, getopts::Occur::Optional, ); - #[cfg(feature = "ugoira")] opts.opt( "", "force-yuv420p", @@ -573,7 +568,6 @@ pub fn parse_cmd() -> Option { HasArg::Maybe, getopts::Occur::Optional, ); - #[cfg(feature = "ugoira")] opts.opt( "", "x264-profile", @@ -595,7 +589,6 @@ pub fn parse_cmd() -> Option { "DIR", ); opts.optopt("", "user-agent", gettext("The User-Agent header."), "UA"); - #[cfg(feature = "ugoira")] opts.opt( "", "x264-crf", @@ -604,7 +597,6 @@ pub fn parse_cmd() -> Option { HasArg::Maybe, getopts::Occur::Optional, ); - #[cfg(feature = "ugoira")] opts.opt( "", "ugoira-max-fps", @@ -697,6 +689,26 @@ pub fn parse_cmd() -> Option { "disable-push-task", gettext("Prevent to run push task."), ); + opts.optopt( + "", + "ugoira", + gettext("The path to ugoira cli executable."), + "PATH", + ); + #[cfg(feature = "ugoira")] + opts.opt( + "", + "ugoira-cli", + &format!( + "{} ({} {})", + gettext("Whether to use ugoira cli."), + gettext("Default:"), + "yes" + ), + "yes/no", + HasArg::Maybe, + getopts::Occur::Optional, + ); let result = match opts.parse(&argv[1..]) { Ok(m) => m, Err(err) => { @@ -959,7 +971,6 @@ pub fn parse_cmd() -> Option { return None; } } - #[cfg(feature = "ugoira")] match parse_optional_opt(&result, "force-yuv420p", true, parse_bool) { Ok(b) => re.as_mut().unwrap().force_yuv420p = b, Err(e) => { @@ -973,7 +984,6 @@ pub fn parse_cmd() -> Option { return None; } } - #[cfg(feature = "ugoira")] match parse_optional_opt( &result, "x264-profile", @@ -994,7 +1004,6 @@ pub fn parse_cmd() -> Option { } re.as_mut().unwrap().download_base = result.opt_str("download-base"); re.as_mut().unwrap().user_agent = result.opt_str("user-agent"); - #[cfg(feature = "ugoira")] match parse_optional_opt(&result, "x264-crf", -1f32, parse_f32) { Ok(r) => match r { Some(crf) => { @@ -1018,7 +1027,6 @@ pub fn parse_cmd() -> Option { return None; } } - #[cfg(feature = "ugoira")] match parse_optional_opt(&result, "ugoira-max-fps", 60f32, parse_f32) { Ok(r) => match r { Some(crf) => { @@ -1130,6 +1138,21 @@ pub fn parse_cmd() -> Option { { re.as_mut().unwrap().disable_push_task = result.opt_present("disable-push-task"); } + re.as_mut().unwrap().ugoira = result.opt_str("ugoira"); + #[cfg(feature = "ugoira")] + match parse_optional_opt(&result, "ugoira-cli", true, parse_bool) { + Ok(b) => re.as_mut().unwrap().ugoira_cli = b, + Err(e) => { + log::error!( + "{} {}", + gettext("Failed to parse :") + .replace("", "ugoira-cli") + .as_str(), + e + ); + return None; + } + } re } diff --git a/src/settings_list.rs b/src/settings_list.rs index 54bf0674..7b22c83a 100644 --- a/src/settings_list.rs +++ b/src/settings_list.rs @@ -7,19 +7,16 @@ use crate::retry_interval::check_retry_interval; use crate::settings::SettingDes; use crate::settings::JsonValueType; use crate::opt::author_name_filter::check_author_name_filters; -#[cfg(feature = "ugoira")] use crate::opt::crf::check_crf; use crate::opt::header_map::check_header_map; use crate::opt::proxy::check_proxy; use crate::opt::size::parse_u32_size; #[cfg(feature = "server")] use crate::server::cors::parse_cors_entries; -#[cfg(feature = "ugoira")] use crate::ugoira::X264Profile; use json::JsonValue; #[cfg(feature = "server")] use std::net::SocketAddr; -#[cfg(any(feature = "server", feature = "ugoira"))] use std::str::FromStr; pub fn get_settings_list() -> Vec { @@ -50,17 +47,13 @@ pub fn get_settings_list() -> Vec { SettingDes::new("max-download-tasks", gettext("The maximum number of tasks to download files at the same time."), JsonValueType::Number, Some(check_nozero_usize)).unwrap(), SettingDes::new("download-multiple-posts", gettext("Download multiple posts/artworks at the same time."), JsonValueType::Boolean, None).unwrap(), SettingDes::new("max-download-post-tasks", gettext("The maximum number of tasks to download posts/artworks at the same time."), JsonValueType::Number, Some(check_nozero_usize)).unwrap(), - #[cfg(feature = "ugoira")] SettingDes::new("force-yuv420p", gettext("Force yuv420p as output pixel format when converting ugoira(GIF) to video."), JsonValueType::Boolean, None).unwrap(), - #[cfg(feature = "ugoira")] SettingDes::new("x264-profile", gettext("The x264 profile when converting ugoira(GIF) to video."), JsonValueType::Str, Some(check_x264_profile)).unwrap(), #[cfg(feature = "db")] SettingDes::new("db", gettext("Database settings."), JsonValueType::Object, Some(check_db_config)).unwrap(), SettingDes::new("download-base", gettext("The base directory to save downloaded files."), JsonValueType::Str, None).unwrap(), SettingDes::new("user-agent", gettext("The User-Agent header."), JsonValueType::Str, None).unwrap(), - #[cfg(feature = "ugoira")] SettingDes::new("x264-crf", gettext("The Constant Rate Factor when converting ugoira(GIF) to video."), JsonValueType::Number, Some(check_crf)).unwrap(), - #[cfg(feature = "ugoira")] SettingDes::new("ugoira-max-fps", gettext("The max fps when converting ugoira(GIF) to video."), JsonValueType::Number, Some(check_ugoira_max_fps)).unwrap(), SettingDes::new("fanbox-page-number", gettext("Use page number for pictures' file name in fanbox."), JsonValueType::Boolean, None).unwrap(), #[cfg(feature = "server")] @@ -78,6 +71,9 @@ pub fn get_settings_list() -> Vec { SettingDes::new("log-cfg", gettext("The path to the config file of log4rs."), JsonValueType::Str, None).unwrap(), #[cfg(feature = "server")] SettingDes::new("ffprobe", gettext("The path to ffprobe executable."), JsonValueType::Str, None).unwrap(), + SettingDes::new("ugoira", gettext("The path to ugoira cli executable."), JsonValueType::Str, None).unwrap(), + #[cfg(feature = "ugoira")] + SettingDes::new("ugoira-cli", gettext("Whether to use ugoira cli."), JsonValueType::Boolean, None).unwrap(), ] } @@ -133,7 +129,6 @@ fn check_user_or_not(obj: &JsonValue) -> bool { r.is_ok() } -#[cfg(feature = "ugoira")] fn check_x264_profile(obj: &JsonValue) -> bool { match obj.as_str() { Some(profile) => X264Profile::from_str(profile).is_ok(), @@ -141,7 +136,6 @@ fn check_x264_profile(obj: &JsonValue) -> bool { } } -#[cfg(feature = "ugoira")] fn check_ugoira_max_fps(obj: &JsonValue) -> bool { match obj.as_f32() { Some(fps) => fps > 0f32 && fps <= 1000f32, diff --git a/src/ugoira.rs b/src/ugoira.rs index 59749e5d..cb21412b 100644 --- a/src/ugoira.rs +++ b/src/ugoira.rs @@ -1,21 +1,27 @@ +#[cfg(feature = "ugoira")] use crate::_ugoira; +#[cfg(feature = "avdict")] use crate::avdict::AVDict; +#[cfg(feature = "avdict")] use crate::avdict::AVDictCodeError; use crate::ext::cstr::ToCStr; use crate::ext::cstr::ToCStrError; use crate::ext::json::ToJson; +#[cfg(feature = "ugoira")] use crate::ext::rawhandle::ToRawHandle; +use crate::ext::subprocess::PopenAsyncExt; use crate::ext::try_err::TryErr; use crate::gettext; +use std::collections::HashMap; use std::convert::AsRef; use std::default::Default; use std::ffi::CStr; use std::ffi::OsStr; +use std::ffi::OsString; use std::fmt::Debug; use std::fmt::Display; #[cfg(test)] use std::fs::{create_dir, File}; -#[cfg(test)] use std::io::Read; use std::ops::Drop; use std::os::raw::c_int; @@ -24,30 +30,39 @@ use std::os::raw::c_void; use std::path::Path; use std::str::FromStr; use std::str::Utf8Error; - -const UGOIRA_OK: c_int = _ugoira::UGOIRA_OK as c_int; -const UGOIRA_NULL_POINTER: c_int = _ugoira::UGOIRA_NULL_POINTER as c_int; -const UGOIRA_ZIP: c_int = _ugoira::UGOIRA_ZIP as c_int; -const UGOIRA_INVALID_MAX_FPS: c_int = _ugoira::UGOIRA_INVALID_MAX_FPS as c_int; -const UGOIRA_INVALID_FRAMES: c_int = _ugoira::UGOIRA_INVALID_FRAMES as c_int; -const UGOIRA_INVALID_CRF: c_int = _ugoira::UGOIRA_INVALID_CRF as c_int; -const UGOIRA_REMOVE_OUTPUT_FILE_FAILED: c_int = _ugoira::UGOIRA_REMOVE_OUTPUT_FILE_FAILED as c_int; -const UGOIRA_OOM: c_int = _ugoira::UGOIRA_OOM as c_int; -const UGOIRA_NO_VIDEO_STREAM: c_int = _ugoira::UGOIRA_NO_VIDEO_STREAM as c_int; -const UGOIRA_NO_AVAILABLE_DECODER: c_int = _ugoira::UGOIRA_NO_AVAILABLE_DECODER as c_int; -const UGOIRA_NO_AVAILABLE_ENCODER: c_int = _ugoira::UGOIRA_NO_AVAILABLE_ENCODER as c_int; -const UGOIRA_OPEN_FILE: c_int = _ugoira::UGOIRA_OPEN_FILE as c_int; -const UGOIRA_UNABLE_SCALE: c_int = _ugoira::UGOIRA_UNABLE_SCALE as c_int; - -#[derive(Debug, derive_more::From, PartialEq)] +use subprocess::ExitStatus; +use subprocess::Popen; +use subprocess::PopenConfig; +use subprocess::Redirection; + +const UGOIRA_OK: c_int = 0; +const UGOIRA_NULL_POINTER: c_int = 1; +const UGOIRA_ZIP: c_int = 2; +const UGOIRA_INVALID_MAX_FPS: c_int = 3; +const UGOIRA_INVALID_FRAMES: c_int = 4; +const UGOIRA_INVALID_CRF: c_int = 5; +const UGOIRA_REMOVE_OUTPUT_FILE_FAILED: c_int = 6; +const UGOIRA_OOM: c_int = 7; +const UGOIRA_NO_VIDEO_STREAM: c_int = 8; +const UGOIRA_NO_AVAILABLE_DECODER: c_int = 9; +const UGOIRA_NO_AVAILABLE_ENCODER: c_int = 10; +const UGOIRA_OPEN_FILE: c_int = 11; +const UGOIRA_UNABLE_SCALE: c_int = 12; +const UGOIRA_JSON_ERROR: c_int = 13; + +#[derive(Debug, derive_more::From)] pub enum UgoiraError { String(String), Utf8(Utf8Error), ToCStr(ToCStrError), + #[cfg(feature = "avdict")] FfmpegError(AVDictCodeError), CodeError(UgoiraCodeError), + #[cfg(feature = "ugoira")] ZipError(UgoiraZipError), + #[cfg(feature = "ugoira")] ZipError2(UgoiraZipError2), + Popen(subprocess::PopenError), } impl Display for UgoiraError { @@ -60,10 +75,14 @@ impl Display for UgoiraError { s )), Self::ToCStr(s) => f.write_fmt(format_args!("{}", s)), + #[cfg(feature = "avdict")] Self::FfmpegError(s) => f.write_fmt(format_args!("{}", s)), Self::CodeError(s) => f.write_fmt(format_args!("{}", s)), + #[cfg(feature = "ugoira")] Self::ZipError(s) => f.write_fmt(format_args!("{}", s)), + #[cfg(feature = "ugoira")] Self::ZipError2(s) => f.write_fmt(format_args!("{}", s)), + Self::Popen(p) => f.write_fmt(format_args!("{}", p)), } } } @@ -77,13 +96,17 @@ impl From<&str> for UgoiraError { impl From for UgoiraError { fn from(v: c_int) -> Self { if v < 0 { - Self::FfmpegError(AVDictCodeError::from(v)) + #[cfg(feature = "avdict")] + return Self::FfmpegError(AVDictCodeError::from(v)); + #[cfg(not(feature = "avdict"))] + Self::String(format!("Error code from ffmpeg: {}", v)) } else { Self::CodeError(UgoiraCodeError::from(v)) } } } +#[cfg(feature = "ugoira")] impl From<_ugoira::UgoiraError> for UgoiraError { fn from(v: _ugoira::UgoiraError) -> Self { if v.code < 0 { @@ -110,6 +133,7 @@ impl UgoiraCodeError { match self.code { UGOIRA_OK => "OK", UGOIRA_NULL_POINTER => gettext("Arguments have null pointers."), + UGOIRA_ZIP => "Libzip error", UGOIRA_INVALID_MAX_FPS => gettext("Invalid max fps."), UGOIRA_INVALID_FRAMES => gettext("Invalid frames."), UGOIRA_INVALID_CRF => gettext("Invalid crf."), @@ -120,6 +144,7 @@ impl UgoiraCodeError { UGOIRA_NO_AVAILABLE_ENCODER => gettext("No available encoder."), UGOIRA_OPEN_FILE => gettext("Failed to open output file."), UGOIRA_UNABLE_SCALE => gettext("Unable to scale image."), + UGOIRA_JSON_ERROR => gettext("Failed to parse JSON file."), _ => gettext("Unknown error."), } } @@ -143,11 +168,13 @@ impl From for UgoiraCodeError { } } +#[cfg(feature = "ugoira")] #[derive(Clone, Copy, Debug, PartialEq, PartialOrd)] pub struct UgoiraZipError { code: c_int, } +#[cfg(feature = "ugoira")] impl UgoiraZipError { pub fn to_str(&self) -> Result { let s = unsafe { _ugoira::ugoira_get_zip_err_msg(self.code) }; @@ -162,6 +189,7 @@ impl UgoiraZipError { } } +#[cfg(feature = "ugoira")] impl Display for UgoiraZipError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str( @@ -176,16 +204,19 @@ impl Display for UgoiraZipError { } } +#[cfg(feature = "ugoira")] impl From for UgoiraZipError { fn from(v: c_int) -> Self { Self { code: v } } } +#[cfg(feature = "ugoira")] pub struct UgoiraZipError2 { err: *mut _ugoira::zip_error_t, } +#[cfg(feature = "ugoira")] impl UgoiraZipError2 { pub fn to_str(&self) -> Result { if self.err.is_null() { @@ -203,6 +234,7 @@ impl UgoiraZipError2 { } } +#[cfg(feature = "ugoira")] impl Debug for UgoiraZipError2 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.err.is_null() { @@ -217,6 +249,7 @@ impl Debug for UgoiraZipError2 { } } +#[cfg(feature = "ugoira")] impl Display for UgoiraZipError2 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str( @@ -232,6 +265,7 @@ impl Display for UgoiraZipError2 { } } +#[cfg(feature = "ugoira")] impl Drop for UgoiraZipError2 { fn drop(&mut self) { if !self.err.is_null() { @@ -241,12 +275,14 @@ impl Drop for UgoiraZipError2 { } } +#[cfg(feature = "ugoira")] impl From<*mut _ugoira::zip_error_t> for UgoiraZipError2 { fn from(err: *mut _ugoira::zip_error_t) -> Self { Self { err } } } +#[cfg(feature = "ugoira")] impl PartialEq for UgoiraZipError2 { fn eq(&self, other: &Self) -> bool { if self.err.is_null() && other.err.is_null() { @@ -265,20 +301,25 @@ impl PartialEq for UgoiraZipError2 { } } +#[cfg(feature = "ugoira")] unsafe impl Send for UgoiraZipError2 {} +#[cfg(feature = "ugoira")] unsafe impl Sync for UgoiraZipError2 {} +#[cfg(feature = "ugoira")] impl ToRawHandle<_ugoira::zip_error_t> for UgoiraZipError2 { unsafe fn to_raw_handle(&self) -> *mut _ugoira::zip_error_t { self.err } } +#[cfg(feature = "ugoira")] pub struct UgoiraFrames { head: *mut _ugoira::UgoiraFrame, tail: *mut _ugoira::UgoiraFrame, } +#[cfg(feature = "ugoira")] #[allow(dead_code)] impl UgoiraFrames { pub fn new() -> Self { @@ -339,18 +380,21 @@ impl UgoiraFrames { } } +#[cfg(feature = "ugoira")] impl AsRef for UgoiraFrames { fn as_ref(&self) -> &Self { self } } +#[cfg(feature = "ugoira")] impl Default for UgoiraFrames { fn default() -> Self { Self::new() } } +#[cfg(feature = "ugoira")] impl Drop for UgoiraFrames { fn drop(&mut self) { if !self.head.is_null() { @@ -361,12 +405,14 @@ impl Drop for UgoiraFrames { } } +#[cfg(feature = "ugoira")] impl ToRawHandle<_ugoira::UgoiraFrame> for UgoiraFrames { unsafe fn to_raw_handle(&self) -> *mut _ugoira::UgoiraFrame { self.head } } +#[cfg(feature = "ugoira")] impl ToRawHandle<_ugoira::AVDictionary> for AVDict { unsafe fn to_raw_handle(&self) -> *mut _ugoira::AVDictionary { self.m as *mut _ugoira::AVDictionary @@ -447,6 +493,7 @@ impl TryFrom<&str> for X264Profile { } } +#[cfg(feature = "ugoira")] pub fn convert_ugoira_to_mp4< S: AsRef + ?Sized, D: AsRef + ?Sized, @@ -490,7 +537,94 @@ pub fn convert_ugoira_to_mp4< Ok(()) } -#[cfg(test)] +pub async fn convert_ugoira_to_mp4_subprocess< + B: AsRef + ?Sized, + S: AsRef + ?Sized, + D: AsRef + ?Sized, + J: AsRef + ?Sized, +>( + base: &B, + src: &S, + dest: &D, + json: &J, + max_fps: f32, + metadata: HashMap, + force_yuv420p: bool, + crf: Option, + profile: Option, +) -> Result<(), UgoiraError> { + let mut argv: Vec = Vec::with_capacity(5); + argv.push(base.as_ref().to_owned()); + argv.push(src.as_ref().to_owned()); + argv.push(dest.as_ref().to_owned()); + argv.push(json.as_ref().to_owned()); + argv.push(format!("-M{}", max_fps).into()); + for (k, v) in metadata { + argv.push("-m".into()); + argv.push(format!("{}={}", k, v).into()); + } + if force_yuv420p { + argv.push("-f".into()); + } + match crf { + Some(crf) => { + argv.push("--crf".into()); + argv.push(crf.to_string().into()); + } + None => {} + } + match profile { + Some(p) => { + if !p.is_auto() { + argv.push(format!("-P{}", p.as_str()).into()); + } + } + None => {} + } + log::debug!(target: "ugoira_cli", "Command line: {:?}", argv); + let mut p = Popen::create( + &argv, + PopenConfig { + stdin: Redirection::None, + stdout: Redirection::Pipe, + stderr: Redirection::Merge, + ..Default::default() + }, + )?; + let e = p.async_wait().await; + let is_ok = match &e { + ExitStatus::Exited(e) => *e == 0, + _ => false, + }; + match &mut p.stdout { + Some(f) => { + let mut s = String::new(); + match f.read_to_string(&mut s) { + Ok(_) => { + if is_ok { + log::debug!(target: "ugoira_cli", "Output:\n{}", s); + } else { + log::info!(target: "ugoira_cli", "Output:\n{}", s); + } + } + Err(_) => {} + } + } + None => {} + } + if !is_ok { + match e { + ExitStatus::Exited(e) => { + return Err(UgoiraError::from(UgoiraCodeError::from(e as c_int))); + } + _ => {} + } + return Err(UgoiraError::from(format!("Unknown exit status: {:?}", e))); + } + Ok(()) +} + +#[cfg(all(feature = "ugoira", test))] async fn get_ugoira_zip_error2() -> UgoiraZipError2 { let ugo = unsafe { _ugoira::new_ugoira_error() }; if ugo.is_null() { @@ -499,6 +633,7 @@ async fn get_ugoira_zip_error2() -> UgoiraZipError2 { UgoiraZipError2 { err: ugo } } +#[cfg(feature = "ugoira")] #[tokio::test] async fn test_ugoira_zip_error2() { let task = tokio::spawn(get_ugoira_zip_error2()); @@ -506,6 +641,7 @@ async fn test_ugoira_zip_error2() { assert!(re.to_str().is_ok()) } +#[cfg(feature = "ugoira")] #[test] fn test_ugoira_frames() { let mut f = UgoiraFrames::new(); @@ -520,12 +656,14 @@ fn test_ugoira_frames() { assert_eq!(1, f2.len()); } +#[cfg(feature = "ugoira")] #[test] fn test_ugoira_zip_error() { let e = UgoiraZipError::from(3); assert!(e.to_str().is_ok()) } +#[cfg(feature = "ugoira")] #[test] fn test_convert_ugoira_to_mp4() -> Result<(), UgoiraError> { let frames_path = Path::new("./testdata/74841737_frames.json"); @@ -557,3 +695,41 @@ fn test_convert_ugoira_to_mp4() -> Result<(), UgoiraError> { &metadata, ) } + +#[proc_macros::async_timeout_test(120s)] +#[tokio::test(flavor = "multi_thread")] +async fn test_convert_ugoira_to_mp4_subprocess() -> Result<(), UgoiraError> { + #[cfg(feature = "ugoira")] + let base = crate::utils::get_exe_path_else_current() + .join("../ugoira") + .to_str() + .unwrap() + .to_owned(); + #[cfg(not(feature = "ugoira"))] + let base = match std::env::var("UGOIRA") { + Ok(b) => b, + Err(_) => { + println!("No ugoira location specified, skip test."); + return Ok(()); + } + }; + let p = Path::new("./test"); + if !p.exists() { + let re = create_dir("./test"); + assert!(re.is_ok() || p.exists()); + } + let mut m = HashMap::new(); + m.insert(String::from("title"), String::from("動く nachoneko :3")); + convert_ugoira_to_mp4_subprocess( + &base, + "./testdata/74841737_ugoira600x600.zip", + "./test/74841737_sub.mp4", + "./testdata/74841737_frames.json", + 60.0, + m, + false, + None, + None, + ) + .await +}