Download pixiv artwork now support app api

This commit is contained in:
2023-10-05 12:19:28 +00:00
committed by GitHub
parent 60d2d6dd87
commit 53619d9890
7 changed files with 559 additions and 78 deletions

View File

@@ -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/<id>`
/// * `value` - The JSON object

View File

@@ -275,12 +275,237 @@ pub async fn download_artwork(
Ok(())
}
pub async fn download_artwork_ugoira(
pw: Arc<PixivWebClient>,
id: u64,
base: Arc<PathBuf>,
datas: Arc<PixivData>,
) -> 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 <src> -> <dest>")
.replace("<src>", file_name.to_str().unwrap_or("(null)"))
.replace("<dest>", output_file_name.to_str().unwrap_or("(null)"))
.as_str()
);
}
return Ok(());
}
pub async fn download_artwork_app(
ac: PixivAppClient,
pw: Arc<PixivWebClient>,
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 <src> -> <dest>")
.replace("<src>", file_name.to_str().unwrap_or("(null)"))
.replace("<dest>", output_file_name.to_str().unwrap_or("(null)"))
.as_str()
);
}
return Ok(());
return download_artwork_ugoira(pw, id, base, datas).await;
}
_ => {
println!(

View File

@@ -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<PixivAppIllust, PixivDownloaderError> {
let obj = self.internal.get_illust_details(id).await?;
Ok(PixivAppIllust::new(obj["illust"].clone()))
}
}
impl AsRef<PixivAppClientInternal> for PixivAppClient {

215
src/pixivapp/illust.rs Normal file
View File

@@ -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<u64> {
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<u64> {
self.data["restrict"].as_u64()
}
pub fn user_id(&self) -> Option<u64> {
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<bool> {
self.data["user"]["is_followed"].as_bool()
}
pub fn tags(&self) -> Vec<Tag> {
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<u64> {
self.data["page_count"].as_u64()
}
pub fn width(&self) -> Option<u64> {
self.data["width"].as_u64()
}
pub fn height(&self) -> Option<u64> {
self.data["height"].as_u64()
}
pub fn sanity_level(&self) -> Option<u64> {
self.data["sanity_level"].as_u64()
}
pub fn x_restrict(&self) -> Option<u64> {
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<ImageUrls> {
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<u64> {
self.data["total_view"].as_u64()
}
pub fn total_bookmarks(&self) -> Option<u64> {
self.data["total_bookmarks"].as_u64()
}
pub fn is_bookmarked(&self) -> Option<bool> {
self.data["is_bookmarked"].as_bool()
}
pub fn visible(&self) -> Option<bool> {
self.data["visible"].as_bool()
}
pub fn is_muted(&self) -> Option<bool> {
self.data["is_muted"].as_bool()
}
pub fn total_comments(&self) -> Option<u64> {
self.data["total_comments"].as_u64()
}
pub fn illust_ai_type(&self) -> Option<u64> {
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()
}
}

View File

@@ -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()
}
}

View File

@@ -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;

37
src/pixivapp/tag.rs Normal file
View File

@@ -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()
}
}