mirror of
https://github.com/lifegpc/pixiv_downloader.git
synced 2026-06-06 05:49:01 +08:00
Add support to send compressed image
This commit is contained in:
@@ -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 {
|
||||
|
||||
10
src/opts.rs
10
src/opts.rs
@@ -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 {
|
||||
@@ -201,6 +203,7 @@ impl CommandOpts {
|
||||
connect_timeout: None,
|
||||
client_timeout: None,
|
||||
ffprobe: None,
|
||||
ffmpeg: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
},
|
||||
)
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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};
|
||||
@@ -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(""))
|
||||
@@ -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));
|
||||
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;
|
||||
}
|
||||
}
|
||||
let img = img.build().map_err(|_| "Failed to gen.")?;
|
||||
photos.push(InputMedia::from(img));
|
||||
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()
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user