Compare commits
10 commits
c49b5a058a
...
f1bc906d26
Author | SHA1 | Date | |
---|---|---|---|
f1bc906d26 | |||
14482d2e9b | |||
ce2f638c32 | |||
3a82332bea | |||
6367cc7d61 | |||
7da57eee1b | |||
878a7fee69 | |||
5853827d10 | |||
f14a6107b6 | |||
41c4778195 |
4 changed files with 47 additions and 16 deletions
10
README.md
Normal file
10
README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
usage:
|
||||||
|
|
||||||
|
```console
|
||||||
|
cargo build --release
|
||||||
|
./target/release/lonefire https://www.pixiv.net/artworks/111503285
|
||||||
|
```
|
||||||
|
|
||||||
|
or, install from AUR
|
||||||
|
[https://aur.archlinux.org/packages/lonefire](https://aur.archlinux.org/packages/lonefire)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use config::PixivResponse;
|
use config::{MAX_CONCURRENT_DOWNLOADS, PixivResponse};
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
use helpers::{format_filename, generate_image_urls, normalize_image_url, set_headers};
|
use helpers::{
|
||||||
|
filter_valid_urls, format_filename, generate_image_urls, normalize_image_url, set_headers,
|
||||||
|
};
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
Client,
|
Client,
|
||||||
|
@ -15,10 +17,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{config, helpers};
|
||||||
config::{self, MAX_CONCURRENT_DOWNLOADS},
|
|
||||||
helpers,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn download_pixiv_artwork(artwork_url: &str) -> Result<()> {
|
pub async fn download_pixiv_artwork(artwork_url: &str) -> Result<()> {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
|
@ -59,11 +58,11 @@ pub async fn download_pixiv_artwork(artwork_url: &str) -> Result<()> {
|
||||||
|
|
||||||
let page_count = illust_data.page_count as usize;
|
let page_count = illust_data.page_count as usize;
|
||||||
let image_urls: Vec<String> = match &illust_data.urls.original {
|
let image_urls: Vec<String> = match &illust_data.urls.original {
|
||||||
Some(original_url) if original_url.contains(&artwork_id) => {
|
Some(original_url) if original_url.contains(&artwork_id) => (0..page_count)
|
||||||
generate_image_urls(original_url, page_count)
|
.map(|seq| original_url.replace("p0", &format!("p{}", seq)))
|
||||||
}
|
.collect(),
|
||||||
_ => {
|
_ => {
|
||||||
illust_data
|
let urls = illust_data
|
||||||
.user_illusts
|
.user_illusts
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|user_illusts| {
|
.map(|user_illusts| {
|
||||||
|
@ -75,7 +74,9 @@ pub async fn download_pixiv_artwork(artwork_url: &str) -> Result<()> {
|
||||||
.flat_map(|normalized_url| generate_image_urls(&normalized_url, page_count))
|
.flat_map(|normalized_url| generate_image_urls(&normalized_url, page_count))
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
filter_valid_urls(urls).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if image_urls.is_empty() {
|
if image_urls.is_empty() {
|
||||||
|
@ -84,10 +85,10 @@ pub async fn download_pixiv_artwork(artwork_url: &str) -> Result<()> {
|
||||||
|
|
||||||
let m = MultiProgress::new();
|
let m = MultiProgress::new();
|
||||||
let sty = ProgressStyle::with_template(
|
let sty = ProgressStyle::with_template(
|
||||||
"[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}",
|
"{spinner:.green}[{percent}%][{wide_bar:.cyan/blue}] {bytes}/{total_bytes} {msg}",
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.progress_chars("##-");
|
.progress_chars("#>-");
|
||||||
|
|
||||||
stream::iter(image_urls.into_iter().map(|url| {
|
stream::iter(image_urls.into_iter().map(|url| {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|
@ -113,7 +114,7 @@ async fn download_image(client: Client, url: &str, dir: &Path, pb: &ProgressBar)
|
||||||
let filepath = dir.join(&filename);
|
let filepath = dir.join(&filename);
|
||||||
let temp_path = filepath.with_extension("part");
|
let temp_path = filepath.with_extension("part");
|
||||||
if filepath.exists() {
|
if filepath.exists() {
|
||||||
pb.finish_with_message("Already exists");
|
pb.finish_with_message(format!("{filename} already exists"));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let existing_size = temp_path.metadata().map(|s| s.len()).unwrap_or(0);
|
let existing_size = temp_path.metadata().map(|s| s.len()).unwrap_or(0);
|
||||||
|
@ -156,7 +157,7 @@ async fn download_image(client: Client, url: &str, dir: &Path, pb: &ProgressBar)
|
||||||
pb.inc(chunk.len() as u64);
|
pb.inc(chunk.len() as u64);
|
||||||
}
|
}
|
||||||
|
|
||||||
pb.finish_with_message("Download completed.");
|
pb.finish_with_message(format!("{filename} 🗹"));
|
||||||
rename(temp_path, filepath)?;
|
rename(temp_path, filepath)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
use futures::{StreamExt, stream};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::header::{
|
use reqwest::header::{
|
||||||
ACCEPT, ACCEPT_LANGUAGE, CONNECTION, HeaderMap, HeaderValue, REFERER, USER_AGENT,
|
ACCEPT, ACCEPT_LANGUAGE, CONNECTION, HeaderMap, HeaderValue, REFERER, USER_AGENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::config::MAX_CONCURRENT_DOWNLOADS;
|
||||||
|
|
||||||
pub fn normalize_image_url(base_url: &str) -> String {
|
pub fn normalize_image_url(base_url: &str) -> String {
|
||||||
let re_custom_thumb = Regex::new(r"/c/250x250_80_a2/custom-thumb").unwrap();
|
let re_custom_thumb = Regex::new(r"/c/250x250_80_a2/custom-thumb").unwrap();
|
||||||
let re_img_master = Regex::new(r"/c/250x250_80_a2/img-master").unwrap();
|
let re_img_master = Regex::new(r"/c/250x250_80_a2/img-master").unwrap();
|
||||||
|
@ -29,6 +32,23 @@ pub fn generate_image_urls(base_url: &str, page_count: usize) -> Vec<String> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn probe_image_url(url: String) -> Option<String> {
|
||||||
|
let probe_client = reqwest::Client::new();
|
||||||
|
let headers = set_headers();
|
||||||
|
match probe_client.head(&url).headers(headers).send().await {
|
||||||
|
Ok(response) if response.status().is_success() => Some(url),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn filter_valid_urls(urls: Vec<String>) -> Vec<String> {
|
||||||
|
stream::iter(urls.into_iter().map(probe_image_url))
|
||||||
|
.buffer_unordered(MAX_CONCURRENT_DOWNLOADS)
|
||||||
|
.filter_map(|url| async { url })
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn format_filename(url: &str) -> Option<String> {
|
pub fn format_filename(url: &str) -> Option<String> {
|
||||||
let re = Regex::new(r"/(\d+)_p(\d+)(\.\w+)$").unwrap();
|
let re = Regex::new(r"/(\d+)_p(\d+)(\.\w+)$").unwrap();
|
||||||
if let Some(caps) = re.captures(url) {
|
if let Some(caps) = re.captures(url) {
|
||||||
|
|
|
@ -9,7 +9,7 @@ mod helpers;
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
eprintln!("Usage: pixiv_download <URL>");
|
eprintln!("Usage: lonefire <URL>\nBTW, lonefire can't download GIF image");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
let artwork_url = &args[1];
|
let artwork_url = &args[1];
|
||||||
|
|
Loading…
Add table
Reference in a new issue