mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 17:16:52 +08:00
feat: 后端支持封面下载
This commit is contained in:
@@ -685,6 +685,36 @@ impl BiliClient {
|
|||||||
Ok(subtitle)
|
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 {
|
fn get_cookie(&self) -> String {
|
||||||
let sessdata = self.app.get_config().read().sessdata.clone();
|
let sessdata = self.app.get_config().read().sessdata.clone();
|
||||||
format!("SESSDATA={sessdata}")
|
format!("SESSDATA={sessdata}")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use crate::danmaku_xml_to_ass::canvas::CanvasConfig;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
#[allow(clippy::struct_field_names)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub download_dir: PathBuf,
|
pub download_dir: PathBuf,
|
||||||
pub enable_file_logger: bool,
|
pub enable_file_logger: bool,
|
||||||
@@ -23,6 +24,7 @@ pub struct Config {
|
|||||||
pub download_ass_danmaku: bool,
|
pub download_ass_danmaku: bool,
|
||||||
pub download_json_danmaku: bool,
|
pub download_json_danmaku: bool,
|
||||||
pub download_subtitle: bool,
|
pub download_subtitle: bool,
|
||||||
|
pub download_cover: bool,
|
||||||
pub dir_fmt: String,
|
pub dir_fmt: String,
|
||||||
pub dir_fmt_for_part: String,
|
pub dir_fmt_for_part: String,
|
||||||
pub time_fmt: String,
|
pub time_fmt: String,
|
||||||
@@ -101,6 +103,7 @@ impl Config {
|
|||||||
download_ass_danmaku: true,
|
download_ass_danmaku: true,
|
||||||
download_json_danmaku: true,
|
download_json_danmaku: true,
|
||||||
download_subtitle: true,
|
download_subtitle: true,
|
||||||
|
download_cover: true,
|
||||||
dir_fmt: "{collection_title}/{episode_title}".to_string(),
|
dir_fmt: "{collection_title}/{episode_title}".to_string(),
|
||||||
dir_fmt_for_part: DEFAULT_FMT_FOR_PART.to_string(),
|
dir_fmt_for_part: DEFAULT_FMT_FOR_PART.to_string(),
|
||||||
time_fmt: "%Y-%m-%d_%H-%M-%S".to_string(),
|
time_fmt: "%Y-%m-%d_%H-%M-%S".to_string(),
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use uuid::Uuid;
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
downloader::tasks::{
|
downloader::tasks::{
|
||||||
audio_task::AudioTask, danmaku_task::DanmakuTask, merge_task::MergeTask,
|
audio_task::AudioTask, cover_task::CoverTask, danmaku_task::DanmakuTask,
|
||||||
subtitle_task::SubtitleTask, video_task::VideoTask,
|
merge_task::MergeTask, subtitle_task::SubtitleTask, video_task::VideoTask,
|
||||||
},
|
},
|
||||||
extensions::AppHandleExt,
|
extensions::AppHandleExt,
|
||||||
types::{
|
types::{
|
||||||
@@ -54,6 +54,7 @@ pub struct DownloadProgress {
|
|||||||
pub merge_task: MergeTask,
|
pub merge_task: MergeTask,
|
||||||
pub subtitle_task: SubtitleTask,
|
pub subtitle_task: SubtitleTask,
|
||||||
pub danmaku_task: DanmakuTask,
|
pub danmaku_task: DanmakuTask,
|
||||||
|
pub cover_task: CoverTask,
|
||||||
pub create_ts: u64,
|
pub create_ts: u64,
|
||||||
pub completed_ts: Option<u64>,
|
pub completed_ts: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -123,6 +124,7 @@ impl DownloadProgress {
|
|||||||
merge_task: tasks.merge,
|
merge_task: tasks.merge,
|
||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
|
cover_task: tasks.cover,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -171,6 +173,7 @@ impl DownloadProgress {
|
|||||||
merge_task: tasks.merge,
|
merge_task: tasks.merge,
|
||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
|
cover_task: tasks.cover,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -308,6 +311,7 @@ impl DownloadProgress {
|
|||||||
&& self.merge_task.is_completed()
|
&& self.merge_task.is_completed()
|
||||||
&& self.danmaku_task.is_completed()
|
&& self.danmaku_task.is_completed()
|
||||||
&& self.subtitle_task.is_completed()
|
&& self.subtitle_task.is_completed()
|
||||||
|
&& self.cover_task.is_completed()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_uncompleted(&mut self) {
|
pub fn mark_uncompleted(&mut self) {
|
||||||
@@ -316,6 +320,7 @@ impl DownloadProgress {
|
|||||||
self.merge_task.completed = false;
|
self.merge_task.completed = false;
|
||||||
self.danmaku_task.completed = false;
|
self.danmaku_task.completed = false;
|
||||||
self.subtitle_task.completed = false;
|
self.subtitle_task.completed = false;
|
||||||
|
self.cover_task.completed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ids_string(&self) -> String {
|
pub fn get_ids_string(&self) -> String {
|
||||||
@@ -366,6 +371,7 @@ fn create_normal_progresses_for_single(
|
|||||||
merge_task: tasks.merge,
|
merge_task: tasks.merge,
|
||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
|
cover_task: tasks.cover,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -403,6 +409,7 @@ fn create_normal_progresses_for_single(
|
|||||||
merge_task: tasks.merge,
|
merge_task: tasks.merge,
|
||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
|
cover_task: tasks.cover,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -440,6 +447,7 @@ fn create_normal_progresses_for_single(
|
|||||||
merge_task: tasks.merge.clone(),
|
merge_task: tasks.merge.clone(),
|
||||||
danmaku_task: tasks.danmaku.clone(),
|
danmaku_task: tasks.danmaku.clone(),
|
||||||
subtitle_task: tasks.subtitle.clone(),
|
subtitle_task: tasks.subtitle.clone(),
|
||||||
|
cover_task: tasks.cover.clone(),
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -509,6 +517,7 @@ fn create_normal_progresses_for_season(
|
|||||||
merge_task: tasks.merge,
|
merge_task: tasks.merge,
|
||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
|
cover_task: tasks.cover,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -546,6 +555,7 @@ fn create_normal_progresses_for_season(
|
|||||||
merge_task: tasks.merge,
|
merge_task: tasks.merge,
|
||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
|
cover_task: tasks.cover,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -584,6 +594,7 @@ fn create_normal_progresses_for_season(
|
|||||||
merge_task: tasks.merge.clone(),
|
merge_task: tasks.merge.clone(),
|
||||||
danmaku_task: tasks.danmaku.clone(),
|
danmaku_task: tasks.danmaku.clone(),
|
||||||
subtitle_task: tasks.subtitle.clone(),
|
subtitle_task: tasks.subtitle.clone(),
|
||||||
|
cover_task: tasks.cover.clone(),
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -603,10 +614,11 @@ struct Tasks {
|
|||||||
merge: MergeTask,
|
merge: MergeTask,
|
||||||
danmaku: DanmakuTask,
|
danmaku: DanmakuTask,
|
||||||
subtitle: SubtitleTask,
|
subtitle: SubtitleTask,
|
||||||
|
cover: CoverTask,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
fn new(config: &Config, _cover_url: &str) -> Self {
|
fn new(config: &Config, cover_url: &str) -> Self {
|
||||||
let video = VideoTask {
|
let video = VideoTask {
|
||||||
selected: config.download_video,
|
selected: config.download_video,
|
||||||
url: String::new(),
|
url: String::new(),
|
||||||
@@ -643,12 +655,19 @@ impl Tasks {
|
|||||||
completed: false,
|
completed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let cover = CoverTask {
|
||||||
|
selected: config.download_cover,
|
||||||
|
url: cover_url.to_string(),
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
video,
|
video,
|
||||||
audio,
|
audio,
|
||||||
merge,
|
merge,
|
||||||
danmaku,
|
danmaku,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
cover,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,13 @@ impl DownloadTask {
|
|||||||
tracing::debug!("{ids_string} `{filename}`字幕下载完成");
|
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()
|
let completed_ts = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
@@ -721,6 +728,24 @@ impl DownloadTask {
|
|||||||
Ok(())
|
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) {
|
async fn sleep_between_task(&self) {
|
||||||
let task_id = &self.task_id;
|
let task_id = &self.task_id;
|
||||||
let mut remaining_sec = self.app.get_config().read().task_download_interval_sec;
|
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 audio_task;
|
||||||
|
pub mod cover_task;
|
||||||
pub mod danmaku_task;
|
pub mod danmaku_task;
|
||||||
pub mod merge_task;
|
pub mod merge_task;
|
||||||
pub mod subtitle_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 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 CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1"
|
||||||
export type CommandError = { err_title: string; err_message: string }
|
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 Consulting = { consulting_flag: boolean; consulting_url: string }
|
||||||
export type ContentList = { bold: boolean; content: string; number: string }
|
export type ContentList = { bold: boolean; content: string; number: string }
|
||||||
export type Cooperation = { link: 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 CreateBangumiDownloadTaskParams = { ep_ids: number[]; info: BangumiInfo }
|
||||||
export type CreateCheeseDownloadTaskParams = { ep_ids: number[]; info: CheeseInfo }
|
export type CreateCheeseDownloadTaskParams = { ep_ids: number[]; info: CheeseInfo }
|
||||||
export type CreateDownloadTaskParams = { Normal: CreateNormalDownloadTaskParams } | { Bangumi: CreateBangumiDownloadTaskParams } | { Cheese: CreateCheeseDownloadTaskParams }
|
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 DimensionInWatchLater = { width: number; height: number; rotate: number }
|
||||||
export type Dolby = { type: number; audio: MediaInNormal[] | null }
|
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 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 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 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 }
|
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