mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-05 15:38:52 +08:00
feat: 后端支持封面下载
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user