Skip to content

Commit

Permalink
Add support to send compressed image
Browse files Browse the repository at this point in the history
  • Loading branch information
lifegpc authored Oct 6, 2024
1 parent f75f2df commit b24632d
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 40 deletions.
15 changes: 15 additions & 0 deletions src/opthelper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,21 @@ impl OptHelper {
},
}
}

/// The path to ffmpeg executable.
pub fn ffmpeg(&self) -> Option<String> {
match &self.opt.get_ref().ffmpeg {
Some(s) => Some(s.clone()),
None => match self.settings.get_ref().get_str("ffmpeg") {
Some(s) => Some(s.clone()),
None => {
#[cfg(feature = "docker")]
return Some(String::from("ffmpeg"));
None
}
},
}
}
}

impl Default for OptHelper {
Expand Down
10 changes: 10 additions & 0 deletions src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ pub struct CommandOpts {
pub client_timeout: Option<u64>,
/// The path to ffprobe executable.
pub ffprobe: Option<String>,
/// The path to ffmpeg executable.
pub ffmpeg: Option<String>,
}

impl CommandOpts {
Expand Down Expand Up @@ -201,6 +203,7 @@ impl CommandOpts {
connect_timeout: None,
client_timeout: None,
ffprobe: None,
ffmpeg: None,
}
}

Expand Down Expand Up @@ -746,6 +749,12 @@ pub fn parse_cmd() -> Option<CommandOpts> {
gettext("The path to ffprobe executable."),
"PATH",
);
opts.optopt(
"",
"ffmpeg",
gettext("The path to ffmpeg executable."),
"PATH",
);
let result = match opts.parse(&argv[1..]) {
Ok(m) => m,
Err(err) => {
Expand Down Expand Up @@ -1217,6 +1226,7 @@ pub fn parse_cmd() -> Option<CommandOpts> {
}
}
re.as_mut().unwrap().ffprobe = result.opt_str("ffprobe");
re.as_mut().unwrap().ffmpeg = result.opt_str("ffmpeg");
re
}

Expand Down
113 changes: 87 additions & 26 deletions src/push/telegram/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,11 @@ use crate::error::PixivDownloaderError;
use crate::ext::subprocess::PopenAsyncExt;
use crate::ext::try_err::TryErr4;
use crate::get_helper;
use std::{ffi::OsStr, io::Read};
use subprocess::{ExitStatus, Popen, PopenConfig, Redirection};
use std::{ffi::OsStr, io::Read, path::PathBuf};
use subprocess::{Popen, PopenConfig, Redirection};

pub const MAX_PHOTO_SIZE: u64 = 10485760;

pub async fn check_ffprobe<S: AsRef<str> + ?Sized>(path: &S) -> Result<bool, PixivDownloaderError> {
let mut p = Popen::create(
&[path.as_ref(), "-h"],
PopenConfig {
stdin: Redirection::None,
stdout: Redirection::Pipe,
stderr: Redirection::Pipe,
..PopenConfig::default()
},
)
.try_err4("Failed to create popen: ")?;
p.communicate(None)?;
let re = p.async_wait().await;
Ok(match re {
ExitStatus::Exited(o) => o == 0,
_ => false,
})
}

pub struct SupportedImage {
pub supported: bool,
pub size_too_big: bool,
Expand Down Expand Up @@ -61,7 +42,7 @@ pub async fn get_image_size<S: AsRef<OsStr> + ?Sized, P: AsRef<OsStr> + ?Sized>(
PopenConfig {
stdin: Redirection::None,
stdout: Redirection::Pipe,
stderr: Redirection::None,
stderr: Redirection::Pipe,
..PopenConfig::default()
},
)
Expand Down Expand Up @@ -99,15 +80,95 @@ pub async fn get_image_size<S: AsRef<OsStr> + ?Sized, P: AsRef<OsStr> + ?Sized>(
Ok((s[0].parse()?, s[1].parse()?))
}

pub async fn generate_image<S: AsRef<OsStr> + ?Sized, D: AsRef<OsStr> + ?Sized>(
src: &S,
dest: &D,
max_side: i64,
quality: i8,
) -> Result<(), PixivDownloaderError> {
let helper = get_helper();
let ffprobe = helper.ffprobe().unwrap_or(String::from("ffprobe"));
let (width, height) = get_image_size(&ffprobe, src).await?;
let ffmpeg = helper.ffmpeg().unwrap_or(String::from("ffmpeg"));
let (w, h) = if width > height {
(max_side, max_side * height / width)
} else {
(max_side * width / height, max_side)
};
let argv = [
ffmpeg.into(),
"-n".into(),
"-i".into(),
src.as_ref().to_owned(),
"-vf".into(),
format!("scale={}x{}", w, h).into(),
"-qmin".into(),
format!("{}", quality).into(),
"-qmax".into(),
format!("{}", quality).into(),
dest.as_ref().to_owned(),
];
let mut p = Popen::create(
&argv,
PopenConfig {
stdin: Redirection::None,
stdout: Redirection::Pipe,
stderr: Redirection::Pipe,
..PopenConfig::default()
},
)
.try_err4("Failed to create popen: ")?;
let re = p.async_wait().await;
if !re.success() {
log::error!(target: "telegram_image", "Failed to generate thumbnail for {}: {:?}.", src.as_ref().to_string_lossy(), re);
match &mut p.stdout {
Some(f) => {
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
let s = String::from_utf8_lossy(&buf);
log::info!(target: "telegram_image", "ffmpeg output: {}", s);
}
None => {}
}
return Err(PixivDownloaderError::from("Failed to generate thumbnail."));
}
let s = match &mut p.stdout {
Some(f) => {
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
String::from_utf8_lossy(&buf).into_owned()
}
None => String::new(),
};
log::debug!(target: "telegram_image", "Ffmpeg output: {}", s);
Ok(())
}

pub fn get_thumbnail_filename(
ori: &PathBuf,
max_side: i64,
quality: i8,
) -> Result<PathBuf, PixivDownloaderError> {
let mut o = ori.to_path_buf();
let filename = o
.as_path()
.file_stem()
.ok_or("No filename in path.")?
.to_owned();
o.set_file_name(format!(
"{}-{}-q{}.jpg",
filename.to_string_lossy(),
max_side,
quality
));
Ok(o)
}

pub async fn is_supported_image<S: AsRef<OsStr> + ?Sized>(
path: &S,
) -> Result<SupportedImage, PixivDownloaderError> {
let helper = get_helper();
let ffprobe = helper.ffprobe().unwrap_or(String::from("ffprobe"));
let re = check_ffprobe(&ffprobe).await?;
if !re {
return Err(PixivDownloaderError::from("ffprobe seems not works."));
}
let (width, height) = get_image_size(&ffprobe, path).await?;
let w = width as f64;
let h = height as f64;
Expand Down
127 changes: 113 additions & 14 deletions src/server/push/task/pixiv_send_message.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::super::super::preclude::*;
use crate::db::push_task::{
AuthorLocation, EveryPushConfig, PushConfig, PushDeerConfig, TelegramBackend,
TelegramPushConfig,
TelegramBigPhotoSendMethod, TelegramPushConfig,
};
use crate::error::PixivDownloaderError;
use crate::formdata::FormDataPartBuilder;
Expand All @@ -12,10 +12,13 @@ use crate::pixivapp::illust::PixivAppIllust;
use crate::push::every_push::{EveryPushClient, EveryPushTextType};
use crate::push::pushdeer::PushdeerClient;
use crate::push::telegram::botapi_client::{BotapiClient, BotapiClientConfig};
use crate::push::telegram::image::{is_supported_image, MAX_PHOTO_SIZE};
use crate::push::telegram::image::{
generate_image, get_thumbnail_filename, is_supported_image, MAX_PHOTO_SIZE,
};
use crate::push::telegram::text::{encode_data, TextSpliter};
use crate::push::telegram::tg_type::{
InputFile, InputMedia, InputMediaPhotoBuilder, ParseMode, ReplyParametersBuilder,
InputFile, InputMedia, InputMediaDocumentBuilder, InputMediaPhotoBuilder, ParseMode,
ReplyParametersBuilder,
};
use crate::utils::{get_file_name_from_url, parse_pixiv_id};
use crate::{get_helper, gettext};
Expand Down Expand Up @@ -225,6 +228,50 @@ impl RunContext {
};
let send_as_file =
!is_supported && (!too_big || cfg.big_photo.is_document());
let p = if !is_supported && !send_as_file {
match &cfg.big_photo {
TelegramBigPhotoSendMethod::Compress(c) => {
if let Ok(filename) =
get_thumbnail_filename(&p, c.max_side, c.quality)
{
let fn1 = filename.to_string_lossy();
let o = match self.ctx.tmp_cache.get_local_cache(&fn1).await
{
Ok(o) => o,
Err(_) => None,
};
match o {
Some(o) => o,
None => {
match generate_image(
&p, &filename, c.max_side, c.quality,
)
.await
{
Ok(_) => {
let _ = self
.ctx
.tmp_cache
.push_local_cache(&fn1)
.await;
filename
}
Err(e) => {
log::warn!(target: "pixiv_send_message", "Failed to generate thumbnial: {}", e);
p
}
}
}
}
} else {
p
}
}
TelegramBigPhotoSendMethod::Document => p,
}
} else {
p
};
let name = p
.file_name()
.map(|a| a.to_str().unwrap_or(""))
Expand Down Expand Up @@ -899,28 +946,74 @@ impl RunContext {
let mut i = 0u64;
let mut photos = Vec::new();
let mut photo_files = Vec::new();
let mut new_photos = Vec::new();
let mut new_photo_files = Vec::new();
let mut have_doc = false;
let mut have_nondoc = false;
let mut new_have_doc = false;
let mut new_have_nondoc = false;
while i < len {
let (f, send_as_file) = self
.get_input_file(i, download_media, cfg)
.await?
.ok_or("Failed to get image.")?;
let mut is_content = false;
let u = match f {
InputFile::URL(u) => u,
InputFile::Content(c) => {
photo_files.push((format!("img{}", i), c));
is_content = true;
format!("attach://img{}", i)
}
};
let mut img = InputMediaPhotoBuilder::default();
img.media(u).has_spoiler(is_r18);
if photos.is_empty() {
let text = ts.to_html(None);
img.caption(Some(text)).parse_mode(Some(ParseMode::HTML));
}
let img = img.build().map_err(|_| "Failed to gen.")?;
photos.push(InputMedia::from(img));
if send_as_file {
let mut doc = InputMediaDocumentBuilder::default();
doc.media(u);
if photos.is_empty() {
let text = ts.to_html(None);
doc.caption(Some(text)).parse_mode(Some(ParseMode::HTML));
}
let doc = doc.build().map_err(|_| "Failed to gen.")?;
if have_nondoc {
new_photos.push(InputMedia::from(doc));
if is_content {
match photo_files.pop() {
Some(p) => new_photo_files.push(p),
None => {}
}
}
new_have_doc = true;
} else {
photos.push(InputMedia::from(doc));
have_doc = true;
}
} else {
let mut img = InputMediaPhotoBuilder::default();
img.media(u).has_spoiler(is_r18);
if photos.is_empty() {
let text = ts.to_html(None);
img.caption(Some(text)).parse_mode(Some(ParseMode::HTML));
}
let img = img.build().map_err(|_| "Failed to gen.")?;
if have_doc {
new_photos.push(InputMedia::from(img));
if is_content {
match photo_files.pop() {
Some(p) => new_photo_files.push(p),
None => {}
}
}
new_have_nondoc = true;
} else {
photos.push(InputMedia::from(img));
have_nondoc = true;
}
}
i += 1;
if i == len || photos.len() == 10 {
while (i == len && !photos.is_empty())
|| photos.len() == 10
|| !new_photos.is_empty()
{
let r = match last_message_id {
Some(m) => Some(
ReplyParametersBuilder::default()
Expand All @@ -944,8 +1037,14 @@ impl RunContext {
.await?
.to_result()?;
last_message_id = m.first().map(|m| m.message_id);
photos = Vec::new();
photo_files = Vec::new();
photos = new_photos;
photo_files = new_photo_files;
new_photos = Vec::new();
new_photo_files = Vec::new();
have_doc = new_have_doc;
have_nondoc = new_have_nondoc;
new_have_doc = false;
new_have_nondoc = false;
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/settings_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub fn get_settings_list() -> Vec<SettingDes> {
SettingDes::new("ugoira-cli", gettext("Whether to use ugoira cli."), JsonValueType::Boolean, None).unwrap(),
SettingDes::new("connect-timeout", gettext("Set a timeout in milliseconds for only the connect phase of a client."), JsonValueType::Number, Some(check_nonzero_u64)).unwrap(),
SettingDes::new("client-timeout", gettext("Set request timeout in milliseconds. The timeout is applied from when the request starts connecting until the response body has finished. Not used for downloader."), JsonValueType::Number, Some(check_nonzero_u64)).unwrap(),
SettingDes::new("ffmpeg", gettext("The path to ffmpeg executable."), JsonValueType::Str, None).unwrap(),
]
}

Expand Down

0 comments on commit b24632d

Please sign in to comment.