feat: 后端支持nfo元数据下载

This commit is contained in:
lanyeeee
2025-07-28 05:22:33 +08:00
parent 999c48cd5e
commit 72d8c32617
9 changed files with 617 additions and 5 deletions
+36 -1
View File
@@ -24,7 +24,7 @@ use crate::{
get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams,
get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams,
normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo,
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, subtitle::Subtitle,
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, subtitle::Subtitle, tags::Tags,
user_info::UserInfo, watch_later_info::WatchLaterInfo,
},
};
@@ -715,6 +715,41 @@ impl BiliClient {
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(&params)
.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 {
let sessdata = self.app.get_config().read().sessdata.clone();
format!("SESSDATA={sessdata}")
+2
View File
@@ -25,6 +25,7 @@ pub struct Config {
pub download_json_danmaku: bool,
pub download_subtitle: bool,
pub download_cover: bool,
pub download_nfo: bool,
pub dir_fmt: String,
pub dir_fmt_for_part: String,
pub time_fmt: String,
@@ -104,6 +105,7 @@ impl Config {
download_json_danmaku: true,
download_subtitle: true,
download_cover: true,
download_nfo: 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(),
+20 -1
View File
@@ -13,7 +13,8 @@ use crate::{
config::Config,
downloader::tasks::{
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,
types::{
@@ -55,6 +56,7 @@ pub struct DownloadProgress {
pub subtitle_task: SubtitleTask,
pub danmaku_task: DanmakuTask,
pub cover_task: CoverTask,
pub nfo_task: NfoTask,
pub create_ts: u64,
pub completed_ts: Option<u64>,
}
@@ -125,6 +127,7 @@ impl DownloadProgress {
danmaku_task: tasks.danmaku,
subtitle_task: tasks.subtitle,
cover_task: tasks.cover,
nfo_task: tasks.nfo,
create_ts,
completed_ts: None,
};
@@ -174,6 +177,7 @@ impl DownloadProgress {
danmaku_task: tasks.danmaku,
subtitle_task: tasks.subtitle,
cover_task: tasks.cover,
nfo_task: tasks.nfo,
create_ts,
completed_ts: None,
};
@@ -312,6 +316,7 @@ impl DownloadProgress {
&& self.danmaku_task.is_completed()
&& self.subtitle_task.is_completed()
&& self.cover_task.is_completed()
&& self.nfo_task.is_completed()
}
pub fn mark_uncompleted(&mut self) {
@@ -321,6 +326,7 @@ impl DownloadProgress {
self.danmaku_task.completed = false;
self.subtitle_task.completed = false;
self.cover_task.completed = false;
self.nfo_task.completed = false;
}
pub fn get_ids_string(&self) -> String {
@@ -372,6 +378,7 @@ fn create_normal_progresses_for_single(
danmaku_task: tasks.danmaku,
subtitle_task: tasks.subtitle,
cover_task: tasks.cover,
nfo_task: tasks.nfo,
create_ts,
completed_ts: None,
};
@@ -410,6 +417,7 @@ fn create_normal_progresses_for_single(
danmaku_task: tasks.danmaku,
subtitle_task: tasks.subtitle,
cover_task: tasks.cover,
nfo_task: tasks.nfo,
create_ts,
completed_ts: None,
};
@@ -448,6 +456,7 @@ fn create_normal_progresses_for_single(
danmaku_task: tasks.danmaku.clone(),
subtitle_task: tasks.subtitle.clone(),
cover_task: tasks.cover.clone(),
nfo_task: tasks.nfo.clone(),
create_ts,
completed_ts: None,
};
@@ -518,6 +527,7 @@ fn create_normal_progresses_for_season(
danmaku_task: tasks.danmaku,
subtitle_task: tasks.subtitle,
cover_task: tasks.cover,
nfo_task: tasks.nfo,
create_ts,
completed_ts: None,
};
@@ -556,6 +566,7 @@ fn create_normal_progresses_for_season(
danmaku_task: tasks.danmaku,
subtitle_task: tasks.subtitle,
cover_task: tasks.cover,
nfo_task: tasks.nfo,
create_ts,
completed_ts: None,
};
@@ -595,6 +606,7 @@ fn create_normal_progresses_for_season(
danmaku_task: tasks.danmaku.clone(),
subtitle_task: tasks.subtitle.clone(),
cover_task: tasks.cover.clone(),
nfo_task: tasks.nfo.clone(),
create_ts,
completed_ts: None,
};
@@ -615,6 +627,7 @@ struct Tasks {
danmaku: DanmakuTask,
subtitle: SubtitleTask,
cover: CoverTask,
nfo: NfoTask,
}
impl Tasks {
@@ -661,6 +674,11 @@ impl Tasks {
completed: false,
};
let nfo = NfoTask {
selected: config.download_nfo,
completed: false,
};
Self {
video,
audio,
@@ -668,6 +686,7 @@ impl Tasks {
danmaku,
subtitle,
cover,
nfo,
}
}
}
+186 -1
View File
@@ -17,10 +17,17 @@ use tokio::{
};
use crate::{
bili_client::BiliClient,
danmaku_xml_to_ass::xml_to_ass,
downloader::episode_type::EpisodeType,
events::DownloadEvent,
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},
};
@@ -325,6 +332,15 @@ impl DownloadTask {
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()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
@@ -746,6 +762,116 @@ impl DownloadTask {
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) {
let task_id = &self.task_id;
let mut remaining_sec = self.app.get_config().read().task_download_interval_sec;
@@ -978,3 +1104,62 @@ impl DownloadChunkTask {
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))
}
}
+1
View File
@@ -2,5 +2,6 @@ pub mod audio_task;
pub mod cover_task;
pub mod danmaku_task;
pub mod merge_task;
pub mod nfo_task;
pub mod subtitle_task;
pub mod video_task;
+354
View File
@@ -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)
}
}
+1
View File
@@ -18,6 +18,7 @@ pub mod player_info;
pub mod qrcode_data;
pub mod qrcode_status;
pub mod subtitle;
pub mod tags;
pub mod user_info;
pub mod video_quality;
pub mod watch_later_info;
+14
View File
@@ -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
View File
@@ -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 CodecType = "Unknown" | "Audio" | "AVC" | "HEVC" | "AV1"
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 ContentList = { bold: boolean; content: string; number: 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 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; 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 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 }
@@ -308,6 +308,7 @@ export type MediaInWatchLater = { aid: number; videos: number; tid: number; tnam
export type MergeTask = { selected: boolean; completed: boolean }
export type NewEp = { desc: string; id: number; is_new: number; title: 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 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 }