diff --git a/src/data/data.rs b/src/data/data.rs index 8316554..8ce8bf1 100644 --- a/src/data/data.rs +++ b/src/data/data.rs @@ -5,6 +5,7 @@ use crate::opt::author_name_filter::AuthorFiler; use crate::opthelper::get_helper; use crate::pixiv_link::PixivID; use crate::pixiv_link::ToPixivID; +use crate::pixivapp::illust::PixivAppIllust; use int_enum::IntEnum; use json::JsonValue; use xml::unescape; @@ -56,6 +57,27 @@ impl PixivData { }) } + /// Read data from [PixivAppIllust]. + pub fn from_app_illust(&mut self, illust: &PixivAppIllust) { + self.title = illust.title().map(|s| s.to_owned()); + self.author = illust.user_name().map(|s| s.to_owned()); + self.description = illust.caption().map(|s| s.to_owned()); + let mut tags = Vec::new(); + for i in illust.tags() { + if let Some(name) = i.name() { + tags.push((name.to_owned(), i.translated_name().map(|s| s.to_owned()))); + } + } + self.tags.replace(tags); + self.ai_type = match illust.illust_ai_type() { + Some(t) => match PixivAiType::from_int(t as u8) { + Ok(t) => Some(t), + Err(_) => None, + }, + None => None, + }; + } + /// Read data from JSON object. /// The object is from `https://www.pixiv.net/artworks/` /// * `value` - The JSON object diff --git a/src/download.rs b/src/download.rs index 64d38d5..ab36d89 100644 --- a/src/download.rs +++ b/src/download.rs @@ -275,12 +275,237 @@ pub async fn download_artwork( Ok(()) } +pub async fn download_artwork_ugoira( + pw: Arc, + id: u64, + base: Arc, + datas: Arc, +) -> Result<(), PixivDownloaderError> { + let helper = get_helper(); + let ugoira_data = pw + .get_ugoira(id) + .await + .try_err(gettext("Failed to get ugoira's data."))?; + let src = (&ugoira_data["originalSrc"]) + .as_str() + .try_err(gettext("Can not find source link for ugoira."))?; + let dh = DownloaderHelper::builder(src)? + .headers(json::object! { "referer": "https://www.pixiv.net/" }) + .build(); + let tasks = TaskManager::default(); + tasks + .add_task(download_file( + dh, + if helper.enable_multi_progress_bar() { + Some(get_progress_bar()) + } else { + None + }, + Arc::clone(&base), + )) + .await; + tasks.join().await; + let mut tasks = tasks.take_finished_tasks(); + let task = tasks.get_mut(0).try_err(gettext("No finished task."))?; + task.await??; + #[cfg(feature = "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 = match get_video_metadata(Arc::clone(&datas).as_ref()) { + Ok(m) => m, + Err(e) => { + println!( + "{} {}", + gettext("Warning: Failed to generate video's metadata:"), + e + ); + AVDict::new() + } + }; + let mut options = AVDict::new(); + if helper.force_yuv420p() { + options.set("force_yuv420p", "1", None)?; + } + let profile = helper.x264_profile(); + if !profile.is_auto() { + options.set("profile", profile.as_str(), None)?; + } + match helper.x264_crf() { + Some(crf) => { + options.set("crf", format!("{}", crf), None)?; + } + None => {} + } + let frames = UgoiraFrames::from_json(&ugoira_data["frames"])?; + let output_file_name = base.join(format!("{}.mp4", id)); + convert_ugoira_to_mp4( + &file_name, + &output_file_name, + &frames, + helper.ugoira_max_fps(), + &options, + &metadata, + )?; + println!( + "{}", + gettext("Converted -> ") + .replace("", file_name.to_str().unwrap_or("(null)")) + .replace("", output_file_name.to_str().unwrap_or("(null)")) + .as_str() + ); + } + return Ok(()); +} + pub async fn download_artwork_app( ac: PixivAppClient, pw: Arc, id: u64, ) -> Result<(), PixivDownloaderError> { let data = ac.get_illust_details(id).await?; + let helper = get_helper(); + if helper.verbose() { + println!("{:#?}", data); + } + match crate::pixivapp::check::CheckUnknown::check_unknown(&data) { + Ok(_) => {} + Err(e) => { + println!( + "{} {}", + gettext("Warning: Post info contains unknown data:"), + e + ); + } + } + let base = Arc::new(PathBuf::from(helper.download_base())); + let json_file = base.join(format!("{}.json", id)); + let mut datas = PixivData::new(id).unwrap(); + datas.from_app_illust(&data); + let datas = Arc::new(datas); + let json_data = JSONDataFile::from(Arc::clone(&datas)); + if !json_data.save(&json_file) { + return Err(PixivDownloaderError::from(gettext( + "Failed to save metadata to JSON file.", + ))); + } + let illust_type = data.typ(); + match illust_type { + Some(illust_type) => match illust_type { + "ugoira" => { + return download_artwork_ugoira(pw, id, base, datas).await; + } + _ => {} + }, + None => { + println!("{}", gettext("Warning: Failed to get illust's type.")); + } + } + let page_count = data + .page_count() + .ok_or(gettext("Failed to get page count."))?; + if page_count > 1 && helper.download_multiple_files() { + let mut np = 0u16; + let tasks = TaskManager::default(); + let mut re: Result<(), PixivDownloaderError> = Ok(()); + for page in data.meta_pages() { + let url = match page.original() { + Some(url) => url.to_owned(), + None => { + concat_pixiv_downloader_error!( + re, + Err::<(), &str>(gettext("Failed to get original picture's link.")) + ); + continue; + } + }; + tasks + .add_task(download_artwork_link( + url, + np, + if helper.enable_multi_progress_bar() { + Some(get_progress_bar()) + } else { + None + }, + Arc::clone(&datas), + Arc::clone(&base), + )) + .await; + np += 1; + } + tasks.join().await; + let tasks = tasks.take_finished_tasks(); + for task in tasks { + let r = task.await; + let r = match r { + Ok(r) => r, + Err(e) => Err(PixivDownloaderError::from(e)), + }; + concat_pixiv_downloader_error!(re, r); + } + return re; + } else if page_count > 1 { + let mut np = 0u16; + let tasks = TaskManager::default(); + for page in data.meta_pages() { + let link = page + .original() + .ok_or(gettext("Failed to get original picture's link."))?; + tasks + .add_task(download_artwork_link( + link.to_owned(), + np, + if helper.enable_multi_progress_bar() { + Some(get_progress_bar()) + } else { + None + }, + Arc::clone(&datas), + Arc::clone(&base), + )) + .await; + tasks.join().await; + np += 1; + } + let mut re = Ok(()); + let tasks = tasks.take_finished_tasks(); + for task in tasks { + let r = task.await; + let r = match r { + Ok(r) => r, + Err(e) => Err(PixivDownloaderError::from(e)), + }; + concat_pixiv_downloader_error!(re, r); + } + return re; + } else { + let link = data + .original_image_url() + .ok_or(gettext("Failed to get original picture's link."))?; + let tasks = TaskManager::default(); + tasks + .add_task(download_artwork_link( + link.to_owned(), + 0, + if helper.enable_multi_progress_bar() { + Some(get_progress_bar()) + } else { + None + }, + Arc::clone(&datas), + Arc::clone(&base), + )) + .await; + tasks.join().await; + let mut tasks = tasks.take_finished_tasks(); + let task = tasks.get_mut(0).try_err(gettext("No tasks finished."))?; + task.await??; + } Ok(()) } @@ -355,84 +580,7 @@ pub async fn download_artwork_web( 0 => {} // Normal illust 1 => {} // Manga illust 2 => { - let ugoira_data = pw - .get_ugoira(id) - .await - .try_err(gettext("Failed to get ugoira's data."))?; - let src = (&ugoira_data["originalSrc"]) - .as_str() - .try_err(gettext("Can not find source link for ugoira."))?; - let dh = DownloaderHelper::builder(src)? - .headers(json::object! { "referer": "https://www.pixiv.net/" }) - .build(); - let tasks = TaskManager::default(); - tasks - .add_task(download_file( - dh, - if helper.enable_multi_progress_bar() { - Some(get_progress_bar()) - } else { - None - }, - Arc::clone(&base), - )) - .await; - tasks.join().await; - let mut tasks = tasks.take_finished_tasks(); - let task = tasks.get_mut(0).try_err(gettext("No finished task."))?; - task.await??; - #[cfg(feature = "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 = match get_video_metadata(Arc::clone(&datas).as_ref()) { - Ok(m) => m, - Err(e) => { - println!( - "{} {}", - gettext("Warning: Failed to generate video's metadata:"), - e - ); - AVDict::new() - } - }; - let mut options = AVDict::new(); - if helper.force_yuv420p() { - options.set("force_yuv420p", "1", None)?; - } - let profile = helper.x264_profile(); - if !profile.is_auto() { - options.set("profile", profile.as_str(), None)?; - } - match helper.x264_crf() { - Some(crf) => { - options.set("crf", format!("{}", crf), None)?; - } - None => {} - } - let frames = UgoiraFrames::from_json(&ugoira_data["frames"])?; - let output_file_name = base.join(format!("{}.mp4", id)); - convert_ugoira_to_mp4( - &file_name, - &output_file_name, - &frames, - helper.ugoira_max_fps(), - &options, - &metadata, - )?; - println!( - "{}", - gettext("Converted -> ") - .replace("", file_name.to_str().unwrap_or("(null)")) - .replace("", output_file_name.to_str().unwrap_or("(null)")) - .as_str() - ); - } - return Ok(()); + return download_artwork_ugoira(pw, id, base, datas).await; } _ => { println!( diff --git a/src/pixiv_app.rs b/src/pixiv_app.rs index 2a4cc99..0855402 100644 --- a/src/pixiv_app.rs +++ b/src/pixiv_app.rs @@ -6,6 +6,7 @@ use crate::ext::replace::ReplaceWith2; use crate::ext::rw_lock::GetRwLock; use crate::opthelper::OptHelper; use crate::pixivapp::error::handle_error; +use crate::pixivapp::illust::PixivAppIllust; use crate::webclient::{ReqMiddleware, WebClient}; use crate::{get_helper, gettext}; use chrono::{DateTime, Local, SecondsFormat, Utc}; @@ -286,6 +287,14 @@ impl PixivAppClient { .add_req_middleware(Box::new(PixivAppMiddleware::new(r.internal.clone()))); r } + + pub async fn get_illust_details( + &self, + id: u64, + ) -> Result { + let obj = self.internal.get_illust_details(id).await?; + Ok(PixivAppIllust::new(obj["illust"].clone())) + } } impl AsRef for PixivAppClient { diff --git a/src/pixivapp/illust.rs b/src/pixivapp/illust.rs new file mode 100644 index 0000000..810bb4f --- /dev/null +++ b/src/pixivapp/illust.rs @@ -0,0 +1,215 @@ +use super::check::CheckUnknown; +use super::image_urls::ImageUrls; +use super::tag::Tag; +use json::JsonValue; +use proc_macros::check_json_keys; + +pub struct PixivAppIllust { + data: JsonValue, +} + +impl PixivAppIllust { + pub fn new(data: JsonValue) -> Self { + Self { data } + } + + pub fn id(&self) -> Option { + self.data["id"].as_u64() + } + + pub fn title(&self) -> Option<&str> { + self.data["title"].as_str() + } + + pub fn typ(&self) -> Option<&str> { + self.data["type"].as_str() + } + + pub fn image_urls(&self) -> ImageUrls { + return ImageUrls::new(self.data["image_urls"].clone()); + } + + pub fn caption(&self) -> Option<&str> { + self.data["caption"].as_str() + } + + pub fn restrict(&self) -> Option { + self.data["restrict"].as_u64() + } + + pub fn user_id(&self) -> Option { + self.data["user"]["id"].as_u64() + } + + pub fn user_name(&self) -> Option<&str> { + self.data["user"]["name"].as_str() + } + + pub fn user_account(&self) -> Option<&str> { + self.data["user"]["account"].as_str() + } + + pub fn user_profile_image_urls_medium(&self) -> Option<&str> { + self.data["user"]["profile_image_urls"]["medium"].as_str() + } + + pub fn user_is_followed(&self) -> Option { + self.data["user"]["is_followed"].as_bool() + } + + pub fn tags(&self) -> Vec { + let mut tags = Vec::new(); + for tag in self.data["tags"].members() { + tags.push(Tag::new(tag.clone())); + } + tags + } + + pub fn create_date(&self) -> Option<&str> { + self.data["create_date"].as_str() + } + + pub fn page_count(&self) -> Option { + self.data["page_count"].as_u64() + } + + pub fn width(&self) -> Option { + self.data["width"].as_u64() + } + + pub fn height(&self) -> Option { + self.data["height"].as_u64() + } + + pub fn sanity_level(&self) -> Option { + self.data["sanity_level"].as_u64() + } + + pub fn x_restrict(&self) -> Option { + self.data["x_restrict"].as_u64() + } + + pub fn original_image_url(&self) -> Option<&str> { + self.data["meta_single_page"]["original_image_url"].as_str() + } + + pub fn meta_pages(&self) -> Vec { + let mut meta_pages = Vec::new(); + for meta_page in self.data["meta_pages"].members() { + meta_pages.push(ImageUrls::new(meta_page["image_urls"].clone())); + } + meta_pages + } + + pub fn total_view(&self) -> Option { + self.data["total_view"].as_u64() + } + + pub fn total_bookmarks(&self) -> Option { + self.data["total_bookmarks"].as_u64() + } + + pub fn is_bookmarked(&self) -> Option { + self.data["is_bookmarked"].as_bool() + } + + pub fn visible(&self) -> Option { + self.data["visible"].as_bool() + } + + pub fn is_muted(&self) -> Option { + self.data["is_muted"].as_bool() + } + + pub fn total_comments(&self) -> Option { + self.data["total_comments"].as_u64() + } + + pub fn illust_ai_type(&self) -> Option { + self.data["illust_ai_type"].as_u64() + } +} + +impl CheckUnknown for PixivAppIllust { + fn check_unknown(&self) -> Result<(), String> { + check_json_keys!( + "id"+, + "title"+, + "type"+typ, + "image_urls": ["square_medium", "medium", "large"], + "caption"+, + "restrict"+, + "user": [ + "id"+, + "name"+, + "account"+, + "profile_image_urls": ["medium"], + "is_followed"+ + ], + "tags", + "tools", + "create_date"+, + "page_count"+, + "width"+, + "height"+, + "sanity_level"+, + "x_restrict"+, + "series", + "meta_single_page": ["original_image_url"], + "meta_pages", + "total_view"+, + "total_bookmarks"+, + "is_bookmarked"+, + "visible"+, + "is_muted"+, + "total_comments"+, + "illust_ai_type"+, + "illust_book_style", + "comment_access_control" + ); + for i in self.tags() { + i.check_unknown()?; + } + for i in self.meta_pages() { + i.check_unknown()?; + } + Ok(()) + } +} + +impl std::fmt::Debug for PixivAppIllust { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PixivAppIllust") + .field("id", &self.id()) + .field("title", &self.title()) + .field("type", &self.typ()) + .field("image_urls", &self.image_urls()) + .field("caption", &self.caption()) + .field("restrict", &self.restrict()) + .field("user_id", &self.user_id()) + .field("user_name", &self.user_name()) + .field("user_account", &self.user_account()) + .field( + "user_profile_image_urls_medium", + &self.user_profile_image_urls_medium(), + ) + .field("user_is_followed", &self.user_is_followed()) + .field("tags", &self.tags()) + .field("create_date", &self.create_date()) + .field("page_count", &self.page_count()) + .field("width", &self.width()) + .field("height", &self.height()) + .field("sanity_level", &self.sanity_level()) + .field("x_restrict", &self.x_restrict()) + .field("original_image_url", &self.original_image_url()) + .field("meta_pages", &self.meta_pages()) + .field("total_view", &self.total_view()) + .field("total_bookmarks", &self.total_bookmarks()) + .field("is_bookmarked", &self.is_bookmarked()) + .field("visible", &self.visible()) + .field("is_muted", &self.is_muted()) + .field("total_comments", &self.total_comments()) + .field("illust_ai_type", &self.illust_ai_type()) + .finish_non_exhaustive() + } +} diff --git a/src/pixivapp/image_urls.rs b/src/pixivapp/image_urls.rs new file mode 100644 index 0000000..d5b1263 --- /dev/null +++ b/src/pixivapp/image_urls.rs @@ -0,0 +1,47 @@ +use super::check::CheckUnknown; +use json::JsonValue; +use proc_macros::check_json_keys; + +pub struct ImageUrls { + data: JsonValue, +} + +impl ImageUrls { + pub fn new(data: JsonValue) -> Self { + Self { data } + } + + pub fn square_medium(&self) -> Option<&str> { + self.data["square_medium"].as_str() + } + + pub fn medium(&self) -> Option<&str> { + self.data["medium"].as_str() + } + + pub fn large(&self) -> Option<&str> { + self.data["large"].as_str() + } + + pub fn original(&self) -> Option<&str> { + self.data["original"].as_str() + } +} + +impl CheckUnknown for ImageUrls { + fn check_unknown(&self) -> Result<(), String> { + check_json_keys!("square_medium"+, "medium"+, "large"+, "original"+); + Ok(()) + } +} + +impl std::fmt::Debug for ImageUrls { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ImageUrls") + .field("square_medium", &self.square_medium()) + .field("medium", &self.medium()) + .field("large", &self.large()) + .field("original", &self.original()) + .finish() + } +} diff --git a/src/pixivapp/mod.rs b/src/pixivapp/mod.rs index 3d3c4ff..7ed4e30 100644 --- a/src/pixivapp/mod.rs +++ b/src/pixivapp/mod.rs @@ -1,3 +1,6 @@ pub mod check; /// Error handling for pixiv app api pub mod error; +pub mod illust; +pub mod image_urls; +pub mod tag; diff --git a/src/pixivapp/tag.rs b/src/pixivapp/tag.rs new file mode 100644 index 0000000..43c052d --- /dev/null +++ b/src/pixivapp/tag.rs @@ -0,0 +1,37 @@ +use super::check::CheckUnknown; +use json::JsonValue; +use proc_macros::check_json_keys; + +pub struct Tag { + data: JsonValue, +} + +impl Tag { + pub fn new(data: JsonValue) -> Self { + Self { data } + } + + pub fn name(&self) -> Option<&str> { + self.data["name"].as_str() + } + + pub fn translated_name(&self) -> Option<&str> { + self.data["translated_name"].as_str() + } +} + +impl CheckUnknown for Tag { + fn check_unknown(&self) -> Result<(), String> { + check_json_keys!("name"+, "translated_name"); + Ok(()) + } +} + +impl std::fmt::Debug for Tag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Tag") + .field("name", &self.name()) + .field("translated_name", &self.translated_name()) + .finish() + } +}