feat: 后端支持封面下载

This commit is contained in:
lanyeeee
2025-07-27 05:07:07 +08:00
parent 3eed0de812
commit 999c48cd5e
7 changed files with 99 additions and 5 deletions
+30
View File
@@ -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}")
+3
View File
@@ -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(),
+22 -3
View File
@@ -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<u64>,
}
@@ -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,
}
}
}
+25
View File
@@ -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;
@@ -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
}
}
+1
View File
@@ -1,4 +1,5 @@
pub mod audio_task;
pub mod cover_task;
pub mod danmaku_task;
pub mod merge_task;
pub mod subtitle_task;
+3 -2
View File
@@ -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 }