Compare commits

..

10 commits

4 changed files with 47 additions and 16 deletions

10
README.md Normal file
View 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)

View file

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

View file

@ -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) {

View file

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