mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 09:06:53 +08:00
feat: 后端支持音视频合并
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -16,6 +16,7 @@ pub struct Config {
|
|||||||
pub prefer_audio_quality: PreferAudioQuality,
|
pub prefer_audio_quality: PreferAudioQuality,
|
||||||
pub download_video: bool,
|
pub download_video: bool,
|
||||||
pub download_audio: bool,
|
pub download_audio: bool,
|
||||||
|
pub auto_merge: 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,
|
||||||
@@ -88,6 +89,7 @@ impl Config {
|
|||||||
prefer_audio_quality: PreferAudioQuality::Best,
|
prefer_audio_quality: PreferAudioQuality::Best,
|
||||||
download_video: true,
|
download_video: true,
|
||||||
download_audio: true,
|
download_audio: true,
|
||||||
|
auto_merge: 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(),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config,
|
config::Config,
|
||||||
downloader::tasks::{audio_task::AudioTask, video_task::VideoTask},
|
downloader::tasks::{audio_task::AudioTask, merge_task::MergeTask, video_task::VideoTask},
|
||||||
extensions::AppHandleExt,
|
extensions::AppHandleExt,
|
||||||
types::{
|
types::{
|
||||||
audio_quality::AudioQuality,
|
audio_quality::AudioQuality,
|
||||||
@@ -48,6 +48,7 @@ pub struct DownloadProgress {
|
|||||||
pub filename: String,
|
pub filename: String,
|
||||||
pub video_task: VideoTask,
|
pub video_task: VideoTask,
|
||||||
pub audio_task: AudioTask,
|
pub audio_task: AudioTask,
|
||||||
|
pub merge_task: MergeTask,
|
||||||
pub create_ts: u64,
|
pub create_ts: u64,
|
||||||
pub completed_ts: Option<u64>,
|
pub completed_ts: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -114,6 +115,7 @@ impl DownloadProgress {
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video,
|
video_task: tasks.video,
|
||||||
audio_task: tasks.audio,
|
audio_task: tasks.audio,
|
||||||
|
merge_task: tasks.merge,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -159,6 +161,7 @@ impl DownloadProgress {
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video,
|
video_task: tasks.video,
|
||||||
audio_task: tasks.audio,
|
audio_task: tasks.audio,
|
||||||
|
merge_task: tasks.merge,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -291,12 +294,15 @@ impl DownloadProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_completed(&self) -> bool {
|
pub fn is_completed(&self) -> bool {
|
||||||
self.video_task.is_completed() && self.audio_task.is_completed()
|
self.video_task.is_completed()
|
||||||
|
&& self.audio_task.is_completed()
|
||||||
|
&& self.merge_task.is_completed()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_uncompleted(&mut self) {
|
pub fn mark_uncompleted(&mut self) {
|
||||||
self.video_task.mark_uncompleted();
|
self.video_task.mark_uncompleted();
|
||||||
self.audio_task.mark_uncompleted();
|
self.audio_task.mark_uncompleted();
|
||||||
|
self.merge_task.completed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ids_string(&self) -> String {
|
pub fn get_ids_string(&self) -> String {
|
||||||
@@ -344,6 +350,7 @@ fn create_normal_progresses_for_single(
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video,
|
video_task: tasks.video,
|
||||||
audio_task: tasks.audio,
|
audio_task: tasks.audio,
|
||||||
|
merge_task: tasks.merge,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -378,6 +385,7 @@ fn create_normal_progresses_for_single(
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video,
|
video_task: tasks.video,
|
||||||
audio_task: tasks.audio,
|
audio_task: tasks.audio,
|
||||||
|
merge_task: tasks.merge,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -412,6 +420,7 @@ fn create_normal_progresses_for_single(
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video.clone(),
|
video_task: tasks.video.clone(),
|
||||||
audio_task: tasks.audio.clone(),
|
audio_task: tasks.audio.clone(),
|
||||||
|
merge_task: tasks.merge.clone(),
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -478,6 +487,7 @@ fn create_normal_progresses_for_season(
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video,
|
video_task: tasks.video,
|
||||||
audio_task: tasks.audio,
|
audio_task: tasks.audio,
|
||||||
|
merge_task: tasks.merge,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -512,6 +522,7 @@ fn create_normal_progresses_for_season(
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video,
|
video_task: tasks.video,
|
||||||
audio_task: tasks.audio,
|
audio_task: tasks.audio,
|
||||||
|
merge_task: tasks.merge,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -547,6 +558,7 @@ fn create_normal_progresses_for_season(
|
|||||||
filename: String::new(),
|
filename: String::new(),
|
||||||
video_task: tasks.video.clone(),
|
video_task: tasks.video.clone(),
|
||||||
audio_task: tasks.audio.clone(),
|
audio_task: tasks.audio.clone(),
|
||||||
|
merge_task: tasks.merge.clone(),
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -563,6 +575,7 @@ fn create_normal_progresses_for_season(
|
|||||||
struct Tasks {
|
struct Tasks {
|
||||||
video: VideoTask,
|
video: VideoTask,
|
||||||
audio: AudioTask,
|
audio: AudioTask,
|
||||||
|
merge: MergeTask,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
@@ -586,6 +599,15 @@ impl Tasks {
|
|||||||
completed: false,
|
completed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self { video, audio }
|
let merge = MergeTask {
|
||||||
|
selected: config.auto_merge,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
video,
|
||||||
|
audio,
|
||||||
|
merge,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,6 +296,13 @@ impl DownloadTask {
|
|||||||
tracing::debug!("{ids_string} `{filename}`音频下载完成");
|
tracing::debug!("{ids_string} `{filename}`音频下载完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !progress.merge_task.is_completed() {
|
||||||
|
self.merge_video_audio(&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())
|
||||||
@@ -530,6 +537,89 @@ impl DownloadTask {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn merge_video_audio(&self, progress: &DownloadProgress) -> anyhow::Result<()> {
|
||||||
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
|
||||||
|
let video_path = episode_dir.join(format!("{filename}.mp4"));
|
||||||
|
if !video_path.exists() {
|
||||||
|
self.update_progress(|p| p.merge_task.completed = true);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio_path = episode_dir.join(format!("{filename}.m4a"));
|
||||||
|
if !audio_path.exists() {
|
||||||
|
self.update_progress(|p| p.merge_task.completed = true);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_path = episode_dir.join(format!("{filename}-merged.mp4"));
|
||||||
|
|
||||||
|
let ffmpeg_program = std::env::current_exe()
|
||||||
|
.context("获取当前可执行文件路径失败")?
|
||||||
|
.parent()
|
||||||
|
.context("获取当前可执行文件所在目录失败")?
|
||||||
|
.join("com.lanyeeee.bilibili-video-downloader-ffmpeg");
|
||||||
|
|
||||||
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||||
|
let video_path_clone = video_path.clone();
|
||||||
|
let audio_path_clone = audio_path.clone();
|
||||||
|
let output_path_clone = output_path.clone();
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn_blocking(move || {
|
||||||
|
let mut command = std::process::Command::new(ffmpeg_program);
|
||||||
|
|
||||||
|
command
|
||||||
|
.arg("-i")
|
||||||
|
.arg(video_path_clone)
|
||||||
|
.arg("-i")
|
||||||
|
.arg(audio_path_clone)
|
||||||
|
.arg("-c")
|
||||||
|
.arg("copy")
|
||||||
|
.arg("-map")
|
||||||
|
.arg("0:v:0")
|
||||||
|
.arg("-map")
|
||||||
|
.arg("1:a:0")
|
||||||
|
.arg(output_path_clone)
|
||||||
|
.arg("-y");
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// 隐藏窗口
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
command.creation_flags(0x0800_0000);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = command.output();
|
||||||
|
|
||||||
|
let _ = tx.send(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = rx.await??;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let err = anyhow!(format!("STDOUT: {stdout}"))
|
||||||
|
.context(format!("STDERR: {stderr}"))
|
||||||
|
.context("原因可能是视频或音频文件损坏,建议[重来]试试");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::remove_file(&video_path)
|
||||||
|
.context(format!("删除视频文件`{}`失败", video_path.display()))?;
|
||||||
|
std::fs::remove_file(&audio_path)
|
||||||
|
.context(format!("删除音频文件`{}`失败", audio_path.display()))?;
|
||||||
|
std::fs::rename(&output_path, &video_path).context(format!(
|
||||||
|
"将`{}`重命名为`{}`失败",
|
||||||
|
output_path.display(),
|
||||||
|
video_path.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
self.update_progress(|p| p.merge_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,14 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
pub struct MergeTask {
|
||||||
|
pub selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MergeTask {
|
||||||
|
pub fn is_completed(&self) -> bool {
|
||||||
|
!self.selected || self.completed
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod audio_task;
|
pub mod audio_task;
|
||||||
|
pub mod merge_task;
|
||||||
pub mod video_task;
|
pub mod video_task;
|
||||||
|
|||||||
@@ -30,6 +30,9 @@
|
|||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"externalBin": [
|
||||||
|
"ffmpeg/com.lanyeeee.bilibili-video-downloader-ffmpeg"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+3
-2
@@ -185,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 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 = { 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 Config = { downloadDir: string; enableFileLogger: boolean; sessdata: string; preferVideoQuality: PreferVideoQuality; preferCodecType: PreferCodecType; preferAudioQuality: PreferAudioQuality; downloadVideo: boolean; downloadAudio: boolean; autoMerge: 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 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 }
|
||||||
@@ -202,7 +202,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; 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; 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 }
|
||||||
@@ -248,6 +248,7 @@ export type MediaInCheese = { start_with_sap: number; bandwidth: number; sar: st
|
|||||||
export type MediaInFav = { id: number; type: number; title: string; cover: string; intro: string; page: number; duration: number; upper: UpperInMedia; attr: number; cnt_info: CntInfoInMedia; link: string; ctime: number; pubtime: number; fav_time: number; bv_id: string; bvid: string; ugc: Ugc | null; media_list_link: string }
|
export type MediaInFav = { id: number; type: number; title: string; cover: string; intro: string; page: number; duration: number; upper: UpperInMedia; attr: number; cnt_info: CntInfoInMedia; link: string; ctime: number; pubtime: number; fav_time: number; bv_id: string; bvid: string; ugc: Ugc | null; media_list_link: string }
|
||||||
export type MediaInNormal = { id: number; start_with_sap: number; bandwidth: number; sar: string; codecs: string; base_url: string; backup_url: string[]; segment_base: SegmentBaseInNormal; mime_type: string; frame_rate: string; width: number; height: number; codecid: number }
|
export type MediaInNormal = { id: number; start_with_sap: number; bandwidth: number; sar: string; codecs: string; base_url: string; backup_url: string[]; segment_base: SegmentBaseInNormal; mime_type: string; frame_rate: string; width: number; height: number; codecid: number }
|
||||||
export type MediaInWatchLater = { aid: number; videos: number; tid: number; tname: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; state: number; duration: number; redirect_url: string | null; mission_id: number | null; rights: RightsInWatchLater; owner: OwnerInWatchLater; stat: StatInWatchLater; dynamic: string; dimension: DimensionInWatchLater; short_link_v2: string; up_from_v2: number | null; first_frame: string | null; pub_location: string | null; cover43: string; tidv2: number; tnamev2: string; pid_v2: number; pid_name_v2: string; page: PageInWatchLater; count: number; cid: number; progress: number; add_at: number; bvid: string; uri: string; enable_vt: number; view_text_1: string; card_type: number; left_icon_type: number; left_text: string; right_icon_type: number; right_text: string; arc_state: number; pgc_label: string; show_up: boolean; forbid_fav: boolean; forbid_sort: boolean; season_title: string; long_title: string; index_title: string; c_source: string; season_id: number | null }
|
export type MediaInWatchLater = { aid: number; videos: number; tid: number; tname: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; state: number; duration: number; redirect_url: string | null; mission_id: number | null; rights: RightsInWatchLater; owner: OwnerInWatchLater; stat: StatInWatchLater; dynamic: string; dimension: DimensionInWatchLater; short_link_v2: string; up_from_v2: number | null; first_frame: string | null; pub_location: string | null; cover43: string; tidv2: number; tnamev2: string; pid_v2: number; pid_name_v2: string; page: PageInWatchLater; count: number; cid: number; progress: number; add_at: number; bvid: string; uri: string; enable_vt: number; view_text_1: string; card_type: number; left_icon_type: number; left_text: string; right_icon_type: number; right_text: string; arc_state: number; pgc_label: string; show_up: boolean; forbid_fav: boolean; forbid_sort: boolean; season_title: string; long_title: string; index_title: string; c_source: string; season_id: number | null }
|
||||||
|
export type MergeTask = { selected: boolean; completed: boolean }
|
||||||
export type NewEp = { desc: string; id: number; is_new: number; title: string }
|
export type NewEp = { desc: string; id: number; is_new: number; title: string }
|
||||||
export type NewEpInSeason = { cover: string; id: number; index_show: string }
|
export type NewEpInSeason = { cover: string; id: number; index_show: string }
|
||||||
export type NormalInfo = { bvid: string; aid: number; videos: number; tid: number; tid_v2: number; tname: string; tname_v2: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; desc_v2: DescV2[] | null; state: number; duration: number; rights: Rights; owner: OwnerInNormal; stat: StatInNormal; argue_info: ArgueInfo; dynamic: string; cid: number; dimension: Dimension; teenage_mode: number; is_chargeable_season: boolean; is_story: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_upower_preview: boolean; enable_vt: number; vt_display: string; is_upower_exclusive_with_qa: boolean; no_cache: boolean; pages: PageInNormal[]; subtitle: SubtitleInNormal; staff: Staff[] | null; ugc_season: UgcSeason | null; is_season_display: boolean; user_garb: UserGarb; honor_reply: HonorReply; like_icon: string; need_jump_bv: boolean; disable_show_up_info: boolean; is_story_play: number; is_view_self: boolean }
|
export type NormalInfo = { bvid: string; aid: number; videos: number; tid: number; tid_v2: number; tname: string; tname_v2: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; desc_v2: DescV2[] | null; state: number; duration: number; rights: Rights; owner: OwnerInNormal; stat: StatInNormal; argue_info: ArgueInfo; dynamic: string; cid: number; dimension: Dimension; teenage_mode: number; is_chargeable_season: boolean; is_story: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_upower_preview: boolean; enable_vt: number; vt_display: string; is_upower_exclusive_with_qa: boolean; no_cache: boolean; pages: PageInNormal[]; subtitle: SubtitleInNormal; staff: Staff[] | null; ugc_season: UgcSeason | null; is_season_display: boolean; user_garb: UserGarb; honor_reply: HonorReply; like_icon: string; need_jump_bv: boolean; disable_show_up_info: boolean; is_story_play: number; is_view_self: boolean }
|
||||||
|
|||||||
Reference in New Issue
Block a user