Skip to content

Commit ac7e17c

Browse files
authored
Merge pull request #177 from lanyeeee/develop
Develop
2 parents edca1fe + 0fd43ab commit ac7e17c

12 files changed

Lines changed: 154 additions & 74 deletions

File tree

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2024 lanyeeee (https://github.com/lanyeeee)
3+
Copyright (c) 2024-2026 lanyeeee (https://github.com/lanyeeee)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

src-tauri/src/commands.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::path::PathBuf;
2+
use std::sync::Arc;
23
use std::time::Duration;
34

45
// TODO: 用`#![allow(clippy::used_underscore_binding)]`来消除警告
@@ -7,6 +8,7 @@ use indexmap::IndexMap;
78
use tauri::AppHandle;
89
use tauri_plugin_opener::OpenerExt;
910
use tauri_specta::Event;
11+
use tokio::sync::Semaphore;
1012
use tokio::task::JoinSet;
1113
use tokio::time::sleep;
1214
use walkdir::WalkDir;
@@ -307,20 +309,23 @@ pub async fn download_all_favorites(app: AppHandle) -> CommandResult<()> {
307309
let first_page = jm_client
308310
.get_favorite_folder(0, 1, FavoriteSort::FavoriteTime)
309311
.await
310-
.map_err(|err| CommandError::from("更新收藏夹失败", err))?;
312+
.map_err(|err| CommandError::from("获取收藏夹失败", err))?;
311313
favorite_comics.extend(first_page.list);
312314
// 计算总页数
313315
let count = first_page.count;
314316
let total = first_page
315317
.total
316318
.parse::<i64>()
317-
.map_err(|err| CommandError::from("更新收藏夹失败", err))?;
319+
.map_err(|err| CommandError::from("获取收藏夹失败", err))?;
318320
let page_count = (total / count) + 1;
319321
// 获取收藏夹剩余页
322+
let sem = Arc::new(Semaphore::new(5));
320323
let mut join_set = JoinSet::new();
321324
for page in 2..=page_count {
322325
let jm_client = jm_client.clone();
326+
let sem = sem.clone();
323327
join_set.spawn(async move {
328+
let _permit = sem.acquire().await?;
324329
let page = jm_client
325330
.get_favorite_folder(0, page, FavoriteSort::FavoriteTime)
326331
.await?;
@@ -330,7 +335,7 @@ pub async fn download_all_favorites(app: AppHandle) -> CommandResult<()> {
330335
// 等待所有请求完成
331336
while let Some(Ok(get_favorite_result)) = join_set.join_next().await {
332337
// 如果有请求失败,直接返回错误
333-
let page = get_favorite_result.map_err(|err| CommandError::from("更新收藏夹失败", err))?;
338+
let page = get_favorite_result.map_err(|err| CommandError::from("获取收藏夹失败", err))?;
334339
favorite_comics.extend(page.list);
335340
}
336341
// 至此,收藏夹已经全部获取完毕

src-tauri/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub struct Config {
3232
pub update_downloaded_comics_interval_sec: u64,
3333
pub api_domain_mode: ApiDomainMode,
3434
pub custom_api_domain: String,
35+
pub should_download_cover: bool,
3536
}
3637

3738
impl Config {
@@ -116,6 +117,7 @@ impl Config {
116117
update_downloaded_comics_interval_sec: 0,
117118
api_domain_mode: ApiDomainMode::Domain2,
118119
custom_api_domain: API_DOMAIN_2.to_string(),
120+
should_download_cover: true,
119121
}
120122
}
121123
}

src-tauri/src/download_manager.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ impl DownloadTask {
210210
let comic_title = &self.comic.name;
211211
let chapter_title = &self.chapter_info.chapter_title;
212212
let chapter_id = self.chapter_info.chapter_id;
213+
213214
if let Err(err) = self.comic.save_comic_metadata() {
214215
let err_title = format!("`{comic_title}`保存元数据失败");
215216
let string_chain = err.to_string_chain();
@@ -220,6 +221,21 @@ impl DownloadTask {
220221

221222
return;
222223
}
224+
225+
let should_download_cover = self.app.get_config().read().should_download_cover;
226+
if should_download_cover {
227+
if let Err(err) = self.download_cover().await {
228+
let err_title = format!("`{comic_title}`下载封面失败");
229+
let string_chain = err.to_string_chain();
230+
tracing::error!(err_title, message = string_chain);
231+
232+
self.set_state(DownloadTaskState::Failed);
233+
self.emit_download_task_update_event();
234+
235+
return;
236+
}
237+
}
238+
223239
// 获取此章节每张图片的下载链接以及对应的block_num
224240
let Some(urls_with_block_num) = self.get_urls_with_block_num(chapter_id).await else {
225241
return;
@@ -285,6 +301,28 @@ impl DownloadTask {
285301
self.emit_download_task_update_event();
286302
}
287303

304+
async fn download_cover(&self) -> anyhow::Result<()> {
305+
let cover_path = self.comic.get_cover_path().context("获取封面路径失败")?;
306+
// if cover_path.exists() {
307+
// return Ok(());
308+
// }
309+
310+
let comic_id = self.comic.id;
311+
let url = format!("https://cdn-msp3.18comic.vip/media/albums/{comic_id}.jpg");
312+
313+
let (img_data, _format) = self
314+
.app
315+
.get_jm_client()
316+
.get_img_data_and_format(&url)
317+
.await
318+
.context(format!("下载图片`{url}`失败"))?;
319+
320+
std::fs::write(&cover_path, img_data)
321+
.context(format!("保存图片`{}`失败", cover_path.display()))?;
322+
323+
Ok(())
324+
}
325+
288326
fn create_temp_download_dir(&self) -> Option<PathBuf> {
289327
let comic_title = &self.comic.name;
290328
let chapter_title = &self.chapter_info.chapter_title;

src-tauri/src/export.rs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use zip::{write::SimpleFileOptions, ZipWriter};
1919

2020
use crate::{
2121
events::{ExportCbzEvent, ExportPdfEvent},
22-
extensions::PathIsImg,
22+
extensions::{AnyhowErrorToStringChain, PathIsImg},
2323
types::{ChapterInfo, Comic, ComicInfo},
2424
};
2525

@@ -93,6 +93,13 @@ pub fn cbz(app: &AppHandle, comic: &Comic) -> anyhow::Result<()> {
9393
// 保证导出目录存在
9494
std::fs::create_dir_all(&chapter_export_dir)
9595
.context(format!("创建目录`{}`失败", chapter_export_dir.display()))?;
96+
// 先把封面拷贝到导出目录(如果有)
97+
if let Err(err) = copy_cover(comic, &chapter_export_dir) {
98+
let comic_title = &comic.name;
99+
let err_title = format!("`{comic_title}`导出cbz时,将封面拷贝到导出目录失败");
100+
let string_chain = err.to_string_chain();
101+
tracing::error!(err_title, message = string_chain);
102+
}
96103
// 并发处理
97104
let downloaded_chapter_infos = downloaded_chapter_infos.into_par_iter();
98105
downloaded_chapter_infos.try_for_each(|chapter_info| -> anyhow::Result<()> {
@@ -187,6 +194,18 @@ pub fn cbz(app: &AppHandle, comic: &Comic) -> anyhow::Result<()> {
187194
Ok(())
188195
}
189196

197+
fn copy_cover(comic: &Comic, chapter_export_dir: &Path) -> anyhow::Result<()> {
198+
let src_cover_path = comic.get_cover_path().context("获取封面路径失败")?;
199+
let cover_filename = src_cover_path.file_name().context("获取封面的文件名失败")?;
200+
201+
if src_cover_path.exists() {
202+
let dst_cover_path = chapter_export_dir.join(cover_filename);
203+
std::fs::copy(src_cover_path, dst_cover_path)?;
204+
}
205+
206+
Ok(())
207+
}
208+
190209
struct PdfCreateErrorEventGuard {
191210
uuid: String,
192211
app: AppHandle,
@@ -269,7 +288,7 @@ pub fn pdf(app: &AppHandle, comic: &Comic) -> anyhow::Result<()> {
269288
))?
270289
.filter_map(Result::ok)
271290
.map(|entry| entry.path())
272-
.filter(|path| path.is_img())
291+
.filter(|path| path.is_common_img())
273292
.collect();
274293
image_paths.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
275294

src-tauri/src/extensions.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@ impl AnyhowErrorToStringChain for anyhow::Error {
2525
}
2626

2727
pub trait PathIsImg {
28-
/// 判断路径是否为图片文件
28+
/// 判断路径是否为图片(jpg/png/webp/gif)
2929
fn is_img(&self) -> bool;
30+
31+
/// 判断路径是否为普通图片(jpg/png/webp)
32+
fn is_common_img(&self) -> bool;
3033
}
3134

3235
impl PathIsImg for std::path::Path {
3336
fn is_img(&self) -> bool {
37+
self.extension()
38+
.and_then(|ext| ext.to_str())
39+
.map(str::to_lowercase)
40+
.is_some_and(|ext| matches!(ext.as_str(), "jpg" | "png" | "webp" | "gif"))
41+
}
42+
43+
fn is_common_img(&self) -> bool {
3444
self.extension()
3545
.and_then(|ext| ext.to_str())
3646
.map(str::to_lowercase)

src-tauri/src/jm_client.rs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -536,17 +536,20 @@ impl JmClient {
536536
}
537537

538538
pub async fn get_img_data_and_format(&self, url: &str) -> anyhow::Result<(Bytes, ImageFormat)> {
539-
let request = self.img_client.read().get(url);
540-
539+
let request = self
540+
.img_client
541+
.read()
542+
.get(url)
543+
.header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36");
541544
let http_resp = request.send().await?;
545+
542546
let status = http_resp.status();
543547
if status != StatusCode::OK {
544548
let text = http_resp.text().await?;
545549
let err = anyhow!("下载图片`{url}`失败,预料之外的状态码: {text}");
546550
return Err(err);
547551
}
548552

549-
let mut headers = http_resp.headers().clone();
550553
let mut image_data = http_resp.bytes().await?;
551554

552555
if image_data.is_empty() {
@@ -563,22 +566,11 @@ impl JmClient {
563566
return Err(err);
564567
}
565568

566-
headers = http_resp.headers().clone();
567569
image_data = http_resp.bytes().await?;
568570
}
569-
// 获取 resp headers 的 content-type 字段
570-
let content_type = headers
571-
.get("content-type")
572-
.ok_or(anyhow!("响应中没有content-type字段"))?
573-
.to_str()
574-
.context("响应中的content-type字段不是utf-8字符串")?
575-
.to_string();
576-
// 确定原始图片格式
577-
let format = match content_type.as_str() {
578-
"image/webp" => ImageFormat::WebP,
579-
"image/gif" => ImageFormat::Gif,
580-
_ => return Err(anyhow!("原图出现了意料之外的格式: {content_type}")),
581-
};
571+
572+
let format = image::guess_format(&image_data)
573+
.context("无法从图片数据中猜测出图片格式,可能图片数据不完整或已损坏")?;
582574

583575
Ok((image_data, format))
584576
}

src-tauri/src/types/comic.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,17 @@ impl Comic {
236236
Ok(())
237237
}
238238

239+
pub fn get_cover_path(&self) -> anyhow::Result<PathBuf> {
240+
let comic_download_dir = self
241+
.comic_download_dir
242+
.as_ref()
243+
.context("`comic_download_dir`字段为`None`")?;
244+
245+
let cover_path = comic_download_dir.join("cover.jpg");
246+
247+
Ok(cover_path)
248+
}
249+
239250
fn update_chapter_infos_fields(&mut self) -> anyhow::Result<()> {
240251
let Some(comic_download_dir) = &self.comic_download_dir else {
241252
return Err(anyhow!("`comic_download_dir`字段为`None`"));

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
33
"productName": "jmcomic-downloader",
4-
"version": "0.16.2",
4+
"version": "0.17.0",
55
"identifier": "com.lanyeeee.jmcomic-downloader",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

src/bindings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ export type ComicInFavorite = { id: number; author: string; description: string
249249
export type ComicInSearch = { id: number; author: string; name: string; image: string; category: CategoryRespData; categorySub: CategorySubRespData; liked: boolean; isFavorite: boolean; updateAt: number; isDownloaded: boolean; comicDownloadDir: string }
250250
export type ComicInWeekly = { id: number; author: string; description: string; name: string; image: string; category: Category; category_sub: CategorySub; liked: boolean; is_favorite: boolean; update_at: number; is_downloaded: boolean; comic_download_dir: string }
251251
export type CommandError = { err_title: string; err_message: string }
252-
export type Config = { username: string; password: string; downloadDir: string; exportDir: string; downloadFormat: DownloadFormat; dirFmt: string; proxyMode: ProxyMode; proxyHost: string; proxyPort: number; enableFileLogger: boolean; chapterConcurrency: number; chapterDownloadIntervalSec: number; imgConcurrency: number; imgDownloadIntervalSec: number; downloadAllFavoritesIntervalSec: number; updateDownloadedComicsIntervalSec: number; apiDomainMode: ApiDomainMode; customApiDomain: string }
252+
export type Config = { username: string; password: string; downloadDir: string; exportDir: string; downloadFormat: DownloadFormat; dirFmt: string; proxyMode: ProxyMode; proxyHost: string; proxyPort: number; enableFileLogger: boolean; chapterConcurrency: number; chapterDownloadIntervalSec: number; imgConcurrency: number; imgDownloadIntervalSec: number; downloadAllFavoritesIntervalSec: number; updateDownloadedComicsIntervalSec: number; apiDomainMode: ApiDomainMode; customApiDomain: string; shouldDownloadCover: boolean }
253253
export type DownloadAllFavoritesEvent = { event: "GetFavoritesStart" } | { event: "GetComicsProgress"; data: { current: number; total: number } } | { event: "StartCreateDownloadTasks"; data: { comicId: number; comicTitle: string; current: number; total: number } } | { event: "CreatingDownloadTask"; data: { comicId: number; current: number } } | { event: "EndCreateDownloadTasks"; data: { comicId: number } } | { event: "GetComicsEnd" }
254254
export type DownloadFormat = "Jpeg" | "Png" | "Webp"
255255
export type DownloadSleepingEvent = { id: number; remainingSec: number }

0 commit comments

Comments
 (0)