mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 17:16:52 +08:00
feat: 后端支持nfo元数据下载
This commit is contained in:
@@ -24,7 +24,7 @@ use crate::{
|
|||||||
get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams,
|
get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams,
|
||||||
get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams,
|
get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams,
|
||||||
normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo,
|
normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo,
|
||||||
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, subtitle::Subtitle,
|
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, subtitle::Subtitle, tags::Tags,
|
||||||
user_info::UserInfo, watch_later_info::WatchLaterInfo,
|
user_info::UserInfo, watch_later_info::WatchLaterInfo,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -715,6 +715,41 @@ impl BiliClient {
|
|||||||
Ok((bytes, ext.to_string()))
|
Ok((bytes, ext.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_tags(&self, aid: i64) -> anyhow::Result<Tags> {
|
||||||
|
// 发送获取普通视频标签的请求
|
||||||
|
let params = json!({"aid": aid});
|
||||||
|
let request = self
|
||||||
|
.api_client
|
||||||
|
.read()
|
||||||
|
.get("https://api.bilibili.com/x/web-interface/view/detail/tag")
|
||||||
|
.query(¶ms)
|
||||||
|
.header("cookie", self.get_cookie());
|
||||||
|
let http_resp = request.send().await?;
|
||||||
|
// 检查http响应状态码
|
||||||
|
let status = http_resp.status();
|
||||||
|
let body = http_resp.text().await?;
|
||||||
|
if status != StatusCode::OK {
|
||||||
|
return Err(anyhow!("预料之外的状态码({status}): {body}"));
|
||||||
|
}
|
||||||
|
// 尝试将body解析为BiliResp
|
||||||
|
let bili_resp: BiliResp =
|
||||||
|
serde_json::from_str(&body).context(format!("将body解析为BiliResp失败: {body}"))?;
|
||||||
|
// 检查BiliResp的code字段
|
||||||
|
if bili_resp.code != 0 {
|
||||||
|
return Err(anyhow!("预料之外的code: {bili_resp:?}"));
|
||||||
|
}
|
||||||
|
// 检查BiliResp的data是否存在
|
||||||
|
let Some(data) = bili_resp.data else {
|
||||||
|
return Err(anyhow!("BiliResp中不存在data字段: {bili_resp:?}"));
|
||||||
|
};
|
||||||
|
// 尝试将data解析为Tags
|
||||||
|
let data_str = data.to_string();
|
||||||
|
let tags: Tags =
|
||||||
|
serde_json::from_str(&data_str).context(format!("将data解析为Tags失败: {data_str}"))?;
|
||||||
|
|
||||||
|
Ok(tags)
|
||||||
|
}
|
||||||
|
|
||||||
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}")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub struct Config {
|
|||||||
pub download_json_danmaku: bool,
|
pub download_json_danmaku: bool,
|
||||||
pub download_subtitle: bool,
|
pub download_subtitle: bool,
|
||||||
pub download_cover: bool,
|
pub download_cover: bool,
|
||||||
|
pub download_nfo: 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,
|
||||||
@@ -104,6 +105,7 @@ impl Config {
|
|||||||
download_json_danmaku: true,
|
download_json_danmaku: true,
|
||||||
download_subtitle: true,
|
download_subtitle: true,
|
||||||
download_cover: true,
|
download_cover: true,
|
||||||
|
download_nfo: 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(),
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use crate::{
|
|||||||
config::Config,
|
config::Config,
|
||||||
downloader::tasks::{
|
downloader::tasks::{
|
||||||
audio_task::AudioTask, cover_task::CoverTask, danmaku_task::DanmakuTask,
|
audio_task::AudioTask, cover_task::CoverTask, danmaku_task::DanmakuTask,
|
||||||
merge_task::MergeTask, subtitle_task::SubtitleTask, video_task::VideoTask,
|
merge_task::MergeTask, nfo_task::NfoTask, subtitle_task::SubtitleTask,
|
||||||
|
video_task::VideoTask,
|
||||||
},
|
},
|
||||||
extensions::AppHandleExt,
|
extensions::AppHandleExt,
|
||||||
types::{
|
types::{
|
||||||
@@ -55,6 +56,7 @@ pub struct DownloadProgress {
|
|||||||
pub subtitle_task: SubtitleTask,
|
pub subtitle_task: SubtitleTask,
|
||||||
pub danmaku_task: DanmakuTask,
|
pub danmaku_task: DanmakuTask,
|
||||||
pub cover_task: CoverTask,
|
pub cover_task: CoverTask,
|
||||||
|
pub nfo_task: NfoTask,
|
||||||
pub create_ts: u64,
|
pub create_ts: u64,
|
||||||
pub completed_ts: Option<u64>,
|
pub completed_ts: Option<u64>,
|
||||||
}
|
}
|
||||||
@@ -125,6 +127,7 @@ impl DownloadProgress {
|
|||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
cover_task: tasks.cover,
|
cover_task: tasks.cover,
|
||||||
|
nfo_task: tasks.nfo,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -174,6 +177,7 @@ impl DownloadProgress {
|
|||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
cover_task: tasks.cover,
|
cover_task: tasks.cover,
|
||||||
|
nfo_task: tasks.nfo,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -312,6 +316,7 @@ impl DownloadProgress {
|
|||||||
&& 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()
|
&& self.cover_task.is_completed()
|
||||||
|
&& self.nfo_task.is_completed()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_uncompleted(&mut self) {
|
pub fn mark_uncompleted(&mut self) {
|
||||||
@@ -321,6 +326,7 @@ impl DownloadProgress {
|
|||||||
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;
|
self.cover_task.completed = false;
|
||||||
|
self.nfo_task.completed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_ids_string(&self) -> String {
|
pub fn get_ids_string(&self) -> String {
|
||||||
@@ -372,6 +378,7 @@ fn create_normal_progresses_for_single(
|
|||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
cover_task: tasks.cover,
|
cover_task: tasks.cover,
|
||||||
|
nfo_task: tasks.nfo,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -410,6 +417,7 @@ fn create_normal_progresses_for_single(
|
|||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
cover_task: tasks.cover,
|
cover_task: tasks.cover,
|
||||||
|
nfo_task: tasks.nfo,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -448,6 +456,7 @@ fn create_normal_progresses_for_single(
|
|||||||
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(),
|
cover_task: tasks.cover.clone(),
|
||||||
|
nfo_task: tasks.nfo.clone(),
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -518,6 +527,7 @@ fn create_normal_progresses_for_season(
|
|||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
cover_task: tasks.cover,
|
cover_task: tasks.cover,
|
||||||
|
nfo_task: tasks.nfo,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -556,6 +566,7 @@ fn create_normal_progresses_for_season(
|
|||||||
danmaku_task: tasks.danmaku,
|
danmaku_task: tasks.danmaku,
|
||||||
subtitle_task: tasks.subtitle,
|
subtitle_task: tasks.subtitle,
|
||||||
cover_task: tasks.cover,
|
cover_task: tasks.cover,
|
||||||
|
nfo_task: tasks.nfo,
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -595,6 +606,7 @@ fn create_normal_progresses_for_season(
|
|||||||
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(),
|
cover_task: tasks.cover.clone(),
|
||||||
|
nfo_task: tasks.nfo.clone(),
|
||||||
create_ts,
|
create_ts,
|
||||||
completed_ts: None,
|
completed_ts: None,
|
||||||
};
|
};
|
||||||
@@ -615,6 +627,7 @@ struct Tasks {
|
|||||||
danmaku: DanmakuTask,
|
danmaku: DanmakuTask,
|
||||||
subtitle: SubtitleTask,
|
subtitle: SubtitleTask,
|
||||||
cover: CoverTask,
|
cover: CoverTask,
|
||||||
|
nfo: NfoTask,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tasks {
|
impl Tasks {
|
||||||
@@ -661,6 +674,11 @@ impl Tasks {
|
|||||||
completed: false,
|
completed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let nfo = NfoTask {
|
||||||
|
selected: config.download_nfo,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
video,
|
video,
|
||||||
audio,
|
audio,
|
||||||
@@ -668,6 +686,7 @@ impl Tasks {
|
|||||||
danmaku,
|
danmaku,
|
||||||
subtitle,
|
subtitle,
|
||||||
cover,
|
cover,
|
||||||
|
nfo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,17 @@ use tokio::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
bili_client::BiliClient,
|
||||||
danmaku_xml_to_ass::xml_to_ass,
|
danmaku_xml_to_ass::xml_to_ass,
|
||||||
|
downloader::episode_type::EpisodeType,
|
||||||
events::DownloadEvent,
|
events::DownloadEvent,
|
||||||
extensions::{AnyhowErrorToStringChain, AppHandleExt},
|
extensions::{AnyhowErrorToStringChain, AppHandleExt},
|
||||||
types::create_download_task_params::CreateDownloadTaskParams,
|
types::{
|
||||||
|
bangumi_info::BangumiInfo, cheese_info::CheeseInfo,
|
||||||
|
create_download_task_params::CreateDownloadTaskParams,
|
||||||
|
get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams,
|
||||||
|
get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo,
|
||||||
|
},
|
||||||
utils::{self, ToXml},
|
utils::{self, ToXml},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,6 +332,15 @@ impl DownloadTask {
|
|||||||
tracing::debug!("{ids_string} `{filename}`封面下载完成");
|
tracing::debug!("{ids_string} `{filename}`封面下载完成");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut episode_info = None;
|
||||||
|
|
||||||
|
if !progress.nfo_task.is_completed() {
|
||||||
|
self.download_nfo(&progress, &mut episode_info)
|
||||||
|
.await
|
||||||
|
.context(format!("{ids_string} `{filename}`下载NFO失败"))?;
|
||||||
|
tracing::debug!("{ids_string} `{filename}`NFO下载完成");
|
||||||
|
}
|
||||||
|
|
||||||
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())
|
||||||
@@ -746,6 +762,116 @@ impl DownloadTask {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn download_nfo(
|
||||||
|
&self,
|
||||||
|
progress: &DownloadProgress,
|
||||||
|
episode_info: &mut Option<EpisodeInfo>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (episode_dir, filename) = (&progress.episode_dir, &progress.filename);
|
||||||
|
let (aid, ep_id, episode_type) = (progress.aid, progress.ep_id, progress.episode_type);
|
||||||
|
|
||||||
|
let bili_client = self.app.get_bili_client();
|
||||||
|
|
||||||
|
let episode_info = episode_info
|
||||||
|
.get_or_init(&bili_client, aid, ep_id, episode_type)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match episode_info {
|
||||||
|
EpisodeInfo::Normal(info) => {
|
||||||
|
let tags = bili_client
|
||||||
|
.get_tags(aid)
|
||||||
|
.await
|
||||||
|
.context("获取视频标签失败")?;
|
||||||
|
let movie_nfo = info
|
||||||
|
.to_movie_nfo(tags)
|
||||||
|
.context("将普通视频信息转换为movie NFO失败")?;
|
||||||
|
let nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
std::fs::write(&nfo_path, movie_nfo)
|
||||||
|
.context(format!("保存普通视频NFO到`{}`失败", nfo_path.display()))?;
|
||||||
|
|
||||||
|
if let Some(ugc_season) = &info.ugc_season {
|
||||||
|
let collection_cover = &ugc_season.cover;
|
||||||
|
let (cover_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(collection_cover)
|
||||||
|
.await
|
||||||
|
.context("获取普通视频合集封面失败")?;
|
||||||
|
let cover_path = episode_dir.join(format!("poster.{ext}"));
|
||||||
|
std::fs::write(&cover_path, cover_data).context(format!(
|
||||||
|
"保存普通视频合集封面到`{}`失败",
|
||||||
|
cover_path.display()
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EpisodeInfo::Bangumi(info, ep_id) => {
|
||||||
|
let tvshow_nfo = info
|
||||||
|
.to_tvshow_nfo()
|
||||||
|
.context("将番剧信息转换为tvshow NFO失败")?;
|
||||||
|
let tvshow_nfo_path = episode_dir.join("tvshow.nfo");
|
||||||
|
std::fs::write(&tvshow_nfo_path, tvshow_nfo)
|
||||||
|
.context(format!("保存番剧NFO到`{}`失败", tvshow_nfo_path.display()))?;
|
||||||
|
|
||||||
|
let episode_details_nfo = info
|
||||||
|
.to_episode_details_nfo(*ep_id)
|
||||||
|
.context("将番剧信息转换为episodedetail NFO失败")?;
|
||||||
|
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
std::fs::write(&episode_details_nfo_path, episode_details_nfo).context(format!(
|
||||||
|
"保存番剧NFO到`{}`失败",
|
||||||
|
episode_details_nfo_path.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let poster_url = &info.cover;
|
||||||
|
let (poster_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(poster_url)
|
||||||
|
.await
|
||||||
|
.context("获取番剧封面失败")?;
|
||||||
|
let poster_path = episode_dir.join(format!("poster.{ext}"));
|
||||||
|
std::fs::write(&poster_path, poster_data)
|
||||||
|
.context(format!("保存番剧封面到`{}`失败", poster_path.display()))?;
|
||||||
|
|
||||||
|
let fanart_url = &info.bkg_cover;
|
||||||
|
if !fanart_url.is_empty() {
|
||||||
|
let (fanart_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(fanart_url)
|
||||||
|
.await
|
||||||
|
.context("获取番剧封面失败")?;
|
||||||
|
let fanart_path = episode_dir.join(format!("fanart.{ext}"));
|
||||||
|
std::fs::write(&fanart_path, fanart_data)
|
||||||
|
.context(format!("保存番剧封面到`{}`失败", fanart_path.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EpisodeInfo::Cheese(info, ep_id) => {
|
||||||
|
let tvshow_nfo = info
|
||||||
|
.to_tvshow_nfo()
|
||||||
|
.context("将课程信息转换为tvshow NFO失败")?;
|
||||||
|
let tvshow_nfo_path = episode_dir.join("tvshow.nfo");
|
||||||
|
std::fs::write(&tvshow_nfo_path, tvshow_nfo)
|
||||||
|
.context(format!("保存课程NFO到`{}`失败", tvshow_nfo_path.display()))?;
|
||||||
|
|
||||||
|
let episode_details_nfo = info
|
||||||
|
.to_episode_details_nfo(*ep_id)
|
||||||
|
.context("将课程信息转换为episodedetail NFO失败")?;
|
||||||
|
let episode_details_nfo_path = episode_dir.join(format!("{filename}.nfo"));
|
||||||
|
std::fs::write(&episode_details_nfo_path, episode_details_nfo).context(format!(
|
||||||
|
"保存课程NFO到`{}`失败",
|
||||||
|
episode_details_nfo_path.display()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let poster_url = &info.cover;
|
||||||
|
let (poster_data, ext) = bili_client
|
||||||
|
.get_cover_data_and_ext(poster_url)
|
||||||
|
.await
|
||||||
|
.context("获取课程封面失败")?;
|
||||||
|
let poster_path = episode_dir.join(format!("poster.{ext}"));
|
||||||
|
std::fs::write(&poster_path, poster_data)
|
||||||
|
.context(format!("保存课程封面到`{}`失败", poster_path.display()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_progress(|p| p.nfo_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;
|
||||||
@@ -978,3 +1104,62 @@ impl DownloadChunkTask {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum EpisodeInfo {
|
||||||
|
Normal(NormalInfo),
|
||||||
|
Bangumi(BangumiInfo, i64),
|
||||||
|
Cheese(CheeseInfo, i64),
|
||||||
|
}
|
||||||
|
|
||||||
|
trait GetOrInitEpisodeInfo {
|
||||||
|
async fn get_or_init<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
bili_client: &BiliClient,
|
||||||
|
aid: i64,
|
||||||
|
ep_id: Option<i64>,
|
||||||
|
episode_type: EpisodeType,
|
||||||
|
) -> anyhow::Result<&'a mut EpisodeInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetOrInitEpisodeInfo for Option<EpisodeInfo> {
|
||||||
|
async fn get_or_init<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
bili_client: &BiliClient,
|
||||||
|
aid: i64,
|
||||||
|
ep_id: Option<i64>,
|
||||||
|
episode_type: EpisodeType,
|
||||||
|
) -> anyhow::Result<&'a mut EpisodeInfo> {
|
||||||
|
if let Some(info) = self {
|
||||||
|
return Ok(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_info = match episode_type {
|
||||||
|
EpisodeType::Normal => {
|
||||||
|
let info = bili_client
|
||||||
|
.get_normal_info(GetNormalInfoParams::Aid(aid))
|
||||||
|
.await
|
||||||
|
.context("获取普通视频信息失败")?;
|
||||||
|
EpisodeInfo::Normal(info)
|
||||||
|
}
|
||||||
|
EpisodeType::Bangumi => {
|
||||||
|
let ep_id = ep_id.context("ep_id为None")?;
|
||||||
|
let info = bili_client
|
||||||
|
.get_bangumi_info(GetBangumiInfoParams::EpId(ep_id))
|
||||||
|
.await
|
||||||
|
.context("获取番剧信息失败")?;
|
||||||
|
EpisodeInfo::Bangumi(info, ep_id)
|
||||||
|
}
|
||||||
|
EpisodeType::Cheese => {
|
||||||
|
let ep_id = ep_id.context("ep_id为None")?;
|
||||||
|
let info = bili_client
|
||||||
|
.get_cheese_info(GetCheeseInfoParams::EpId(ep_id))
|
||||||
|
.await
|
||||||
|
.context("获取课程信息失败")?;
|
||||||
|
EpisodeInfo::Cheese(info, ep_id)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(self.insert(new_info))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ pub mod audio_task;
|
|||||||
pub mod cover_task;
|
pub mod cover_task;
|
||||||
pub mod danmaku_task;
|
pub mod danmaku_task;
|
||||||
pub mod merge_task;
|
pub mod merge_task;
|
||||||
|
pub mod nfo_task;
|
||||||
pub mod subtitle_task;
|
pub mod subtitle_task;
|
||||||
pub mod video_task;
|
pub mod video_task;
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use chrono::{DateTime, Datelike, NaiveDateTime};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use yaserde::{YaDeserialize, YaSerialize};
|
||||||
|
|
||||||
|
use crate::types::{
|
||||||
|
bangumi_info::BangumiInfo, cheese_info::CheeseInfo, normal_info::NormalInfo, tags::Tags,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
pub struct NfoTask {
|
||||||
|
pub selected: bool,
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NfoTask {
|
||||||
|
pub fn is_completed(&self) -> bool {
|
||||||
|
!self.selected || self.completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(YaSerialize, YaDeserialize)]
|
||||||
|
#[yaserde(rename = "movie")]
|
||||||
|
struct Movie {
|
||||||
|
title: String,
|
||||||
|
plot: String,
|
||||||
|
tagline: Option<String>,
|
||||||
|
runtime: u64,
|
||||||
|
premiered: String,
|
||||||
|
year: i32,
|
||||||
|
studio: Vec<String>,
|
||||||
|
genre: Vec<String>,
|
||||||
|
tag: Vec<String>,
|
||||||
|
country: Vec<String>,
|
||||||
|
set: Option<Set>,
|
||||||
|
director: Vec<String>,
|
||||||
|
actor: Vec<Actor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(YaSerialize, YaDeserialize)]
|
||||||
|
#[yaserde(rename = "set")]
|
||||||
|
struct Set {
|
||||||
|
name: String,
|
||||||
|
overview: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(YaSerialize, YaDeserialize)]
|
||||||
|
#[yaserde(rename = "actor")]
|
||||||
|
struct Actor {
|
||||||
|
name: String,
|
||||||
|
role: String,
|
||||||
|
order: i64,
|
||||||
|
thumb: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(YaSerialize, YaDeserialize)]
|
||||||
|
#[yaserde(rename = "tvshow")]
|
||||||
|
struct Tvshow {
|
||||||
|
title: String,
|
||||||
|
plot: String,
|
||||||
|
tagline: Option<String>,
|
||||||
|
premiered: String,
|
||||||
|
year: i32,
|
||||||
|
studio: Vec<String>,
|
||||||
|
status: String,
|
||||||
|
genre: Vec<String>,
|
||||||
|
tag: Vec<String>,
|
||||||
|
country: Vec<String>,
|
||||||
|
director: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(YaSerialize, YaDeserialize)]
|
||||||
|
#[yaserde(rename = "episodedetails")]
|
||||||
|
struct EpisodeDetails {
|
||||||
|
title: String,
|
||||||
|
plot: String,
|
||||||
|
tagline: Option<String>,
|
||||||
|
runtime: u64,
|
||||||
|
premiered: String,
|
||||||
|
year: i32,
|
||||||
|
episode: i64,
|
||||||
|
studio: Vec<String>,
|
||||||
|
genre: Vec<String>,
|
||||||
|
tag: Vec<String>,
|
||||||
|
country: Vec<String>,
|
||||||
|
director: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NormalInfo {
|
||||||
|
pub fn to_movie_nfo(&self, tags: Tags) -> anyhow::Result<String> {
|
||||||
|
let genre = vec![
|
||||||
|
"Bilibili视频".to_string(),
|
||||||
|
self.tname.clone(),
|
||||||
|
self.tname_v2.clone(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let tag: Vec<String> = tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| t.tag_name)
|
||||||
|
.filter(|tag_name| !tag_name.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let ts = self.pubdate;
|
||||||
|
let date_time = DateTime::from_timestamp(ts, 0)
|
||||||
|
.context(format!("将视频发布时间戳转换为日期时间失败: {ts}"))?
|
||||||
|
.with_timezone(&chrono::Local);
|
||||||
|
|
||||||
|
let set = self.ugc_season.as_ref().map(|ugc_season| Set {
|
||||||
|
name: ugc_season.title.clone(),
|
||||||
|
overview: ugc_season.intro.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let actor = self.staff.as_ref().map_or(Vec::new(), |staff| {
|
||||||
|
staff
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(order, staff)| Actor {
|
||||||
|
name: staff.name.clone(),
|
||||||
|
role: staff.title.clone(),
|
||||||
|
#[allow(clippy::cast_possible_wrap)]
|
||||||
|
order: order as i64,
|
||||||
|
thumb: staff.face.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let movie = Movie {
|
||||||
|
title: self.title.clone(),
|
||||||
|
plot: self.desc.clone(),
|
||||||
|
tagline: None,
|
||||||
|
runtime: self.duration / 60,
|
||||||
|
premiered: date_time.format("%Y-%m-%d").to_string(),
|
||||||
|
year: date_time.year(),
|
||||||
|
studio: vec!["Bilibili".to_string()],
|
||||||
|
genre,
|
||||||
|
tag,
|
||||||
|
country: Vec::new(),
|
||||||
|
set,
|
||||||
|
director: vec![self.owner.name.clone()],
|
||||||
|
actor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cfg = yaserde::ser::Config {
|
||||||
|
perform_indent: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nfo = yaserde::ser::to_string_with_config(&movie, &cfg).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
Ok(nfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BangumiInfo {
|
||||||
|
pub fn to_tvshow_nfo(&self) -> anyhow::Result<String> {
|
||||||
|
let time_str = &self.publish.pub_time;
|
||||||
|
let date_time = NaiveDateTime::parse_from_str(time_str, "%Y-%m-%d %H:%M:%S").context(
|
||||||
|
format!("将番剧发布时间字符串转换为日期时间失败: {time_str}"),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let status = match self.publish.is_finish {
|
||||||
|
0 => "Continuing".to_string(),
|
||||||
|
_ => "Ended".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tv_show = Tvshow {
|
||||||
|
title: self.title.clone(),
|
||||||
|
plot: self.evaluate.clone(),
|
||||||
|
tagline: Some(self.share_sub_title.clone()),
|
||||||
|
premiered: date_time.format("%Y-%m-%d").to_string(),
|
||||||
|
year: date_time.year(),
|
||||||
|
studio: vec!["Bilibili".to_string()],
|
||||||
|
status,
|
||||||
|
genre: self.get_genre(),
|
||||||
|
tag: Vec::new(),
|
||||||
|
country: self.get_country(),
|
||||||
|
director: self.get_director(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cfg = yaserde::ser::Config {
|
||||||
|
perform_indent: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nfo = yaserde::ser::to_string_with_config(&tv_show, &cfg).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
Ok(nfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_episode_details_nfo(&self, ep_id: i64) -> anyhow::Result<String> {
|
||||||
|
let (episode, episode_order) = self.get_episode_with_order(ep_id)?;
|
||||||
|
|
||||||
|
let ts = episode.pub_time;
|
||||||
|
let date_time = DateTime::from_timestamp(ts, 0)
|
||||||
|
.context(format!("将番剧发布时间戳转换为日期时间失败: {ts}"))?
|
||||||
|
.with_timezone(&chrono::Local);
|
||||||
|
|
||||||
|
let title = episode
|
||||||
|
.show_title
|
||||||
|
.clone()
|
||||||
|
.context("episode.show_title为None")?;
|
||||||
|
|
||||||
|
let plot = episode
|
||||||
|
.share_copy
|
||||||
|
.clone()
|
||||||
|
.context("episode.share_copy为None")?;
|
||||||
|
|
||||||
|
let duration = episode.duration.context("episode.duration为None")?;
|
||||||
|
|
||||||
|
let episode_details = EpisodeDetails {
|
||||||
|
title,
|
||||||
|
plot,
|
||||||
|
tagline: None,
|
||||||
|
runtime: duration / 1000 / 60,
|
||||||
|
premiered: date_time.format("%Y-%m-%d").to_string(),
|
||||||
|
year: date_time.year(),
|
||||||
|
episode: episode_order,
|
||||||
|
studio: vec!["Bilibili".to_string()],
|
||||||
|
genre: self.get_genre(),
|
||||||
|
tag: Vec::new(),
|
||||||
|
country: self.get_country(),
|
||||||
|
director: self.get_director(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cfg = yaserde::ser::Config {
|
||||||
|
perform_indent: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nfo =
|
||||||
|
yaserde::ser::to_string_with_config(&episode_details, &cfg).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
Ok(nfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_director(&self) -> Vec<String> {
|
||||||
|
if let Some(up_info) = &self.up_info {
|
||||||
|
vec![up_info.uname.clone()]
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_country(&self) -> Vec<String> {
|
||||||
|
self.areas
|
||||||
|
.iter()
|
||||||
|
.filter(|area| !area.name.is_empty())
|
||||||
|
.map(|area| area.name.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_genre(&self) -> Vec<String> {
|
||||||
|
let type_name = match self.type_field {
|
||||||
|
1 => "番剧",
|
||||||
|
2 => "电影",
|
||||||
|
3 => "纪录片",
|
||||||
|
4 => "国创",
|
||||||
|
5 => "电视剧",
|
||||||
|
6 => "漫画",
|
||||||
|
7 => "综艺",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut genre = Vec::new();
|
||||||
|
if !type_name.is_empty() {
|
||||||
|
genre.push(format!("Bilibili{type_name}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for style in &self.styles {
|
||||||
|
if !style.is_empty() {
|
||||||
|
genre.push(style.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
genre
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CheeseInfo {
|
||||||
|
pub fn to_tvshow_nfo(&self) -> anyhow::Result<String> {
|
||||||
|
let episode = self.episodes.first().context("episodes列表为空")?;
|
||||||
|
let ts = episode.release_date;
|
||||||
|
let date_time = DateTime::from_timestamp(ts, 0)
|
||||||
|
.context(format!("将课程的发布时间戳转换为日期时间失败: {ts}"))?
|
||||||
|
.with_timezone(&chrono::Local);
|
||||||
|
|
||||||
|
let status = match self.release_status.as_str() {
|
||||||
|
"已完结" => "Ended".to_string(),
|
||||||
|
_ => "Continuing".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let tv_show = Tvshow {
|
||||||
|
title: self.title.clone(),
|
||||||
|
plot: self.subtitle.clone(),
|
||||||
|
tagline: None,
|
||||||
|
premiered: date_time.format("%Y-%m-%d").to_string(),
|
||||||
|
year: date_time.year(),
|
||||||
|
studio: vec!["Bilibili".to_string()],
|
||||||
|
status,
|
||||||
|
genre: vec!["Bilibili课程".to_string()],
|
||||||
|
tag: Vec::new(),
|
||||||
|
country: Vec::new(),
|
||||||
|
director: vec![self.up_info.uname.clone()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let cfg = yaserde::ser::Config {
|
||||||
|
perform_indent: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nfo = yaserde::ser::to_string_with_config(&tv_show, &cfg).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
Ok(nfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_episode_details_nfo(&self, ep_id: i64) -> anyhow::Result<String> {
|
||||||
|
let episode = self
|
||||||
|
.episodes
|
||||||
|
.iter()
|
||||||
|
.find(|ep| ep.id == ep_id)
|
||||||
|
.context(format!("找不到ep_id为`{ep_id}`的课程"))?;
|
||||||
|
|
||||||
|
let ts = episode.release_date;
|
||||||
|
let date_time = DateTime::from_timestamp(ts, 0)
|
||||||
|
.context(format!("将课程发布时间戳转换为日期时间失败: {ts}"))?
|
||||||
|
.with_timezone(&chrono::Local);
|
||||||
|
|
||||||
|
let episode_details = EpisodeDetails {
|
||||||
|
title: episode.title.clone(),
|
||||||
|
plot: episode.subtitle.clone(),
|
||||||
|
tagline: None,
|
||||||
|
runtime: episode.duration / 60,
|
||||||
|
premiered: date_time.format("%Y-%m-%d").to_string(),
|
||||||
|
year: date_time.year(),
|
||||||
|
episode: episode.index,
|
||||||
|
studio: vec!["Bilibili".to_string()],
|
||||||
|
genre: vec!["Bilibili课程".to_string()],
|
||||||
|
tag: Vec::new(),
|
||||||
|
country: Vec::new(),
|
||||||
|
director: vec![self.up_info.uname.clone()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let cfg = yaserde::ser::Config {
|
||||||
|
perform_indent: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let nfo =
|
||||||
|
yaserde::ser::to_string_with_config(&episode_details, &cfg).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
Ok(nfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ pub mod player_info;
|
|||||||
pub mod qrcode_data;
|
pub mod qrcode_data;
|
||||||
pub mod qrcode_status;
|
pub mod qrcode_status;
|
||||||
pub mod subtitle;
|
pub mod subtitle;
|
||||||
|
pub mod tags;
|
||||||
pub mod user_info;
|
pub mod user_info;
|
||||||
pub mod video_quality;
|
pub mod video_quality;
|
||||||
pub mod watch_later_info;
|
pub mod watch_later_info;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
|
||||||
|
pub type Tags = Vec<Tag>;
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[allow(clippy::struct_field_names)]
|
||||||
|
pub struct Tag {
|
||||||
|
pub tag_id: i64,
|
||||||
|
pub tag_name: String,
|
||||||
|
pub music_id: String,
|
||||||
|
pub tag_type: String,
|
||||||
|
pub jump_url: String,
|
||||||
|
}
|
||||||
+3
-2
@@ -240,7 +240,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 = { 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 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; download_nfo: 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 }
|
||||||
@@ -259,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; cover_task: CoverTask; 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; nfo_task: NfoTask; 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 }
|
||||||
@@ -308,6 +308,7 @@ export type MediaInWatchLater = { aid: number; videos: number; tid: number; tnam
|
|||||||
export type MergeTask = { selected: boolean; completed: boolean }
|
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 NfoTask = { selected: boolean; completed: 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 }
|
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 NormalMediaUrl = { from: string; result: string; message: string; quality: number; format: string; timelength: number; accept_format: string; accept_description: string[]; accept_quality: number[]; video_codecid: number; seek_param: string; seek_type: string; dash: DashInNormal; support_formats: SupportFormatInNormal[]; last_play_time: number; last_play_cid: number; play_conf: PlayConf }
|
export type NormalMediaUrl = { from: string; result: string; message: string; quality: number; format: string; timelength: number; accept_format: string; accept_description: string[]; accept_quality: number[]; video_codecid: number; seek_param: string; seek_type: string; dash: DashInNormal; support_formats: SupportFormatInNormal[]; last_play_time: number; last_play_cid: number; play_conf: PlayConf }
|
||||||
export type Official = { role: number; title: string; desc: string; type: number }
|
export type Official = { role: number; title: string; desc: string; type: number }
|
||||||
|
|||||||
Reference in New Issue
Block a user