diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index 9666c34..f897e66 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -685,6 +685,36 @@ impl BiliClient { Ok(subtitle) } + pub async fn get_cover_data_and_ext(&self, url: &str) -> anyhow::Result<(Bytes, String)> { + let request = self.api_client.read().get(url); + let http_resp = request.send().await?; + // 检查http响应状态码 + let status = http_resp.status(); + if status != StatusCode::OK { + let body = http_resp.text().await?; + return Err(anyhow!("预料之外的状态码({status}): {body}")); + } + + let content_type = http_resp + .headers() + .get("Content-Type") + .context("缺少 Content-Type 响应头")? + .to_str() + .context("Content-Type 响应头无法转换为字符串")? + .to_string(); + + let ext = match content_type.as_str() { + "image/png" => "png", + "image/webp" => "webp", + "image/avif" => "avif", + _ => "jpg", + }; + + let bytes = http_resp.bytes().await?; + + Ok((bytes, ext.to_string())) + } + fn get_cookie(&self) -> String { let sessdata = self.app.get_config().read().sessdata.clone(); format!("SESSDATA={sessdata}") diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 4edd51c..545b92a 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -9,6 +9,7 @@ use crate::danmaku_xml_to_ass::canvas::CanvasConfig; #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[allow(clippy::struct_excessive_bools)] +#[allow(clippy::struct_field_names)] pub struct Config { pub download_dir: PathBuf, pub enable_file_logger: bool, @@ -23,6 +24,7 @@ pub struct Config { pub download_ass_danmaku: bool, pub download_json_danmaku: bool, pub download_subtitle: bool, + pub download_cover: bool, pub dir_fmt: String, pub dir_fmt_for_part: String, pub time_fmt: String, @@ -101,6 +103,7 @@ impl Config { download_ass_danmaku: true, download_json_danmaku: true, download_subtitle: true, + download_cover: true, dir_fmt: "{collection_title}/{episode_title}".to_string(), dir_fmt_for_part: DEFAULT_FMT_FOR_PART.to_string(), time_fmt: "%Y-%m-%d_%H-%M-%S".to_string(), diff --git a/src-tauri/src/downloader/download_progress.rs b/src-tauri/src/downloader/download_progress.rs index 482e978..d8912a6 100644 --- a/src-tauri/src/downloader/download_progress.rs +++ b/src-tauri/src/downloader/download_progress.rs @@ -12,8 +12,8 @@ use uuid::Uuid; use crate::{ config::Config, downloader::tasks::{ - audio_task::AudioTask, danmaku_task::DanmakuTask, merge_task::MergeTask, - subtitle_task::SubtitleTask, video_task::VideoTask, + audio_task::AudioTask, cover_task::CoverTask, danmaku_task::DanmakuTask, + merge_task::MergeTask, subtitle_task::SubtitleTask, video_task::VideoTask, }, extensions::AppHandleExt, types::{ @@ -54,6 +54,7 @@ pub struct DownloadProgress { pub merge_task: MergeTask, pub subtitle_task: SubtitleTask, pub danmaku_task: DanmakuTask, + pub cover_task: CoverTask, pub create_ts: u64, pub completed_ts: Option, } @@ -123,6 +124,7 @@ impl DownloadProgress { merge_task: tasks.merge, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, + cover_task: tasks.cover, create_ts, completed_ts: None, }; @@ -171,6 +173,7 @@ impl DownloadProgress { merge_task: tasks.merge, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, + cover_task: tasks.cover, create_ts, completed_ts: None, }; @@ -308,6 +311,7 @@ impl DownloadProgress { && self.merge_task.is_completed() && self.danmaku_task.is_completed() && self.subtitle_task.is_completed() + && self.cover_task.is_completed() } pub fn mark_uncompleted(&mut self) { @@ -316,6 +320,7 @@ impl DownloadProgress { self.merge_task.completed = false; self.danmaku_task.completed = false; self.subtitle_task.completed = false; + self.cover_task.completed = false; } pub fn get_ids_string(&self) -> String { @@ -366,6 +371,7 @@ fn create_normal_progresses_for_single( merge_task: tasks.merge, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, + cover_task: tasks.cover, create_ts, completed_ts: None, }; @@ -403,6 +409,7 @@ fn create_normal_progresses_for_single( merge_task: tasks.merge, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, + cover_task: tasks.cover, create_ts, completed_ts: None, }; @@ -440,6 +447,7 @@ fn create_normal_progresses_for_single( merge_task: tasks.merge.clone(), danmaku_task: tasks.danmaku.clone(), subtitle_task: tasks.subtitle.clone(), + cover_task: tasks.cover.clone(), create_ts, completed_ts: None, }; @@ -509,6 +517,7 @@ fn create_normal_progresses_for_season( merge_task: tasks.merge, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, + cover_task: tasks.cover, create_ts, completed_ts: None, }; @@ -546,6 +555,7 @@ fn create_normal_progresses_for_season( merge_task: tasks.merge, danmaku_task: tasks.danmaku, subtitle_task: tasks.subtitle, + cover_task: tasks.cover, create_ts, completed_ts: None, }; @@ -584,6 +594,7 @@ fn create_normal_progresses_for_season( merge_task: tasks.merge.clone(), danmaku_task: tasks.danmaku.clone(), subtitle_task: tasks.subtitle.clone(), + cover_task: tasks.cover.clone(), create_ts, completed_ts: None, }; @@ -603,10 +614,11 @@ struct Tasks { merge: MergeTask, danmaku: DanmakuTask, subtitle: SubtitleTask, + cover: CoverTask, } impl Tasks { - fn new(config: &Config, _cover_url: &str) -> Self { + fn new(config: &Config, cover_url: &str) -> Self { let video = VideoTask { selected: config.download_video, url: String::new(), @@ -643,12 +655,19 @@ impl Tasks { completed: false, }; + let cover = CoverTask { + selected: config.download_cover, + url: cover_url.to_string(), + completed: false, + }; + Self { video, audio, merge, danmaku, subtitle, + cover, } } } diff --git a/src-tauri/src/downloader/download_task.rs b/src-tauri/src/downloader/download_task.rs index eca7dbe..8e5e93b 100644 --- a/src-tauri/src/downloader/download_task.rs +++ b/src-tauri/src/downloader/download_task.rs @@ -318,6 +318,13 @@ impl DownloadTask { tracing::debug!("{ids_string} `{filename}`字幕下载完成"); } + if !progress.cover_task.is_completed() { + self.download_cover(&progress) + .await + .context(format!("{ids_string} `{filename}`下载封面失败"))?; + tracing::debug!("{ids_string} `{filename}`封面下载完成"); + } + let completed_ts = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) @@ -721,6 +728,24 @@ impl DownloadTask { Ok(()) } + async fn download_cover(&self, progress: &DownloadProgress) -> anyhow::Result<()> { + let (episode_dir, filename) = (&progress.episode_dir, &progress.filename); + + let bili_client = self.app.get_bili_client(); + let (cover_data, ext) = bili_client + .get_cover_data_and_ext(&progress.cover_task.url) + .await + .context("获取封面失败")?; + + let save_path = episode_dir.join(format!("{filename}.{ext}")); + std::fs::write(&save_path, cover_data) + .context(format!("保存封面到`{}`失败", save_path.display()))?; + + self.update_progress(|p| p.cover_task.completed = true); + + Ok(()) + } + async fn sleep_between_task(&self) { let task_id = &self.task_id; let mut remaining_sec = self.app.get_config().read().task_download_interval_sec; diff --git a/src-tauri/src/downloader/tasks/cover_task.rs b/src-tauri/src/downloader/tasks/cover_task.rs new file mode 100644 index 0000000..0226052 --- /dev/null +++ b/src-tauri/src/downloader/tasks/cover_task.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct CoverTask { + pub selected: bool, + pub url: String, + pub completed: bool, +} + +impl CoverTask { + pub fn is_completed(&self) -> bool { + !self.selected || self.completed + } +} diff --git a/src-tauri/src/downloader/tasks/mod.rs b/src-tauri/src/downloader/tasks/mod.rs index fc488f3..e5d332c 100644 --- a/src-tauri/src/downloader/tasks/mod.rs +++ b/src-tauri/src/downloader/tasks/mod.rs @@ -1,4 +1,5 @@ pub mod audio_task; +pub mod cover_task; pub mod danmaku_task; pub mod merge_task; pub mod subtitle_task; diff --git a/src/bindings.ts b/src/bindings.ts index 16398ad..a399740 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -240,10 +240,11 @@ export type CntInfo = { collect: number; play: number; thumb_up: number; share: export type CntInfoInMedia = { collect: number; play: number; danmaku: number; vt: number; play_switch: number; reply: number; view_text_1: string } export type CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1" export type CommandError = { err_title: string; err_message: string } -export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig } +export type Config = { download_dir: string; enable_file_logger: boolean; sessdata: string; prefer_video_quality: PreferVideoQuality; prefer_codec_type: PreferCodecType; prefer_audio_quality: PreferAudioQuality; download_video: boolean; download_audio: boolean; auto_merge: boolean; download_xml_danmaku: boolean; download_ass_danmaku: boolean; download_json_danmaku: boolean; download_subtitle: boolean; download_cover: boolean; dir_fmt: string; dir_fmt_for_part: string; time_fmt: string; task_concurrency: number; task_download_interval_sec: number; chunk_concurrency: number; chunk_download_interval_sec: number; danmaku_config: CanvasConfig } export type Consulting = { consulting_flag: boolean; consulting_url: string } export type ContentList = { bold: boolean; content: string; number: string } export type Cooperation = { link: string } +export type CoverTask = { selected: boolean; url: string; completed: boolean } export type CreateBangumiDownloadTaskParams = { ep_ids: number[]; info: BangumiInfo } export type CreateCheeseDownloadTaskParams = { ep_ids: number[]; info: CheeseInfo } export type CreateDownloadTaskParams = { Normal: CreateNormalDownloadTaskParams } | { Bangumi: CreateBangumiDownloadTaskParams } | { Cheese: CreateCheeseDownloadTaskParams } @@ -258,7 +259,7 @@ export type DimensionInBangumi = { height: number; rotate: number; width: number export type DimensionInWatchLater = { width: number; height: number; rotate: number } export type Dolby = { type: number; audio: MediaInNormal[] | null } export type DownloadEvent = { event: "Speed"; data: { speed: string } } | { event: "TaskCreate"; data: { state: DownloadTaskState; progress: DownloadProgress } } | { event: "TaskStateUpdate"; data: { task_id: string; state: DownloadTaskState } } | { event: "TaskSleeping"; data: { task_id: string; remaining_sec: number } } | { event: "TaskDelete"; data: { task_id: string } } | { event: "ProgressPreparing"; data: { task_id: string } } | { event: "ProgressUpdate"; data: { progress: DownloadProgress } } -export type DownloadProgress = { task_id: string; episode_type: EpisodeType; aid: number; bvid: string | null; cid: number; ep_id: number | null; duration: number; pub_ts: number; collection_title: string; part_title: string | null; part_order: number | null; episode_title: string; episode_order: number; up_name: string | null; up_uid: number | null; up_avatar: string | null; episode_dir: string; filename: string; video_task: VideoTask; audio_task: AudioTask; merge_task: MergeTask; subtitle_task: SubtitleTask; danmaku_task: DanmakuTask; create_ts: number; completed_ts: number | null } +export type DownloadProgress = { task_id: string; episode_type: EpisodeType; aid: number; bvid: string | null; cid: number; ep_id: number | null; duration: number; pub_ts: number; collection_title: string; part_title: string | null; part_order: number | null; episode_title: string; episode_order: number; up_name: string | null; up_uid: number | null; up_avatar: string | null; episode_dir: string; filename: string; video_task: VideoTask; audio_task: AudioTask; merge_task: MergeTask; subtitle_task: SubtitleTask; danmaku_task: DanmakuTask; cover_task: CoverTask; create_ts: number; completed_ts: number | null } export type DownloadTaskState = "Pending" | "Downloading" | "Paused" | "Completed" | "Failed" export type DurlDetailInBangumi = { size: number; ahead: string; length: number; vhead: string; backup_url: string[]; url: string; order: number; md5: string } export type DurlDetailInCheese = { size: number; ahead: string; length: number; vhead: string; backup_url: string[]; url: string; order: number; md5: string }