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:
@@ -24,8 +24,8 @@ use crate::{
|
||||
get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams,
|
||||
get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams,
|
||||
normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo,
|
||||
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo,
|
||||
watch_later_info::WatchLaterInfo,
|
||||
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, subtitle::Subtitle,
|
||||
user_info::UserInfo, watch_later_info::WatchLaterInfo,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -670,6 +670,21 @@ impl BiliClient {
|
||||
Ok(replies)
|
||||
}
|
||||
|
||||
pub async fn get_subtitle(&self, url: &str) -> anyhow::Result<Subtitle> {
|
||||
let request = self.api_client.read().get(url);
|
||||
let http_resp = request.send().await?;
|
||||
let status = http_resp.status();
|
||||
let body = http_resp.text().await?;
|
||||
if status != StatusCode::OK {
|
||||
return Err(anyhow!("预料之外的状态码({status}): {body}"));
|
||||
}
|
||||
// 尝试将body解析为Subtitle
|
||||
let subtitle: Subtitle =
|
||||
serde_json::from_str(&body).context(format!("将body解析为Subtitle失败: {body}"))?;
|
||||
|
||||
Ok(subtitle)
|
||||
}
|
||||
|
||||
fn get_cookie(&self) -> String {
|
||||
let sessdata = self.app.get_config().read().sessdata.clone();
|
||||
format!("SESSDATA={sessdata}")
|
||||
|
||||
@@ -22,6 +22,7 @@ pub struct Config {
|
||||
pub download_xml_danmaku: bool,
|
||||
pub download_ass_danmaku: bool,
|
||||
pub download_json_danmaku: bool,
|
||||
pub download_subtitle: bool,
|
||||
pub dir_fmt: String,
|
||||
pub dir_fmt_for_part: String,
|
||||
pub time_fmt: String,
|
||||
@@ -99,6 +100,7 @@ impl Config {
|
||||
download_xml_danmaku: true,
|
||||
download_ass_danmaku: true,
|
||||
download_json_danmaku: true,
|
||||
download_subtitle: 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(),
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
config::Config,
|
||||
downloader::tasks::{
|
||||
audio_task::AudioTask, danmaku_task::DanmakuTask, merge_task::MergeTask,
|
||||
video_task::VideoTask,
|
||||
subtitle_task::SubtitleTask, video_task::VideoTask,
|
||||
},
|
||||
extensions::AppHandleExt,
|
||||
types::{
|
||||
@@ -52,6 +52,7 @@ pub struct DownloadProgress {
|
||||
pub video_task: VideoTask,
|
||||
pub audio_task: AudioTask,
|
||||
pub merge_task: MergeTask,
|
||||
pub subtitle_task: SubtitleTask,
|
||||
pub danmaku_task: DanmakuTask,
|
||||
pub create_ts: u64,
|
||||
pub completed_ts: Option<u64>,
|
||||
@@ -121,6 +122,7 @@ impl DownloadProgress {
|
||||
audio_task: tasks.audio,
|
||||
merge_task: tasks.merge,
|
||||
danmaku_task: tasks.danmaku,
|
||||
subtitle_task: tasks.subtitle,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -168,6 +170,7 @@ impl DownloadProgress {
|
||||
audio_task: tasks.audio,
|
||||
merge_task: tasks.merge,
|
||||
danmaku_task: tasks.danmaku,
|
||||
subtitle_task: tasks.subtitle,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -304,6 +307,7 @@ impl DownloadProgress {
|
||||
&& self.audio_task.is_completed()
|
||||
&& self.merge_task.is_completed()
|
||||
&& self.danmaku_task.is_completed()
|
||||
&& self.subtitle_task.is_completed()
|
||||
}
|
||||
|
||||
pub fn mark_uncompleted(&mut self) {
|
||||
@@ -311,6 +315,7 @@ impl DownloadProgress {
|
||||
self.audio_task.mark_uncompleted();
|
||||
self.merge_task.completed = false;
|
||||
self.danmaku_task.completed = false;
|
||||
self.subtitle_task.completed = false;
|
||||
}
|
||||
|
||||
pub fn get_ids_string(&self) -> String {
|
||||
@@ -360,6 +365,7 @@ fn create_normal_progresses_for_single(
|
||||
audio_task: tasks.audio,
|
||||
merge_task: tasks.merge,
|
||||
danmaku_task: tasks.danmaku,
|
||||
subtitle_task: tasks.subtitle,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -396,6 +402,7 @@ fn create_normal_progresses_for_single(
|
||||
audio_task: tasks.audio,
|
||||
merge_task: tasks.merge,
|
||||
danmaku_task: tasks.danmaku,
|
||||
subtitle_task: tasks.subtitle,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -432,6 +439,7 @@ fn create_normal_progresses_for_single(
|
||||
audio_task: tasks.audio.clone(),
|
||||
merge_task: tasks.merge.clone(),
|
||||
danmaku_task: tasks.danmaku.clone(),
|
||||
subtitle_task: tasks.subtitle.clone(),
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -500,6 +508,7 @@ fn create_normal_progresses_for_season(
|
||||
audio_task: tasks.audio,
|
||||
merge_task: tasks.merge,
|
||||
danmaku_task: tasks.danmaku,
|
||||
subtitle_task: tasks.subtitle,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -536,6 +545,7 @@ fn create_normal_progresses_for_season(
|
||||
audio_task: tasks.audio,
|
||||
merge_task: tasks.merge,
|
||||
danmaku_task: tasks.danmaku,
|
||||
subtitle_task: tasks.subtitle,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -573,6 +583,7 @@ fn create_normal_progresses_for_season(
|
||||
audio_task: tasks.audio.clone(),
|
||||
merge_task: tasks.merge.clone(),
|
||||
danmaku_task: tasks.danmaku.clone(),
|
||||
subtitle_task: tasks.subtitle.clone(),
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -591,6 +602,7 @@ struct Tasks {
|
||||
audio: AudioTask,
|
||||
merge: MergeTask,
|
||||
danmaku: DanmakuTask,
|
||||
subtitle: SubtitleTask,
|
||||
}
|
||||
|
||||
impl Tasks {
|
||||
@@ -626,11 +638,17 @@ impl Tasks {
|
||||
completed: false,
|
||||
};
|
||||
|
||||
let subtitle = SubtitleTask {
|
||||
selected: config.download_subtitle,
|
||||
completed: false,
|
||||
};
|
||||
|
||||
Self {
|
||||
video,
|
||||
audio,
|
||||
merge,
|
||||
danmaku,
|
||||
subtitle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +311,13 @@ impl DownloadTask {
|
||||
tracing::debug!("{ids_string} `{filename}`弹幕下载完成");
|
||||
}
|
||||
|
||||
if !progress.subtitle_task.is_completed() {
|
||||
self.download_subtitle(&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())
|
||||
@@ -668,6 +675,52 @@ impl DownloadTask {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_subtitle(&self, progress: &DownloadProgress) -> anyhow::Result<()> {
|
||||
use std::fmt::Write;
|
||||
|
||||
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||
|
||||
let (aid, cid) = {
|
||||
let progress = self.progress.read();
|
||||
(progress.aid, progress.cid)
|
||||
};
|
||||
|
||||
let bili_client = self.app.get_bili_client();
|
||||
let player_info = bili_client
|
||||
.get_player_info(aid, cid)
|
||||
.await
|
||||
.context("获取播放器信息失败")?;
|
||||
|
||||
let subtitle = &player_info.subtitle;
|
||||
for subtitle_detail in &subtitle.subtitles {
|
||||
let url = format!("http:{}", subtitle_detail.subtitle_url);
|
||||
let subtitle = bili_client
|
||||
.get_subtitle(&url)
|
||||
.await
|
||||
.context("获取字幕失败")?;
|
||||
|
||||
let mut srt_content = String::new();
|
||||
for (i, b) in subtitle.body.iter().enumerate() {
|
||||
let index = i + 1;
|
||||
let content = &b.content;
|
||||
let start_time = utils::seconds_to_srt_time(b.from);
|
||||
let end_time = utils::seconds_to_srt_time(b.to);
|
||||
let _ = writeln!(
|
||||
&mut srt_content,
|
||||
"{index}\n{start_time} --> {end_time}\n{content}\n"
|
||||
);
|
||||
}
|
||||
|
||||
let lan = utils::filename_filter(&subtitle_detail.lan);
|
||||
let save_path = episode_dir.join(format!("{filename}.{lan}.srt"));
|
||||
std::fs::write(save_path, srt_content)?;
|
||||
}
|
||||
|
||||
self.update_progress(|p| p.subtitle_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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod audio_task;
|
||||
pub mod danmaku_task;
|
||||
pub mod merge_task;
|
||||
pub mod subtitle_task;
|
||||
pub mod video_task;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub struct SubtitleTask {
|
||||
pub selected: bool,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl SubtitleTask {
|
||||
pub fn is_completed(&self) -> bool {
|
||||
!self.selected || self.completed
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ pub mod normal_media_url;
|
||||
pub mod player_info;
|
||||
pub mod qrcode_data;
|
||||
pub mod qrcode_status;
|
||||
pub mod subtitle;
|
||||
pub mod user_info;
|
||||
pub mod video_quality;
|
||||
pub mod watch_later_info;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub struct Subtitle {
|
||||
pub font_size: f64,
|
||||
pub font_color: String,
|
||||
pub background_alpha: f64,
|
||||
pub background_color: String,
|
||||
#[serde(rename = "Stroke")]
|
||||
pub stroke: String,
|
||||
pub body: Vec<Body>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub struct Body {
|
||||
pub from: f64,
|
||||
pub to: f64,
|
||||
pub location: i64,
|
||||
pub content: String,
|
||||
}
|
||||
@@ -162,3 +162,17 @@ impl ToXml for Vec<DmSegMobileReply> {
|
||||
Ok(xml)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
#[allow(clippy::similar_names)]
|
||||
pub fn seconds_to_srt_time(seconds: f64) -> String {
|
||||
let total_ms = (seconds * 1000.0).round() as u64;
|
||||
let ms = total_ms % 1000;
|
||||
let total_s = total_ms / 1000;
|
||||
let s = total_s % 60;
|
||||
let total_m = total_s / 60;
|
||||
let m = total_m % 60;
|
||||
let h = total_m / 60;
|
||||
format!("{h:02}:{m:02}:{s:02},{ms:03}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user