Skip to content

Commit

Permalink
Add support to send image as file
Browse files Browse the repository at this point in the history
  • Loading branch information
lifegpc authored Oct 6, 2024
1 parent 0af1fdc commit 0bae8f2
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 11 deletions.
45 changes: 45 additions & 0 deletions src/db/push_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,48 @@ pub enum TelegramBackend {
Botapi(BotapiClientConfig),
}

fn default_max_side() -> i64 {
1920
}

fn default_quality() -> i8 {
1
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
/// Control how to compress photo
pub struct TelegramBigPhotoCompressConfig {
/// The pixels of lagest side
#[serde(default = "default_max_side")]
pub max_side: i64,
/// Image qulity
#[serde(default = "default_quality")]
pub quality: i8,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
/// How to send photo which are too big to telegram.
/// Some photo always send as document.
pub enum TelegramBigPhotoSendMethod {
/// Compress image and send as a photo
Compress(TelegramBigPhotoCompressConfig),
/// Send a file as document
Document,
}

impl TelegramBigPhotoSendMethod {
/// Returns true if send a file as document
pub fn is_document(&self) -> bool {
matches!(self, Self::Document)
}
}

fn default_big_photo() -> TelegramBigPhotoSendMethod {
TelegramBigPhotoSendMethod::Document
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TelegramPushConfig {
Expand Down Expand Up @@ -268,6 +310,9 @@ pub struct TelegramPushConfig {
/// Add pixiv tag link to tag
#[serde(default = "default_false")]
pub add_link_to_tag: bool,
/// Control how to send big photo
#[serde(default = "default_big_photo")]
pub big_photo: TelegramBigPhotoSendMethod,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down
15 changes: 15 additions & 0 deletions src/opthelper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,21 @@ impl OptHelper {
.unwrap_or(30_000),
)
}

/// The path to ffprobe executable.
pub fn ffprobe(&self) -> Option<String> {
match &self.opt.get_ref().ffprobe {
Some(s) => Some(s.clone()),
None => match self.settings.get_ref().get_str("ffprobe") {
Some(s) => Some(s.clone()),
None => {
#[cfg(feature = "docker")]
return Some(String::from("ffprobe"));
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 @@ -144,6 +144,8 @@ pub struct CommandOpts {
/// The timeout is applied from when the request starts connecting until the response body
/// has finished. Not used for downloader.
pub client_timeout: Option<u64>,
/// The path to ffprobe executable.
pub ffprobe: Option<String>,
}

impl CommandOpts {
Expand Down Expand Up @@ -198,6 +200,7 @@ impl CommandOpts {
ugoira_cli: None,
connect_timeout: None,
client_timeout: None,
ffprobe: None,
}
}

Expand Down Expand Up @@ -737,6 +740,12 @@ pub fn parse_cmd() -> Option<CommandOpts> {
"TIME",
);
opts.optopt("", "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."), "TIME");
opts.optopt(
"",
"ffprobe",
gettext("The path to ffprobe executable."),
"PATH",
);
let result = match opts.parse(&argv[1..]) {
Ok(m) => m,
Err(err) => {
Expand Down Expand Up @@ -1207,6 +1216,7 @@ pub fn parse_cmd() -> Option<CommandOpts> {
return None;
}
}
re.as_mut().unwrap().ffprobe = result.opt_str("ffprobe");
re
}

Expand Down
126 changes: 126 additions & 0 deletions src/push/telegram/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
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};

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,
}

impl SupportedImage {
pub fn new(supported: bool, size_too_big: bool) -> Self {
Self {
supported,
size_too_big,
}
}
}

pub async fn get_image_size<S: AsRef<OsStr> + ?Sized, P: AsRef<OsStr> + ?Sized>(
ffprobe: &S,
file: &P,
) -> Result<(i64, i64), PixivDownloaderError> {
let argv = [
ffprobe.as_ref().to_owned(),
"-v".into(),
"error".into(),
"-select_streams".into(),
"v:0".into(),
"-show_entries".into(),
"stream=width,height".into(),
"-of".into(),
"csv=s=x:p=0".into(),
file.as_ref().to_owned(),
];
let mut p = Popen::create(
&argv,
PopenConfig {
stdin: Redirection::None,
stdout: Redirection::Pipe,
stderr: Redirection::None,
..PopenConfig::default()
},
)
.try_err4("Failed to create popen: ")?;
let re = p.async_wait().await;
if !re.success() {
log::error!(target: "telegram_image", "Failed to get image size for {}: {:?}.", file.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", "Ffprobe output: {}", s);
}
None => {}
}
return Err(PixivDownloaderError::from("Failed to get image size."));
}
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 => {
log::warn!(target: "telegram_image", "No output for ffprobe.");
return Err(PixivDownloaderError::from("No output for ffprobe."));
}
};
log::debug!(target: "telegram_image", "Ffprobe output: {}", s);
let s: Vec<_> = s.trim().split('x').collect();
if s.len() != 2 {
return Err(PixivDownloaderError::from("Too many output for ffprobe."));
}
Ok((s[0].parse()?, s[1].parse()?))
}

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;
Ok(if w / h >= 20.0 || h / w >= 20.0 {
SupportedImage::new(false, false)
} else if width + height >= 10000 {
SupportedImage::new(false, true)
} else {
let meta = tokio::fs::metadata(path.as_ref()).await?;
if meta.len() >= MAX_PHOTO_SIZE {
SupportedImage::new(false, true)
} else {
SupportedImage::new(true, false)
}
})
}
2 changes: 2 additions & 0 deletions src/push/telegram/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/// Telegram Bot API Client
pub mod botapi_client;
/// Handle images
pub mod image;
/// Split long messages into multiple messages if needed (Only supports HTML messages)
pub mod text;
/// Telegram params type
Expand Down
53 changes: 43 additions & 10 deletions src/server/push/task/pixiv_send_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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::text::{encode_data, TextSpliter};
use crate::push::telegram::tg_type::{
InputFile, InputMedia, InputMediaPhotoBuilder, ParseMode, ReplyParametersBuilder,
Expand Down Expand Up @@ -199,7 +200,8 @@ impl RunContext {
&self,
index: u64,
download_media: bool,
) -> Result<Option<InputFile>, PixivDownloaderError> {
cfg: &TelegramPushConfig,
) -> Result<Option<(InputFile, bool)>, PixivDownloaderError> {
if download_media {
match self._get_image_url(index) {
Some(u) => match self
Expand All @@ -209,6 +211,20 @@ impl RunContext {
.await
{
Ok(p) => {
let (is_supported, too_big) = match is_supported_image(&p).await {
Ok(s) => (s.supported, s.size_too_big),
Err(e) => {
log::warn!(target: "pixiv_send_message", "Failed to test image is supported by using ffprobe: {}", e);
let meta = tokio::fs::metadata(&p).await?;
if meta.len() >= MAX_PHOTO_SIZE {
(false, false)
} else {
(true, false)
}
}
};
let send_as_file =
!is_supported && (!too_big || cfg.big_photo.is_document());
let name = p
.file_name()
.map(|a| a.to_str().unwrap_or(""))
Expand All @@ -219,15 +235,15 @@ impl RunContext {
.filename(name)
.build()
.map_err(|_| "Failed to create FormDataPart.")?;
Ok(Some(InputFile::Content(f)))
Ok(Some((InputFile::Content(f), send_as_file)))
}
Err(e) => Err(e),
},
None => Ok(None),
}
} else {
match self.get_image_url(index).await {
Ok(Some(u)) => Ok(Some(InputFile::URL(u))),
Ok(Some(u)) => Ok(Some((InputFile::URL(u), false))),
Ok(None) => Ok(None),
Err(e) => Err(e),
}
Expand Down Expand Up @@ -831,8 +847,8 @@ impl RunContext {
let mut last_message_id: Option<i64> = None;
let download_media = cfg.download_media.unwrap_or(c.is_custom());
if len == 1 {
let f = self
.get_input_file(0, download_media)
let (f, send_as_file) = self
.get_input_file(0, download_media, cfg)
.await?
.ok_or("Failed to get image.")?;
let r = match last_message_id {
Expand All @@ -845,8 +861,24 @@ impl RunContext {
None => None,
};
let text = ts.to_html(None);
let m = c
.send_photo(
let m = if send_as_file {
c.send_document(
&cfg.chat_id,
cfg.message_thread_id,
f,
None,
Some(text.as_str()),
Some(ParseMode::HTML),
None,
Some(cfg.disable_notification),
Some(cfg.protect_content),
None,
r.as_ref(),
)
.await?
.to_result()?
} else {
c.send_photo(
&cfg.chat_id,
cfg.message_thread_id,
f,
Expand All @@ -860,15 +892,16 @@ impl RunContext {
r.as_ref(),
)
.await?
.to_result()?;
.to_result()?
};
last_message_id = Some(m.message_id);
} else {
let mut i = 0u64;
let mut photos = Vec::new();
let mut photo_files = Vec::new();
while i < len {
let f = self
.get_input_file(i, download_media)
let (f, send_as_file) = self
.get_input_file(i, download_media, cfg)
.await?
.ok_or("Failed to get image.")?;
let u = match f {
Expand Down
1 change: 0 additions & 1 deletion src/settings_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ pub fn get_settings_list() -> Vec<SettingDes> {
SettingDes::new("push-task-max-push-count", gettext("The maximum number of tasks to push to client at the same time."), JsonValueType::Number, Some(check_nozero_usize)).unwrap(),
SettingDes::new("fanbox-http-headers", gettext("Extra http headers for fanbox.cc."), JsonValueType::Object, Some(check_header_map)).unwrap(),
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")]
Expand Down
Loading

0 comments on commit 0bae8f2

Please sign in to comment.