update get_response

This commit is contained in:
lychang
2026-03-17 02:15:58 +08:00
parent 6af97ee8ee
commit 161837bd24
3 changed files with 412 additions and 161 deletions

44
failed_index Normal file
View File

@@ -0,0 +1,44 @@
6813
6814
6815
6816
6817
6818
6819
6820
6821
6822
6823
6824
6825
6826
6827
6828
6829
6830
6831
6832
6833
6834
6835
6836
6837
6838
6839
6840
6841
6842
6843
6844
6845
6846
6847
6848
6849
6850
6851
6852
6853
6854
6855
6856

View File

@@ -1,87 +1,168 @@
use std::str::FromStr;
use std::thread;
use bytes::Bytes;
use std::time::Duration;
mod file;
mod web_parser;
use web_parser::DownloadResult;
fn retry_download(idx: i32, name: &str, times: i32, duration: u64) -> bool {
if web_parser::download_images(idx, name) {
println!("相册 {} ({}) 下载成功", idx, name);
file::write_file("index", Bytes::from(idx.to_string()))
.unwrap_or_else(|_| panic!("Write index error: {}", idx));
true
} else if times > 0 {
println!("相册 {} ({}) 下载失败,{}秒后重试", idx, name, duration);
thread::sleep(Duration::from_millis(duration * 1000));
retry_download(idx, name, times - 1, duration)
} else {
println!("相册 {} ({}) 下载失败{}次,记录到失败列表", idx, name, times);
false
/// 重试下载相册
///
/// # 参数
/// - `idx`: 相册索引
/// - `name`: 相册名称
/// - `times`: 重试次数
/// - `duration`: 重试间隔(秒)
///
/// # 返回值
/// - `DownloadResult`: 下载结果
fn retry_download(idx: i32, name: &str, times: i32, duration: u64) -> DownloadResult {
match web_parser::download_albums(idx, name) {
DownloadResult::Success => {
println!("✅ 相册 {} ({}) 下载成功", idx, name);
DownloadResult::Success
}
DownloadResult::NetworkError if times > 0 => {
println!("🔄 相册 {} ({}) 网络错误,{}秒后重试", idx, name, duration);
thread::sleep(Duration::from_millis(duration * 1000));
retry_download(idx, name, times - 1, duration)
}
DownloadResult::Failed => {
println!("❌ 相册 {} ({}) 下载失败", idx, name);
DownloadResult::Failed
}
DownloadResult::NetworkError => {
println!("⚠️ 相册 {} ({}) 网络错误,记录到失败列表", idx, name);
DownloadResult::NetworkError
}
}
}
/// 处理失败的下载记录
///
/// 从失败列表中读取记录,尝试重新下载
fn process_failed_downloads() {
let failed_indexes = file::read_failed_indexes();
if failed_indexes.is_empty() {
return;
}
println!("🔄 发现 {} 个失败的下载记录,开始重试...", failed_indexes.len());
for failed_idx in failed_indexes {
match web_parser::get_title(failed_idx) {
Ok(name) => {
println!("🔄 重试下载索引 {} 相册 {}...", failed_idx, name);
match retry_download(failed_idx, &name, 1, 5) {
DownloadResult::Success => {
println!("✅ 相册 {} 重试成功", failed_idx);
file::remove_failed_index(failed_idx);
}
_ => println!("❌ 相册 {} 重试失败,保留在失败列表", failed_idx),
}
}
Err(DownloadResult::Failed) => {
println!("🗑️ 相册 {} 不存在,从失败列表中移除", failed_idx);
file::remove_failed_index(failed_idx);
}
Err(DownloadResult::NetworkError) => {
println!("⚠️ 相册 {} 获取标题时网络错误,保留在失败列表", failed_idx);
}
Err(DownloadResult::Success) => unreachable!("DownloadResult::Success should not appear in Err branch"),
}
thread::sleep(Duration::from_millis(100));
}
println!("✅ 失败记录处理完成");
}
/// 更新索引文件
///
/// # 参数
/// - `idx`: 当前索引值
fn update_index(idx: i32) {
file::write_file("index", Bytes::from(idx.to_string()))
.unwrap_or_else(|_| panic!("❌ 写入索引文件错误: {}", idx));
}
/// 处理单个相册下载
///
/// # 参数
/// - `idx`: 相册索引
/// - `blank_count`: 空白计数引用
///
/// # 返回值
/// - `bool`: 是否需要继续处理
fn process_album(idx: i32, blank_count: &mut i32) -> bool {
match web_parser::get_title(idx) {
Ok(name) => {
println!("🚀 开始下载索引 {} 相册 {}...", idx, name);
match retry_download(idx, &name, 3, 15) {
DownloadResult::Success => {
// 下载成功:索引前进,重置空白计数
*blank_count = 0;
true
}
DownloadResult::Failed => {
// 下载失败:索引前进,增加空白计数
*blank_count += 1;
true
}
DownloadResult::NetworkError => {
// 网络错误:记录失败索引,索引前进,重置空白计数
file::write_failed_index(idx);
*blank_count = 0;
true
}
}
}
Err(DownloadResult::Failed) => {
// 页面不存在:索引前进,增加空白计数
println!("❌ 索引 {} 不存在内容", idx);
thread::sleep(Duration::from_millis(3000));
*blank_count += 1;
true
}
Err(DownloadResult::NetworkError) => {
// 获取标题时网络错误:索引前进,增加空白计数
println!("⚠️ 索引 {} 获取标题时网络错误,跳过", idx);
thread::sleep(Duration::from_millis(3000));
*blank_count += 1;
true
}
Err(DownloadResult::Success) => unreachable!("DownloadResult::Success should not appear in Err branch"),
}
}
fn main() {
let mut idx: i32;
// 先处理失败的下载记录
process_failed_downloads();
// 初始化索引和空白计数
let mut idx = file::read_file_string("index")
.ok()
.and_then(|s| i32::from_str(&s).ok())
.unwrap_or(1);
let mut blank_count = 0;
// 先处理失败的记录
let mut failed_indexes = file::read_failed_indexes();
if !failed_indexes.is_empty() {
println!(
"发现 {} 个失败的下载记录,开始重试...",
failed_indexes.len()
);
for failed_idx in failed_indexes.drain(..).collect::<Vec<_>>() {
let name = web_parser::get_title(failed_idx);
if name != "" {
println!("重试下载索引 {} 相册 {}...", failed_idx, name);
let success = retry_download(failed_idx, &name, 2, 10);
if success {
println!("相册 {} 重试成功", failed_idx);
} else {
println!("相册 {} 重试失败,保留在失败列表", failed_idx);
}
} else {
println!("相册 {} 不存在,从失败列表中移除", failed_idx);
file::remove_failed_index(failed_idx);
}
thread::sleep(Duration::from_millis(100));
}
println!("失败记录处理完成");
}
// 继续正常下载流程
let index = file::read_file_string("index").expect("Read index error");
if index.is_empty() {
idx = 1;
} else {
idx = i32::from_str(&index).expect("Index Corrupted");
}
println!("🚀 下载程序启动,从索引 {} 开始...", idx);
// 主下载循环
loop {
let name = web_parser::get_title(idx);
if name == "" {
println!("索引 {} 不存在内容", idx);
thread::sleep(Duration::from_millis(3000));
let should_continue = process_album(idx, &mut blank_count);
if should_continue {
idx += 1;
blank_count += 1;
} else {
println!("开始下载索引 {} 相册 {}...", idx, name);
let success = retry_download(idx, &name, 3, 15);
if success {
idx += 1;
} else {
file::write_failed_index(idx);
idx += 1;
}
update_index(idx);
}
// 检查是否达到空白计数上限
if blank_count >= 50 {
break;
println!("🔄 连续空白计数达到 {},重新处理失败记录", blank_count);
process_failed_downloads();
blank_count = 0;
}
thread::sleep(Duration::from_millis(100));
}
}
}

View File

@@ -7,43 +7,96 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;
/// 自定义错误类型,用于区分不同类型的网络错误
#[derive(Debug)]
pub enum RequestError {
NotFound, // 404 错误
NetworkError, // 网络错误
OtherError(String), // 其他错误
}
impl From<reqwest::Error> for RequestError {
fn from(err: reqwest::Error) -> Self {
if err.is_status() {
if let Some(status) = err.status() {
if status == reqwest::StatusCode::NOT_FOUND {
return RequestError::NotFound;
}
}
RequestError::OtherError(err.to_string())
} else if err.is_timeout() || err.is_connect() {
RequestError::NetworkError
} else {
RequestError::OtherError(err.to_string())
}
}
}
/// 相册数据结构
#[derive(Debug, Serialize, Deserialize)]
struct Album {
title: String,
images: Vec<String>,
}
fn get_response(url: &str) -> Result<Bytes, reqwest::Error> {
/// 下载结果枚举
pub(crate) enum DownloadResult {
Success,
NetworkError,
Failed,
}
/// 发送 HTTP 请求并获取响应
///
/// # 参数
/// - `url`: 请求的 URL
///
/// # 返回值
/// - `Result<Bytes, RequestError>`: 响应内容或错误
fn get_response(url: &str) -> Result<Bytes, RequestError> {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(30))
.build()?;
.build()
.map_err(|_| RequestError::NetworkError)?;
let response = client
.get(url)
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.send()?;
.send()
.map_err(RequestError::from)?;
if !response.status().is_success() {
return Err(response.error_for_status().unwrap_err().into());
// 检查 HTTP 状态码
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Err(RequestError::NotFound);
}
let content_encoding = response
.headers()
if !response.status().is_success() {
return Err(RequestError::OtherError(format!(
"HTTP {}: {}",
response.status(),
response.status().canonical_reason().unwrap_or("Unknown")
)));
}
// 先检查内容编码,再获取 bytes
let content_encoding = response.headers()
.get("content-encoding")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let bytes = response.bytes()?;
let bytes = response.bytes().map_err(RequestError::from)?;
// 处理 gzip 压缩
if content_encoding.contains("gzip") {
use std::io::Read;
let mut decoder = flate2::read::GzDecoder::new(&bytes[..]);
let mut decompressed = Vec::new();
match decoder.read_to_end(&mut decompressed) {
Ok(_) => Ok(Bytes::from(decompressed)),
Err(e) => {
println!("Gzip decompression failed: {}, using raw data", e);
println!("⚠️ Gzip 解压失败: {}, 使用原始数据", e);
Ok(bytes)
}
}
@@ -52,64 +105,131 @@ fn get_response(url: &str) -> Result<Bytes, reqwest::Error> {
}
}
pub(crate) fn get_title(uid: i32) -> String {
/// 获取相册标题
///
/// # 参数
/// - `uid`: 相册 ID
///
/// # 返回值
/// - `Result<String, DownloadResult>`: 相册标题或错误
pub(crate) fn get_title(uid: i32) -> Result<String, DownloadResult> {
let url = format!("https://www.meitu131.com/meinv/{}/", uid);
match get_response(&url) {
Ok(value) if !value.is_empty() => {
let body = String::from_utf8_lossy(&value).to_string();
get_html_title(body)
Ok(get_html_title(body))
}
_ => "".to_string(),
Ok(_) => Err(DownloadResult::NetworkError),
Err(RequestError::NotFound) => Err(DownloadResult::Failed),
Err(RequestError::NetworkError) => Err(DownloadResult::NetworkError),
Err(RequestError::OtherError(_)) => Err(DownloadResult::NetworkError),
}
}
/// 从 HTML 内容中提取标题
fn get_html_title(body: String) -> String {
let document = Html::parse_document(&body);
let title_selector = Selector::parse("h1").unwrap();
let title_element = document.select(&title_selector).next().unwrap();
title_element.text().next().unwrap().to_string()
document
.select(&title_selector)
.next()
.and_then(|element| element.text().next())
.unwrap_or("未知标题")
.to_string()
}
fn download_image(url: &str, file_path: &str, retry_times: i8) -> i8 {
/// 下载单张图片
///
/// # 参数
/// - `url`: 图片 URL
/// - `file_path`: 保存路径
/// - `retry_times`: 重试次数
///
/// # 返回值
/// - `DownloadResult`: 下载结果
fn download_image(url: &str, file_path: &str, retry_times: i8) -> DownloadResult {
match get_response(url) {
Ok(value) if !value.is_empty() => {
file::write_file(file_path, value).expect("Write file error");
1
file::write_file(file_path, value).expect("❌ 写入文件错误");
DownloadResult::Success
}
Ok(_) => 0, // 空内容
Err(_) if retry_times > 1 => {
Ok(_) => DownloadResult::Failed, // 空内容
Err(RequestError::NotFound) => DownloadResult::Failed, // 404 错误
Err(RequestError::NetworkError) if retry_times > 1 => {
thread::sleep(Duration::from_millis(1000));
download_image(url, file_path, retry_times - 1)
}
Err(_) => 2, // 网络错误
Err(RequestError::OtherError(_)) if retry_times > 1 => {
thread::sleep(Duration::from_millis(1000));
download_image(url, file_path, retry_times - 1)
}
Err(RequestError::NetworkError) => DownloadResult::NetworkError,
Err(RequestError::OtherError(_)) => DownloadResult::NetworkError,
}
}
pub(crate) fn download_images(uid: i32, title: &str) -> bool {
/// 下载相册的所有图片
///
/// # 参数
/// - `uid`: 相册 ID
/// - `title`: 相册标题
///
/// # 返回值
/// - `DownloadResult`: 下载结果
pub(crate) fn download_albums(uid: i32, title: &str) -> DownloadResult {
let base_url = format!("https://www.meitu131.com/meinv/{}/", uid);
let file_content = get_response(&base_url);
let body = match file_content {
Ok(value) => String::from_utf8(value.to_vec()).expect("Transforms error"),
Err(_e) => "".parse().unwrap(),
// 获取相册主页内容
let body = match get_response(&base_url) {
Ok(value) => String::from_utf8(value.to_vec()).expect("❌ 转换错误"),
Err(RequestError::NotFound) => {
println!("❌ 相册 {} 不存在 (404)", uid);
return DownloadResult::Failed;
}
Err(RequestError::NetworkError) => {
println!("⚠️ 相册 {} 下载时网络波动 (500)", uid);
return DownloadResult::NetworkError;
}
Err(RequestError::OtherError(_)) => {
println!("⚠️ 相册 {} 下载时网络波动 (500)", uid);
return DownloadResult::NetworkError;
}
};
let document = Html::parse_document(&body);
let img_selector = Selector::parse("p[align='center'] img").unwrap();
let mut images = Vec::new();
// 获取总页数
let total_pages = get_total_pages(&document);
// 多线程收集所有图片 URL
let images = collect_image_urls(uid, total_pages, &img_selector, &base_url);
if images.len() < total_pages as usize {
println!("⚠️ 相册 {} 下载时网络波动, 仅下载 {} 页", uid, images.len());
return DownloadResult::NetworkError;
}
// 获取尾页数
let mut total_pages = 1;
// 生成 JSON 文件
generate_json_file(uid, title, &images);
// 多线程下载图片
download_images_multithreaded(uid, &images)
}
/// 获取相册总页数
fn get_total_pages(document: &Html) -> i32 {
let pages_selector = Selector::parse("#pages a").unwrap();
for page_element in document.select(&pages_selector) {
if let Some(text) = page_element.text().next() {
if text == "尾页" {
if let Some(href) = page_element.value().attr("href") {
// 从尾页链接中提取页数
if let Some(page_num_str) = href.split("_").last() {
if let Some(page_num) = page_num_str.split(".").next() {
if let Ok(num) = page_num.parse::<i32>() {
total_pages = num;
return num;
}
}
}
@@ -117,23 +237,25 @@ pub(crate) fn download_images(uid: i32, title: &str) -> bool {
}
}
}
1 // 默认只有一页
}
// 多线程收集所有图片 URL
/// 多线程收集图片 URL
fn collect_image_urls(uid: i32, total_pages: i32, img_selector: &Selector, base_url: &str) -> Vec<String> {
let (tx, rx) = mpsc::channel();
let img_selector_arc = Arc::new(img_selector);
let mut page_handles = vec![];
let img_selector_arc = Arc::new(img_selector.clone());
// 创建页面解析线程池
let max_page_threads = 8; // 限制并发页面解析数
let max_page_threads = 8;
for chunk_start in (1..=total_pages).step_by(max_page_threads as usize) {
let chunk_end = std::cmp::min(chunk_start + max_page_threads as i32 - 1, total_pages);
let mut page_handles = vec![];
for page in chunk_start..=chunk_end {
let tx = tx.clone();
let img_selector = Arc::clone(&img_selector_arc);
let base_url = base_url.clone();
let uid = uid;
let base_url = base_url.to_string();
let handle = thread::spawn(move || {
let page_url = if page == 1 {
@@ -145,22 +267,26 @@ pub(crate) fn download_images(uid: i32, title: &str) -> bool {
let mut page_images = Vec::new();
match get_response(&page_url) {
Ok(value) => {
if value.is_empty() {
println!("Page {} not found, skipping", page);
} else {
let page_body =
String::from_utf8(value.to_vec()).expect("Transforms error");
let page_document = Html::parse_document(&page_body);
for img_element in page_document.select(&img_selector) {
if let Some(src) = img_element.value().attr("src") {
page_images.push(src.to_string());
}
Ok(value) if !value.is_empty() => {
let page_body = String::from_utf8(value.to_vec()).expect("❌ 转换错误");
let page_document = Html::parse_document(&page_body);
for img_element in page_document.select(&img_selector) {
if let Some(src) = img_element.value().attr("src") {
page_images.push(src.to_string());
}
}
}
Err(e) => {
println!("Error fetching page {}: {}, skipping", page, e);
Ok(_) => {
println!("⚠️ 页面 {} 内容为空, 跳过", page);
}
Err(RequestError::NotFound) => {
println!("⚠️ 页面 {} 不存在 (404), 跳过", page);
}
Err(RequestError::NetworkError) => {
println!("⚠️ 获取页面 {} 时网络错误, 跳过", page);
}
Err(RequestError::OtherError(e)) => {
println!("⚠️ 获取页面 {} 时错误: {}, 跳过", page, e);
}
}
@@ -170,90 +296,90 @@ pub(crate) fn download_images(uid: i32, title: &str) -> bool {
}
// 等待当前批次完成
for handle in page_handles.drain(..) {
for handle in page_handles {
handle.join().unwrap();
}
}
// 收集所有页面的图片URL,按页码排序
// 收集并排序所有页面的图片 URL
drop(tx);
let mut page_results: Vec<_> = rx.iter().collect();
page_results.sort_by_key(|(page_num, _)| *page_num);
page_results.into_iter()
.flat_map(|(_, page_images)| page_images)
.collect()
}
for (_, page_images) in page_results {
images.extend(page_images);
}
// 生成 JSON 文件
/// 生成 JSON 文件
fn generate_json_file(uid: i32, title: &str, images: &[String]) {
let album = Album {
title: title.to_string(),
images,
images: images.to_vec(),
};
let json = serde_json::to_string_pretty(&album).expect("Failed to serialize album");
let json = serde_json::to_string_pretty(&album).expect("❌ 序列化相册失败");
let json_path = format!("/mnt/share/data/albums/{}/index.json", uid);
let json_bytes = Bytes::from(json);
file::write_file(&json_path, json_bytes).expect("Failed to write JSON file");
file::write_file(&json_path, Bytes::from(json)).expect("❌ 写入 JSON 文件失败");
}
// 执行多线程下载
/// 多线程下载图片
fn download_images_multithreaded(uid: i32, images: &[String]) -> DownloadResult {
let (tx, rx) = mpsc::channel();
let images_arc = Arc::new(album.images.clone());
let uid_arc = Arc::new(uid);
let images_arc = Arc::new(images.to_vec());
// 创建线程池(限制并发数避免过多连接)
let max_threads = 8; // 限制并发线程数
let mut handles = vec![];
for chunk_start in (0..album.images.len()).step_by(max_threads) {
let chunk_end = std::cmp::min(chunk_start + max_threads, album.images.len());
// 创建线程池
let max_threads = 8;
for chunk_start in (0..images.len()).step_by(max_threads) {
let chunk_end = std::cmp::min(chunk_start + max_threads, images.len());
let mut handles = vec![];
for i in chunk_start..chunk_end {
let tx = tx.clone();
let images = Arc::clone(&images_arc);
let uid = Arc::clone(&uid_arc);
let handle = thread::spawn(move || {
let img_url = &images[i];
let file_path = format!("/mnt/share/data/albums/{}/{}.jpg", uid, i + 1);
let result = download_image(img_url, &file_path, 3);
tx.send((i + 1, result)).unwrap();
});
handles.push(handle);
}
// 等待当前批次完成
for handle in handles.drain(..) {
for handle in handles {
handle.join().unwrap();
}
}
// 收集下载结果
drop(tx); // 关闭发送端
let mut successful_downloads = 0;
let mut failed_downloads = 0;
drop(tx);
let (mut successful, mut failed, mut network_errors) = (0, 0, 0);
for (pic_num, result) in rx {
match result {
1 => {
successful_downloads += 1;
DownloadResult::Success => successful += 1,
DownloadResult::Failed => {
println!("❌ 图片 {} 下载失败(空内容或404)", pic_num);
failed += 1;
}
0 => {
println!("图片 {} 下载失败(空内容)", pic_num);
failed_downloads += 1;
DownloadResult::NetworkError => {
println!("⚠️ 图片 {} 下载失败(网络错误)", pic_num);
network_errors += 1;
}
2 => {
println!("图片 {} 下载失败(网络错误)", pic_num);
failed_downloads += 1;
}
_ => {}
}
}
println!(
"下载完成:成功 {} 张,失败 {} 张",
successful_downloads, failed_downloads
);
println!("📊 下载完成:成功 {} 张,失败 {} 张,网络错误 {} 张",
successful, failed, network_errors);
// 返回执行结果:至少成功下载一张图片返回 true,否则返回 false
failed_downloads == 0
// 返回最终结果
if failed == 0 && network_errors == 0 {
DownloadResult::Success
} else if network_errors > 0 {
DownloadResult::NetworkError
} else {
DownloadResult::Failed
}
}