diff --git a/exif/exif.h b/exif/exif.h index b0d2e39..d8ce9f9 100644 --- a/exif/exif.h +++ b/exif/exif.h @@ -29,6 +29,7 @@ typedef struct ExifDataRef ExifDataRef; EXIF_API ExifImage* create_exif_image(const char* path); EXIF_API ExifDataRef* exif_image_get_exif_data(ExifImage* image); +EXIF_API int exif_image_read_metadata(ExifImage* image); EXIF_API int exif_image_set_exif_data(ExifImage* image, ExifData* data); EXIF_API int exif_image_write_metadata(ExifImage* image); EXIF_API void free_exif_image(ExifImage* img); diff --git a/exif/src/exif.cpp b/exif/src/exif.cpp index ac0e7c4..932d95c 100644 --- a/exif/src/exif.cpp +++ b/exif/src/exif.cpp @@ -30,6 +30,17 @@ ExifDataRef* exif_image_get_exif_data(ExifImage* image) { return &image->exif_data_ref; } +int exif_image_read_metadata(ExifImage* image) { + if (!image) return 1; + try { + image->image->readMetadata(); + } catch (std::exception& e) { + printf("%s\n", e.what()); + return 1; + } + return 0; +} + int exif_image_set_exif_data(ExifImage* image, ExifData* data) { if (!image || !data) return 1; image->image->setExifData(data->data); diff --git a/proc_macros/proc_macros.rs b/proc_macros/proc_macros.rs index 7529c79..edd69f1 100644 --- a/proc_macros/proc_macros.rs +++ b/proc_macros/proc_macros.rs @@ -701,3 +701,37 @@ pub fn derive_check_unknown(item: TokenStream) -> TokenStream { ); stream.into() } + +struct CreateFanboxDownloadHelper { + pub fn_name: Ident, +} + +impl Parse for CreateFanboxDownloadHelper { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let fn_name = input.parse()?; + Ok(Self { fn_name }) + } +} + +#[proc_macro] +pub fn create_fanbox_download_helper(item: TokenStream) -> TokenStream { + let CreateFanboxDownloadHelper { fn_name } = + parse_macro_input!(item as CreateFanboxDownloadHelper); + let defn_name = Ident::new( + format!("download_{}", fn_name.to_string()).as_str(), + fn_name.span(), + ); + let stream = quote!( + #[inline] + pub fn #defn_name(&self) -> Result, crate::downloader::DownloaderError> { + match self.#fn_name() { + Some(url) => { + let client: &std::sync::Arc = self.client.as_ref().as_ref(); + Ok(Some(crate::downloader::DownloaderHelper::builder(url)?.client(client).build())) + } + None => Ok(None), + } + } + ); + stream.into() +} diff --git a/src/data/data.rs b/src/data/data.rs index 7b21029..5293d77 100644 --- a/src/data/data.rs +++ b/src/data/data.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "exif")] +use super::exif::ExifDataSource; use crate::gettext; use crate::opt::author_name_filter::AuthorFiler; use crate::opthelper::get_helper; @@ -84,3 +86,22 @@ impl PixivData { } } } + +#[cfg(feature = "exif")] +impl ExifDataSource for PixivData { + fn image_author(&self) -> Option { + self.author.clone() + } + + fn image_comment(&self) -> Option { + self.description.clone() + } + + fn image_id(&self) -> Option { + Some(self.id.to_link()) + } + + fn image_title(&self) -> Option { + self.title.clone() + } +} diff --git a/src/data/exif.rs b/src/data/exif.rs index cab22d9..3111603 100644 --- a/src/data/exif.rs +++ b/src/data/exif.rs @@ -1,18 +1,39 @@ -use crate::data::data::PixivData; use crate::exif::ExifByteOrder; use crate::exif::ExifData; use crate::exif::ExifImage; use crate::exif::ExifKey; use crate::exif::ExifTypeID; use crate::exif::ExifValue; +use crate::ext::try_err::TryErr; use crate::parser::description::parse_description; use std::convert::TryFrom; use std::ffi::OsStr; use utf16string::LittleEndian; use utf16string::WString; -fn add_image_id(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { - let link = d.id.to_link(); +pub trait ExifDataSource { + fn image_author(&self) -> Option { + None + } + + fn image_comment(&self) -> Option { + None + } + + fn image_id(&self) -> Option { + None + } + + fn image_title(&self) -> Option { + None + } +} + +fn add_image_id(data: &mut ExifData, d: &D) -> Result<(), ()> { + let link = match d.image_id() { + Some(link) => link, + None => return Ok(()), + }; let key = ExifKey::try_from("Exif.Image.ImageID")?; let mut value = ExifValue::try_from(ExifTypeID::AsciiString)?; value.read(link.as_bytes(), None)?; @@ -20,31 +41,33 @@ fn add_image_id(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { Ok(()) } -fn add_image_title(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { - if d.title.is_none() { - return Ok(()); - } - let title = d.title.as_ref().unwrap(); +fn add_image_title(data: &mut ExifData, d: &D) -> Result<(), ()> { + let title = match d.image_title() { + Some(title) => title, + None => return Ok(()), + }; let key = ExifKey::try_from("Exif.Image.ImageDescription")?; let mut value = ExifValue::try_from(ExifTypeID::AsciiString)?; value.read(title.as_bytes(), None)?; data.add(&key, &value)?; let key = ExifKey::try_from("Exif.Image.XPTitle")?; let mut value = ExifValue::try_from(ExifTypeID::BYTE)?; - let s: WString = WString::from(title); + let s: WString = WString::from(title.as_str()); value.read(s.as_bytes(), None)?; data.add(&key, &value)?; Ok(()) } -fn add_image_author(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { - if d.author.is_none() { - return Ok(()); - } - let author = d.author.as_ref().unwrap(); +fn add_image_author(data: &mut ExifData, d: &D) -> Result<(), ()> { + let author = match d.image_author() { + Some(author) => author, + None => { + return Ok(()); + } + }; let key = ExifKey::try_from("Exif.Image.XPAuthor")?; let mut value = ExifValue::try_from(ExifTypeID::BYTE)?; - let s: WString = WString::from(author); + let s: WString = WString::from(author.as_str()); value.read(s.as_bytes(), None)?; data.add(&key, &value)?; let key = ExifKey::try_from("Exif.Image.Artist")?; @@ -54,19 +77,20 @@ fn add_image_author(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { Ok(()) } -fn add_image_comment(data: &mut ExifData, d: &PixivData) -> Result<(), ()> { - if d.description.is_none() { - return Ok(()); - } - let desc = parse_description(d.description.as_ref().unwrap()); - let desc = if desc.is_some() { - desc.as_ref().unwrap() - } else { - d.description.as_ref().unwrap() +fn add_image_comment(data: &mut ExifData, d: &D) -> Result<(), ()> { + let odesc = match d.image_comment() { + Some(desc) => desc, + None => { + return Ok(()); + } + }; + let desc = match parse_description(odesc.as_str()) { + Some(desc) => desc, + None => odesc, }; let key = ExifKey::try_from("Exif.Image.XPComment")?; let mut value = ExifValue::try_from(ExifTypeID::BYTE)?; - let s: WString = WString::from(desc); + let s: WString = WString::from(desc.as_str()); value.read(s.as_bytes(), None)?; data.add(&key, &value)?; Ok(()) @@ -80,13 +104,14 @@ fn add_image_page(data: &mut ExifData, page: u16) -> Result<(), ()> { Ok(()) } -pub fn add_exifdata_to_image + ?Sized>( +pub fn add_exifdata_to_image + ?Sized, D: ExifDataSource>( file_name: &S, - data: &PixivData, + data: &D, page: u16, ) -> Result<(), ()> { let mut f = ExifImage::new(file_name)?; - let mut d = ExifData::new()?; + f.read_metadata()?; + let mut d = f.exif_data().try_err(())?.to_owned(); add_image_id(&mut d, data)?; add_image_title(&mut d, data)?; add_image_author(&mut d, data)?; diff --git a/src/download.rs b/src/download.rs index bec3e7e..14780ab 100644 --- a/src/download.rs +++ b/src/download.rs @@ -28,6 +28,7 @@ use crate::Main; use indicatif::MultiProgress; use json::JsonValue; use reqwest::IntoUrl; +use std::fs::create_dir; use std::path::PathBuf; use std::sync::Arc; @@ -130,7 +131,7 @@ impl Main { if d.is_downloaded() { #[cfg(feature = "exif")] { - if add_exifdata_to_image(&file_name, &datas, np).is_err() { + if add_exifdata_to_image(&file_name, &*datas, np).is_err() { println!( "{} {}", gettext("Failed to add exif data to image:"), @@ -149,7 +150,7 @@ impl Main { #[cfg(feature = "exif")] { if helper.update_exif() && file_name.exists() { - if add_exifdata_to_image(&file_name, &datas, np).is_err() { + if add_exifdata_to_image(&file_name, &*datas, np).is_err() { println!( "{} {}", gettext("Failed to add exif data to image:"), @@ -402,16 +403,36 @@ impl Main { // #TODO allow to continue return Ok(()); } - let base = Arc::new(PathBuf::from(format!("./{}", id.post_id))); - let json_file = base.join("body.json"); + let base = Arc::new(PathBuf::from(format!("./{}/{}", id.creator_id, id.post_id))); + let json_file = base.join("data.json"); let data = FanboxData::new(id, &post).try_err("Failed to create data file.")?; let data_file = JSONDataFile::from(data); + if !base.exists() { + match create_dir(&*base) { + Ok(_) => {} + Err(e) => { + if !base.exists() { + return Err(PixivDownloaderError::from(e)); + } + } + } + } data_file .save(&json_file) .try_err(gettext("Failed to save post data to file."))?; match post { FanboxPost::Article(article) => {} - FanboxPost::Image(img) => {} + FanboxPost::Image(img) => { + let body = img + .body() + .try_err(gettext("Failed to get the body of image post."))?; + let text = body + .text() + .try_err(gettext("Failed to get the text of the image post."))?; + let images = body + .images() + .try_err(gettext("Failed to get images from the image post."))?; + } FanboxPost::Unknown(_) => { return Err(PixivDownloaderError::from(gettext( "Unrecognized post type.", diff --git a/src/downloader/helper.rs b/src/downloader/helper.rs new file mode 100644 index 0000000..0ee9f5f --- /dev/null +++ b/src/downloader/helper.rs @@ -0,0 +1,165 @@ +use super::downloader::Downloader; +use super::enums::DownloaderResult; +use super::error::DownloaderError; +use super::local_file::LocalFile; +use crate::ext::replace::ReplaceWith; +use crate::gettext; +use crate::webclient::ToHeaders; +use crate::webclient::WebClient; +use reqwest::IntoUrl; +use std::convert::TryFrom; +use std::path::Path; +use std::sync::Arc; +use url::Url; + +/// Store download information +pub struct DownloaderHelper { + /// Url + pub url: Url, + /// Web client + pub client: Option>, + /// New headers wants to apply + pub headers: Option>, + /// Recommand file name + pub file_name: Option>>, +} + +pub struct DownloaderHelperBuilder { + helper: DownloaderHelper, +} + +impl DownloaderHelper { + pub fn builder(url: U) -> Result { + Ok(DownloaderHelperBuilder { + helper: Self { + url: url.into_url()?, + client: None, + headers: None, + file_name: None, + }, + }) + } + + /// Get A local [Downloader] + /// * `overwrite` - Whether to overwrite file + /// * `base` - The base directory to store downloaded files. + pub fn download_local + ?Sized>( + &self, + overwrite: Option, + base: &P, + ) -> Result>, DownloaderError> { + let base = base.as_ref(); + let file = match &self.file_name { + Some(file_name) => base.join(file_name.as_ref()), + None => match crate::utils::get_file_name_from_url(self.url.clone()) { + Some(file_name) => base.join(file_name), + None => { + return Err(DownloaderError::from(gettext( + "Failed to get file name from url.", + ))); + } + }, + }; + let headers = match &self.headers { + Some(headers) => headers.to_headers(), + None => None, + }; + match &self.client { + Some(client) => Downloader::::new2( + Arc::clone(client), + self.url.clone(), + headers, + Some(&file), + overwrite, + ), + None => Downloader::::new(self.url.clone(), headers, Some(&file), overwrite), + } + } + + pub fn set_client(&mut self, client: &Arc) { + self.client.replace(Arc::clone(client)); + } + + pub fn set_file_name + ?Sized>(&mut self, p: &P) { + self.file_name.replace(Box::new(p.as_ref().to_owned())); + } + + pub fn set_headers(&mut self, headers: H) { + self.headers.replace_with(match headers.to_headers() { + Some(headers) => Some(Box::new(headers)), + None => None, + }); + } + + pub fn set_no_client(&mut self) { + self.client.take(); + } + + pub fn set_no_file_name(&mut self) { + self.file_name.take(); + } + + pub fn set_no_headers(&mut self) { + self.headers.take(); + } +} + +impl DownloaderHelperBuilder { + pub fn build(self) -> DownloaderHelper { + self.helper + } + + pub fn client(mut self, client: &Arc) -> Self { + self.helper.set_client(client); + self + } + + pub fn file_name + ?Sized>(mut self, p: &P) -> Self { + self.helper.set_file_name(p); + self + } + + pub fn headers(mut self, headers: H) -> Self { + self.helper.set_headers(headers); + self + } + + pub fn no_client(mut self) -> Self { + self.helper.set_no_client(); + self + } + + pub fn no_file_name(mut self) -> Self { + self.helper.set_no_file_name(); + self + } + + pub fn no_headers(mut self) -> Self { + self.helper.set_no_headers(); + self + } +} + +impl From for DownloaderHelper { + fn from(url: Url) -> Self { + Self { + url, + client: None, + headers: None, + file_name: None, + } + } +} + +impl From<&Url> for DownloaderHelper { + fn from(u: &Url) -> Self { + Self::from(u.clone()) + } +} + +impl<'a> TryFrom<&'a str> for DownloaderHelper { + type Error = url::ParseError; + fn try_from(value: &'a str) -> Result { + Ok(Self::from(Url::parse(value)?)) + } +} diff --git a/src/downloader/mod.rs b/src/downloader/mod.rs index 4c57590..ceb0f81 100644 --- a/src/downloader/mod.rs +++ b/src/downloader/mod.rs @@ -4,6 +4,8 @@ pub mod downloader; pub mod enums; /// File downloader's error pub mod error; +/// Downloader helper +pub mod helper; /// Local file type pub mod local_file; /// The pd file @@ -13,4 +15,5 @@ pub mod tasks; pub use downloader::Downloader; pub use enums::DownloaderResult; pub use error::DownloaderError; +pub use helper::DownloaderHelper; pub use local_file::LocalFile; diff --git a/src/error.rs b/src/error.rs index 460eb3e..adebe9e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,7 @@ pub enum PixivDownloaderError { #[cfg(feature = "server")] Hyper(hyper::Error), HTTP(http::Error), + IOError(std::io::Error), } impl From<&str> for PixivDownloaderError { diff --git a/src/exif.rs b/src/exif.rs index e2d6524..2c68bce 100644 --- a/src/exif.rs +++ b/src/exif.rs @@ -701,6 +701,21 @@ impl ExifImage { unsafe { Some(ExifDataRef::from_raw_handle(d)) } } + /// Read all metadata supported by a specific image format from the image. Before this method is called, the image metadata will be cleared. + /// + /// This method returns success even if no metadata is found in the image. Callers must therefore check the size of individual metadata types before accessing the data. + pub fn read_metadata(&mut self) -> Result<(), ()> { + if self.img.is_null() { + return Err(()); + } + let d = unsafe { _exif::exif_image_read_metadata(self.img) }; + if d == 0 { + Ok(()) + } else { + Err(()) + } + } + /// Assign new Exif data. /// The new Exif data is not written to the image until the [Self::write_metadata()] method is called. /// * `data` - An [ExifData] instance holding Exif data to be copied diff --git a/src/fanbox/article/image.rs b/src/fanbox/article/image.rs index 413b3cc..ae2e1a3 100644 --- a/src/fanbox/article/image.rs +++ b/src/fanbox/article/image.rs @@ -3,6 +3,7 @@ use super::super::error::FanboxAPIError; use crate::fanbox_api::FanboxClientInternal; use json::JsonValue; use proc_macros::check_json_keys; +use proc_macros::create_fanbox_download_helper; use std::fmt::Debug; use std::sync::Arc; @@ -41,11 +42,15 @@ impl FanboxArticleImage { self.data["originalUrl"].as_str() } + create_fanbox_download_helper!(original_url); + #[inline] pub fn thumbnail_url(&self) -> Option<&str> { self.data["thumbnailUrl"].as_str() } + create_fanbox_download_helper!(thumbnail_url); + #[inline] pub fn width(&self) -> Option { self.data["width"].as_u64() diff --git a/src/fanbox_api.rs b/src/fanbox_api.rs index 8e50987..157dac5 100644 --- a/src/fanbox_api.rs +++ b/src/fanbox_api.rs @@ -21,7 +21,7 @@ use std::sync::RwLock; /// Fanbox API client pub struct FanboxClientInternal { /// Web client - client: WebClient, + client: Arc, /// true if in is initialized inited: AtomicBool, /// Fanbox global data @@ -65,7 +65,7 @@ impl FanboxClientInternal { /// Create an new instance pub fn new() -> Self { Self { - client: WebClient::default(), + client: Arc::new(WebClient::default()), inited: AtomicBool::new(false), data: RwLock::new(None), } @@ -273,6 +273,12 @@ impl FanboxClientInternal { } } +impl AsRef> for FanboxClientInternal { + fn as_ref(&self) -> &Arc { + &self.client + } +} + /// Fanbox API Client pub struct FanboxClient { /// Internal client