diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index f897e66..5a94534 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -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 { + // 发送获取普通视频标签的请求 + 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 { let sessdata = self.app.get_config().read().sessdata.clone(); format!("SESSDATA={sessdata}") diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 545b92a..3b0dbd6 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -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(), diff --git a/src-tauri/src/downloader/download_progress.rs b/src-tauri/src/downloader/download_progress.rs index d8912a6..98db687 100644 --- a/src-tauri/src/downloader/download_progress.rs +++ b/src-tauri/src/downloader/download_progress.rs @@ -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, } @@ -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, } } } diff --git a/src-tauri/src/downloader/download_task.rs b/src-tauri/src/downloader/download_task.rs index 8e5e93b..c1a0a7d 100644 --- a/src-tauri/src/downloader/download_task.rs +++ b/src-tauri/src/downloader/download_task.rs @@ -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, + ) -> 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, + episode_type: EpisodeType, + ) -> anyhow::Result<&'a mut EpisodeInfo>; +} + +impl GetOrInitEpisodeInfo for Option { + async fn get_or_init<'a>( + &'a mut self, + bili_client: &BiliClient, + aid: i64, + ep_id: Option, + 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)) + } +} diff --git a/src-tauri/src/downloader/tasks/mod.rs b/src-tauri/src/downloader/tasks/mod.rs index e5d332c..048a59d 100644 --- a/src-tauri/src/downloader/tasks/mod.rs +++ b/src-tauri/src/downloader/tasks/mod.rs @@ -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; diff --git a/src-tauri/src/downloader/tasks/nfo_task.rs b/src-tauri/src/downloader/tasks/nfo_task.rs new file mode 100644 index 0000000..476067f --- /dev/null +++ b/src-tauri/src/downloader/tasks/nfo_task.rs @@ -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, + runtime: u64, + premiered: String, + year: i32, + studio: Vec, + genre: Vec, + tag: Vec, + country: Vec, + set: Option, + director: Vec, + actor: Vec, +} + +#[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, + premiered: String, + year: i32, + studio: Vec, + status: String, + genre: Vec, + tag: Vec, + country: Vec, + director: Vec, +} + +#[derive(YaSerialize, YaDeserialize)] +#[yaserde(rename = "episodedetails")] +struct EpisodeDetails { + title: String, + plot: String, + tagline: Option, + runtime: u64, + premiered: String, + year: i32, + episode: i64, + studio: Vec, + genre: Vec, + tag: Vec, + country: Vec, + director: Vec, +} + +impl NormalInfo { + pub fn to_movie_nfo(&self, tags: Tags) -> anyhow::Result { + let genre = vec![ + "Bilibili视频".to_string(), + self.tname.clone(), + self.tname_v2.clone(), + ]; + + let tag: Vec = 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 { + 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 { + 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 { + if let Some(up_info) = &self.up_info { + vec![up_info.uname.clone()] + } else { + Vec::new() + } + } + + fn get_country(&self) -> Vec { + self.areas + .iter() + .filter(|area| !area.name.is_empty()) + .map(|area| area.name.clone()) + .collect() + } + + fn get_genre(&self) -> Vec { + 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 { + 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 { + 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) + } +} diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs index 8a3e106..732465a 100644 --- a/src-tauri/src/types/mod.rs +++ b/src-tauri/src/types/mod.rs @@ -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; diff --git a/src-tauri/src/types/tags.rs b/src-tauri/src/types/tags.rs new file mode 100644 index 0000000..71011d9 --- /dev/null +++ b/src-tauri/src/types/tags.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +pub type Tags = Vec; + +#[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, +} diff --git a/src/bindings.ts b/src/bindings.ts index a399740..81fc69b 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -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 }