mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-05 23:57:23 +08:00
feat: 后端支持音频下载
This commit is contained in:
@@ -13,7 +13,9 @@ pub struct Config {
|
||||
pub sessdata: String,
|
||||
pub prefer_video_quality: PreferVideoQuality,
|
||||
pub prefer_codec_type: PreferCodecType,
|
||||
pub prefer_audio_quality: PreferAudioQuality,
|
||||
pub download_video: bool,
|
||||
pub download_audio: bool,
|
||||
pub dir_fmt: String,
|
||||
pub dir_fmt_for_part: String,
|
||||
pub time_fmt: String,
|
||||
@@ -83,7 +85,9 @@ impl Config {
|
||||
sessdata: String::new(),
|
||||
prefer_video_quality: PreferVideoQuality::Best,
|
||||
prefer_codec_type: PreferCodecType::AVC,
|
||||
prefer_audio_quality: PreferAudioQuality::Best,
|
||||
download_video: true,
|
||||
download_audio: 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(),
|
||||
@@ -162,3 +166,31 @@ pub enum PreferCodecType {
|
||||
HEVC = 12,
|
||||
AV1 = 13,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Type,
|
||||
IntoPrimitive,
|
||||
FromPrimitive,
|
||||
)]
|
||||
#[repr(i64)]
|
||||
pub enum PreferAudioQuality {
|
||||
#[default]
|
||||
Best = -1,
|
||||
#[serde(rename = "64K")]
|
||||
Audio64K = 30216,
|
||||
#[serde(rename = "132K")]
|
||||
Audio132K = 30232,
|
||||
#[serde(rename = "192K")]
|
||||
Audio192K = 30280,
|
||||
#[serde(rename = "Dolby")]
|
||||
AudioDolby = 30250,
|
||||
#[serde(rename = "HiRes")]
|
||||
AudioHiRes = 30251,
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
downloader::tasks::video_task::VideoTask,
|
||||
downloader::tasks::{audio_task::AudioTask, video_task::VideoTask},
|
||||
extensions::AppHandleExt,
|
||||
types::{
|
||||
audio_quality::AudioQuality,
|
||||
bangumi_info::BangumiInfo,
|
||||
cheese_info::CheeseInfo,
|
||||
codec_type::CodecType,
|
||||
@@ -46,6 +47,7 @@ pub struct DownloadProgress {
|
||||
pub episode_dir: PathBuf,
|
||||
pub filename: String,
|
||||
pub video_task: VideoTask,
|
||||
pub audio_task: AudioTask,
|
||||
pub create_ts: u64,
|
||||
pub completed_ts: Option<u64>,
|
||||
}
|
||||
@@ -111,6 +113,7 @@ impl DownloadProgress {
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video,
|
||||
audio_task: tasks.audio,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -155,6 +158,7 @@ impl DownloadProgress {
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video,
|
||||
audio_task: tasks.audio,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -169,9 +173,11 @@ impl DownloadProgress {
|
||||
pub async fn prepare(&mut self, app: &AppHandle) -> anyhow::Result<()> {
|
||||
let video_selected = self.video_task.selected;
|
||||
let video_completed = self.video_task.completed;
|
||||
let audio_selected = self.audio_task.selected;
|
||||
let audio_completed = self.audio_task.completed;
|
||||
|
||||
if !video_selected && video_completed {
|
||||
// 如果视频没有选中,或者已经完成,则不需要准备
|
||||
if (!video_selected && !audio_selected) || (video_completed && audio_completed) {
|
||||
// 如果视频和音频都没有选中,或者都已经完成,则不需要准备
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -191,6 +197,11 @@ impl DownloadProgress {
|
||||
// 如果视频被选中且未完成,则准备视频任务
|
||||
self.video_task.prepare_normal(app, &media_url).await?;
|
||||
}
|
||||
|
||||
if audio_selected && !audio_completed {
|
||||
// 如果音频被选中且未完成,则准备音频任务
|
||||
self.audio_task.prepare_normal(app, &media_url).await?;
|
||||
}
|
||||
}
|
||||
EpisodeType::Bangumi => {
|
||||
let media_url = bili_client
|
||||
@@ -202,6 +213,11 @@ impl DownloadProgress {
|
||||
// 如果视频被选中且未完成,则准备视频任务
|
||||
self.video_task.prepare_bangumi(app, &media_url).await?;
|
||||
}
|
||||
|
||||
if audio_selected && !audio_completed {
|
||||
// 如果音频被选中且未完成,则准备音频任务
|
||||
self.audio_task.prepare_bangumi(app, &media_url).await?;
|
||||
}
|
||||
}
|
||||
EpisodeType::Cheese => {
|
||||
let Some(ep_id) = self.ep_id else {
|
||||
@@ -216,6 +232,11 @@ impl DownloadProgress {
|
||||
// 如果视频被选中且未完成,则准备视频任务
|
||||
self.video_task.prepare_cheese(app, &media_url).await?;
|
||||
}
|
||||
|
||||
if audio_selected && !audio_completed {
|
||||
// 如果音频被选中且未完成,则准备音频任务
|
||||
self.audio_task.prepare_cheese(app, &media_url).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,11 +291,12 @@ impl DownloadProgress {
|
||||
}
|
||||
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.video_task.is_completed()
|
||||
self.video_task.is_completed() && self.audio_task.is_completed()
|
||||
}
|
||||
|
||||
pub fn mark_uncompleted(&mut self) {
|
||||
self.video_task.mark_uncompleted();
|
||||
self.audio_task.mark_uncompleted();
|
||||
}
|
||||
|
||||
pub fn get_ids_string(&self) -> String {
|
||||
@@ -321,6 +343,7 @@ fn create_normal_progresses_for_single(
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video,
|
||||
audio_task: tasks.audio,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -354,6 +377,7 @@ fn create_normal_progresses_for_single(
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video,
|
||||
audio_task: tasks.audio,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -387,6 +411,7 @@ fn create_normal_progresses_for_single(
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video.clone(),
|
||||
audio_task: tasks.audio.clone(),
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -452,6 +477,7 @@ fn create_normal_progresses_for_season(
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video,
|
||||
audio_task: tasks.audio,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -485,6 +511,7 @@ fn create_normal_progresses_for_season(
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video,
|
||||
audio_task: tasks.audio,
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -519,6 +546,7 @@ fn create_normal_progresses_for_season(
|
||||
episode_dir: PathBuf::new(),
|
||||
filename: String::new(),
|
||||
video_task: tasks.video.clone(),
|
||||
audio_task: tasks.audio.clone(),
|
||||
create_ts,
|
||||
completed_ts: None,
|
||||
};
|
||||
@@ -534,6 +562,7 @@ fn create_normal_progresses_for_season(
|
||||
|
||||
struct Tasks {
|
||||
video: VideoTask,
|
||||
audio: AudioTask,
|
||||
}
|
||||
|
||||
impl Tasks {
|
||||
@@ -548,6 +577,15 @@ impl Tasks {
|
||||
completed: false,
|
||||
};
|
||||
|
||||
Self { video }
|
||||
let audio = AudioTask {
|
||||
selected: config.download_audio,
|
||||
url: String::new(),
|
||||
audio_quality: AudioQuality::Unknown,
|
||||
content_length: 0,
|
||||
chunks: Vec::new(),
|
||||
completed: false,
|
||||
};
|
||||
|
||||
Self { video, audio }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,14 @@ impl DownloadTask {
|
||||
tracing::debug!("{ids_string} `{filename}`视频下载完成");
|
||||
}
|
||||
|
||||
if !progress.audio_task.is_completed() && progress.audio_task.content_length != 0 {
|
||||
// 如果音频任务被选中且未完成且有要下载的内容,则下载音频
|
||||
self.download_audio(&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())
|
||||
@@ -412,6 +420,116 @@ impl DownloadTask {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_audio(self: &Arc<Self>, progress: &DownloadProgress) -> anyhow::Result<()> {
|
||||
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||
|
||||
let temp_file_path = episode_dir.join(format!(
|
||||
"{filename}.m4a.com.lanyeeee.bilibili-video-downloader"
|
||||
));
|
||||
let (audio_task, episode_title, ids_string) = {
|
||||
let progress = self.progress.read();
|
||||
(
|
||||
progress.audio_task.clone(),
|
||||
progress.episode_title.clone(),
|
||||
progress.get_ids_string(),
|
||||
)
|
||||
};
|
||||
|
||||
let file = if temp_file_path.exists() {
|
||||
// 如果文件已存在,则打开它
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&temp_file_path)?
|
||||
} else {
|
||||
// 如果文件不存在,创建它并预分配空间
|
||||
let file = File::create(&temp_file_path)?;
|
||||
file.allocate(audio_task.content_length)?;
|
||||
file
|
||||
};
|
||||
let file = Arc::new(Mutex::new(file));
|
||||
|
||||
let chunk_count = audio_task.chunks.len();
|
||||
|
||||
let mut join_set = JoinSet::new();
|
||||
for (chunk_index, chunk) in audio_task.chunks.iter().enumerate() {
|
||||
if chunk.completed {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (start, end) = (chunk.start, chunk.end);
|
||||
|
||||
let download_chunk_task = DownloadChunkTask {
|
||||
download_task: self.clone(),
|
||||
start,
|
||||
end,
|
||||
url: audio_task.url.to_string(),
|
||||
file: file.clone(),
|
||||
chunk_index,
|
||||
};
|
||||
|
||||
join_set.spawn(async move {
|
||||
download_chunk_task.process().await.context(format!(
|
||||
"分片`{chunk_index}/{chunk_count}`下载失败({start}-{end})"
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
while let Some(Ok(download_video_result)) = join_set.join_next().await {
|
||||
match download_video_result {
|
||||
Ok(i) => self.update_progress(|p| p.audio_task.chunks[i].completed = true),
|
||||
Err(err) => {
|
||||
let err_title = format!("{ids_string} `{episode_title}`音频的一个分片下载失败");
|
||||
let string_chain = err.to_string_chain();
|
||||
tracing::error!(err_title, message = string_chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let download_completed = self
|
||||
.progress
|
||||
.read()
|
||||
.audio_task
|
||||
.chunks
|
||||
.iter()
|
||||
.all(|chunk| chunk.completed);
|
||||
if !download_completed {
|
||||
return Err(anyhow!(
|
||||
"音频文件`{}`有分片未下载完成,[继续]可以跳过已下载分片断点续传",
|
||||
temp_file_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
let is_audio_file_complete = utils::is_mp4_complete(&temp_file_path).context(format!(
|
||||
"检查音频文件`{}`是否完整失败",
|
||||
temp_file_path.display()
|
||||
))?;
|
||||
|
||||
if !is_audio_file_complete {
|
||||
self.update_progress(|p| p.video_task.mark_uncompleted());
|
||||
return Err(anyhow!(
|
||||
"音频文件`{}`不完整,[继续]会重新下载所有分片",
|
||||
temp_file_path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// 重命名临时文件
|
||||
let m4a_path = episode_dir.join(format!("{filename}.m4a"));
|
||||
if m4a_path.exists() {
|
||||
std::fs::remove_file(&m4a_path)
|
||||
.context(format!("删除已存在的音频文件`{}`失败", m4a_path.display()))?;
|
||||
}
|
||||
std::fs::rename(&temp_file_path, &m4a_path).context(format!(
|
||||
"将临时文件`{}`重命名为`{}`失败",
|
||||
temp_file_path.display(),
|
||||
m4a_path.display()
|
||||
))?;
|
||||
|
||||
self.update_progress(|p| p.audio_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,333 @@
|
||||
use std::cmp::Reverse;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::AppHandle;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::{
|
||||
downloader::media_chunk::MediaChunk,
|
||||
extensions::AppHandleExt,
|
||||
types::{
|
||||
audio_quality::AudioQuality, bangumi_media_url::BangumiMediaUrl,
|
||||
cheese_media_url::CheeseMediaUrl, normal_media_url::NormalMediaUrl,
|
||||
},
|
||||
};
|
||||
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||
pub struct AudioTask {
|
||||
pub selected: bool,
|
||||
pub url: String,
|
||||
pub audio_quality: AudioQuality,
|
||||
pub content_length: u64,
|
||||
pub chunks: Vec<MediaChunk>,
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl AudioTask {
|
||||
pub async fn prepare_normal(
|
||||
&mut self,
|
||||
app: &AppHandle,
|
||||
media_url: &NormalMediaUrl,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
if let Some(medias) = &media_url.dash.audio {
|
||||
for media in medias {
|
||||
let app = app.clone();
|
||||
let id = media.id;
|
||||
|
||||
let mut urls = Vec::new();
|
||||
urls.extend_from_slice(&media.backup_url);
|
||||
urls.push(media.base_url.clone());
|
||||
|
||||
join_set.spawn(async move {
|
||||
let bili_client = app.get_bili_client();
|
||||
let url_with_content_length =
|
||||
bili_client.get_url_with_content_length(urls).await;
|
||||
MediaForPrepare {
|
||||
id,
|
||||
url_with_content_length,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(medias) = &media_url.dash.dolby.audio {
|
||||
for media in medias {
|
||||
let app = app.clone();
|
||||
let id = media.id;
|
||||
|
||||
let mut urls = Vec::new();
|
||||
urls.extend_from_slice(&media.backup_url);
|
||||
urls.push(media.base_url.clone());
|
||||
|
||||
join_set.spawn(async move {
|
||||
let bili_client = app.get_bili_client();
|
||||
let url_with_content_length =
|
||||
bili_client.get_url_with_content_length(urls).await;
|
||||
MediaForPrepare {
|
||||
id,
|
||||
url_with_content_length,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let flac = media_url.dash.flac.as_ref();
|
||||
if let Some(media) = flac.and_then(|flac| flac.audio.as_ref()) {
|
||||
let app = app.clone();
|
||||
let id = media.id;
|
||||
|
||||
let mut urls = Vec::new();
|
||||
urls.extend_from_slice(&media.backup_url);
|
||||
urls.push(media.base_url.clone());
|
||||
|
||||
join_set.spawn(async move {
|
||||
let bili_client = app.get_bili_client();
|
||||
let url_with_content_length = bili_client.get_url_with_content_length(urls).await;
|
||||
MediaForPrepare {
|
||||
id,
|
||||
url_with_content_length,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut medias: Vec<MediaForPrepare> = Vec::new();
|
||||
|
||||
while let Some(Ok(media)) = join_set.join_next().await {
|
||||
if !media.url_with_content_length.is_empty() {
|
||||
medias.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn prepare_bangumi(
|
||||
&mut self,
|
||||
app: &AppHandle,
|
||||
media_url: &BangumiMediaUrl,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(dash) = &media_url.dash else {
|
||||
// 如果没有音频,则直接返回
|
||||
self.completed = true;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(medias) = &dash.audio else {
|
||||
// 如果没有音频,则直接返回
|
||||
self.completed = true;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if medias.is_empty() {
|
||||
// 如果没有音频,则直接返回
|
||||
self.completed = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
for media in medias {
|
||||
let app = app.clone();
|
||||
let id = media.id;
|
||||
|
||||
let mut urls = Vec::new();
|
||||
urls.extend_from_slice(&media.backup_url);
|
||||
urls.push(media.base_url.clone());
|
||||
|
||||
join_set.spawn(async move {
|
||||
let bili_client = app.get_bili_client();
|
||||
let url_with_content_length = bili_client.get_url_with_content_length(urls).await;
|
||||
MediaForPrepare {
|
||||
id,
|
||||
url_with_content_length,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut medias: Vec<MediaForPrepare> = Vec::new();
|
||||
|
||||
while let Some(Ok(media)) = join_set.join_next().await {
|
||||
if !media.url_with_content_length.is_empty() {
|
||||
medias.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn prepare_cheese(
|
||||
&mut self,
|
||||
app: &AppHandle,
|
||||
media_url: &CheeseMediaUrl,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(dash) = &media_url.dash else {
|
||||
// 如果没有音频,则直接返回
|
||||
self.completed = true;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(medias) = &dash.audio else {
|
||||
// 如果没有音频,则直接返回
|
||||
self.completed = true;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if medias.is_empty() {
|
||||
// 如果没有音频,则直接返回
|
||||
self.completed = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut join_set = JoinSet::new();
|
||||
|
||||
for media in medias {
|
||||
let app = app.clone();
|
||||
let id = media.id;
|
||||
|
||||
let mut urls = Vec::new();
|
||||
urls.extend_from_slice(&media.backup_url);
|
||||
urls.push(media.base_url.clone());
|
||||
|
||||
join_set.spawn(async move {
|
||||
let bili_client = app.get_bili_client();
|
||||
let url_with_content_length = bili_client.get_url_with_content_length(urls).await;
|
||||
MediaForPrepare {
|
||||
id,
|
||||
url_with_content_length,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut medias: Vec<MediaForPrepare> = Vec::new();
|
||||
|
||||
while let Some(Ok(media)) = join_set.join_next().await {
|
||||
if !media.url_with_content_length.is_empty() {
|
||||
medias.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
if medias.is_empty() {
|
||||
return Err(anyhow!("获取音频地址失败"));
|
||||
}
|
||||
|
||||
self.prepare(app, medias);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prepare(&mut self, app: &AppHandle, mut medias: Vec<MediaForPrepare>) {
|
||||
medias.sort_by_key(|m| Reverse(m.id.to_audio_quality_for_prepare()));
|
||||
let best_quality_id = medias[0].id;
|
||||
|
||||
let prefer_quality = app.get_config().read().prefer_audio_quality;
|
||||
let prefer_quality_id: i64 = prefer_quality.into();
|
||||
let prefer_quality_found = medias.iter().any(|m| m.id == prefer_quality_id);
|
||||
let quality_filtered_medias: Vec<MediaForPrepare> = if prefer_quality_found {
|
||||
// 如果用户指定质量存在,则使用用户指定的质量
|
||||
medias
|
||||
.into_iter()
|
||||
.filter(|m| m.id == prefer_quality_id)
|
||||
.collect()
|
||||
} else {
|
||||
// 否则使用最高质量
|
||||
medias
|
||||
.into_iter()
|
||||
.filter(|m| m.id == best_quality_id)
|
||||
.collect()
|
||||
};
|
||||
|
||||
let media = &quality_filtered_medias[0];
|
||||
|
||||
self.audio_quality = media.id.into();
|
||||
|
||||
let (url, content_length) = media
|
||||
.url_with_content_length
|
||||
.iter()
|
||||
.find(|(url, _)| url.starts_with("https://upos-"))
|
||||
.unwrap_or(&media.url_with_content_length[0])
|
||||
.clone();
|
||||
|
||||
self.url = url;
|
||||
|
||||
if self.content_length != content_length {
|
||||
let chunk_count = content_length.div_ceil(CHUNK_SIZE);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let mut chunks = Vec::with_capacity(chunk_count as usize);
|
||||
for i in 0..chunk_count {
|
||||
let start = i * CHUNK_SIZE;
|
||||
let end = std::cmp::min(start + CHUNK_SIZE, content_length) - 1;
|
||||
chunks.push(MediaChunk {
|
||||
start,
|
||||
end,
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
|
||||
self.content_length = content_length;
|
||||
self.chunks = chunks;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_uncompleted(&mut self) {
|
||||
self.completed = false;
|
||||
self.chunks.iter_mut().for_each(|chunk| {
|
||||
chunk.completed = false;
|
||||
});
|
||||
}
|
||||
|
||||
pub fn is_completed(&self) -> bool {
|
||||
!self.selected || self.completed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MediaForPrepare {
|
||||
pub id: i64,
|
||||
pub url_with_content_length: Vec<(String, u64)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum AudioQualityForPrepare {
|
||||
Audio64K,
|
||||
Audio132K,
|
||||
Audio192K,
|
||||
AudioDolby,
|
||||
AudioHiRes,
|
||||
}
|
||||
|
||||
trait ToAudioQualityForPrepare {
|
||||
fn to_audio_quality_for_prepare(self) -> Option<AudioQualityForPrepare>;
|
||||
}
|
||||
|
||||
impl ToAudioQualityForPrepare for i64 {
|
||||
fn to_audio_quality_for_prepare(self) -> Option<AudioQualityForPrepare> {
|
||||
let audio_quality: AudioQuality = self.into();
|
||||
match audio_quality {
|
||||
AudioQuality::Audio64K => Some(AudioQualityForPrepare::Audio64K),
|
||||
AudioQuality::Audio132K => Some(AudioQualityForPrepare::Audio132K),
|
||||
AudioQuality::Audio192K => Some(AudioQualityForPrepare::Audio192K),
|
||||
AudioQuality::AudioDolby => Some(AudioQualityForPrepare::AudioDolby),
|
||||
AudioQuality::AudioHiRes => Some(AudioQualityForPrepare::AudioHiRes),
|
||||
AudioQuality::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod audio_task;
|
||||
pub mod video_task;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use num_enum::{FromPrimitive, IntoPrimitive};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
|
||||
#[derive(
|
||||
Default,
|
||||
Debug,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Type,
|
||||
IntoPrimitive,
|
||||
FromPrimitive,
|
||||
)]
|
||||
#[repr(i64)]
|
||||
pub enum AudioQuality {
|
||||
#[default]
|
||||
Unknown = -1,
|
||||
|
||||
#[serde(rename = "64K")]
|
||||
Audio64K = 30216,
|
||||
#[serde(rename = "132K")]
|
||||
Audio132K = 30232,
|
||||
#[serde(rename = "192K")]
|
||||
Audio192K = 30280,
|
||||
#[serde(rename = "Dolby")]
|
||||
AudioDolby = 30250,
|
||||
#[serde(rename = "HiRes")]
|
||||
AudioHiRes = 30251,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod audio_quality;
|
||||
pub mod bangumi_info;
|
||||
pub mod bangumi_media_url;
|
||||
pub mod cheese_info;
|
||||
|
||||
+5
-2
@@ -170,6 +170,8 @@ export type Activity = { head_bg_url: string; id: number; title: string }
|
||||
export type Arc = { aid: number; videos: number; type_id: number; type_name: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; state: number; duration: number; rights: RightsInNormalEp; author: Author; stat: StatInNormalEp; dynamic: string; dimension: Dimension; is_chargeable_season: boolean; is_blooper: boolean; enable_vt: number; vt_display: string; type_id_v2: number; type_name_v2: string; is_lesson_video: number }
|
||||
export type Area = { id: number; name: string }
|
||||
export type ArgueInfo = { argue_msg: string; argue_type: number; argue_link: string }
|
||||
export type AudioQuality = "Unknown" | "64K" | "132K" | "192K" | "Dolby" | "HiRes"
|
||||
export type AudioTask = { selected: boolean; url: string; audio_quality: AudioQuality; content_length: number; chunks: MediaChunk[]; completed: boolean }
|
||||
export type Author = { mid: number; name: string; face: string }
|
||||
export type AvatarIcon = { icon_resource: IconResource }
|
||||
export type BadgeInfo = { bg_color: string; bg_color_night: string; text: string }
|
||||
@@ -183,7 +185,7 @@ 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 = { downloadDir: string; enableFileLogger: boolean; sessdata: string; preferVideoQuality: PreferVideoQuality; preferCodecType: PreferCodecType; downloadVideo: boolean; dirFmt: string; dirFmtForPart: string; timeFmt: string; taskConcurrency: number; taskDownloadIntervalSec: number; chunkConcurrency: number; chunkDownloadIntervalSec: number }
|
||||
export type Config = { downloadDir: string; enableFileLogger: boolean; sessdata: string; preferVideoQuality: PreferVideoQuality; preferCodecType: PreferCodecType; preferAudioQuality: PreferAudioQuality; downloadVideo: boolean; downloadAudio: boolean; dirFmt: string; dirFmtForPart: string; timeFmt: string; taskConcurrency: number; taskDownloadIntervalSec: number; chunkConcurrency: number; chunkDownloadIntervalSec: number }
|
||||
export type Consulting = { consulting_flag: boolean; consulting_url: string }
|
||||
export type ContentList = { bold: boolean; content: string; number: string }
|
||||
export type Cooperation = { link: string }
|
||||
@@ -200,7 +202,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; 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; 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 }
|
||||
@@ -271,6 +273,7 @@ export type PlayStrategy = { strategies: string[] }
|
||||
export type PlayViewBusinessInfo = { user_status: UserStatusInCheeseUrl }
|
||||
export type PlayerInfo = { aid: number; bvid: string; allow_bp: boolean; no_share: boolean; cid: number; max_limit: number; page_no: number; has_next: boolean; ip_info: IpInfo; login_mid: number; login_mid_hash: string; is_owner: boolean; name: string; permission: string; level_info: LevelInfoInPlayerInfo; vip: VipInPlayerInfo; answer_status: number; block_time: number; role: string; last_play_time: number; last_play_cid: number; now_time: number; online_count: number; need_login_subtitle: boolean; subtitle: SubtitleInPlayerInfo; view_points: ViewPoint[]; preview_toast: string; options: Options; online_switch: OnlineSwitch; fawkes: Fawkes; show_switch: ShowSwitch; toast_block: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_ugc_pay_preview: boolean; elec_high_level: ElecHighLevel; disable_show_up_info: boolean; is_upower_exclusive_with_qa: boolean }
|
||||
export type Positive = { id: number; title: string }
|
||||
export type PreferAudioQuality = "Best" | "64K" | "132K" | "192K" | "Dolby" | "HiRes"
|
||||
export type PreferCodecType = "Unknown" | "AVC" | "HEVC" | "AV1"
|
||||
export type PreferVideoQuality = "Best" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K"
|
||||
export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string }
|
||||
|
||||
Reference in New Issue
Block a user