diff --git a/crates/bili_sync/src/utils/nfo.rs b/crates/bili_sync/src/utils/nfo.rs index adf1e04..8e1191f 100644 --- a/crates/bili_sync/src/utils/nfo.rs +++ b/crates/bili_sync/src/utils/nfo.rs @@ -1,213 +1,241 @@ use anyhow::Result; use bili_sync_entity::*; +use chrono::NaiveDateTime; use quick_xml::Error; use quick_xml::events::{BytesCData, BytesText}; use quick_xml::writer::Writer; -use tokio::io::AsyncWriteExt; +use tokio::io::{AsyncWriteExt, BufWriter}; -use crate::config::NFOTimeType; +use crate::config::{CONFIG, NFOTimeType}; -#[allow(clippy::upper_case_acronyms)] -pub enum NFOMode { - MOVIE, - TVSHOW, - EPOSODE, - UPPER, +pub enum NFO<'a> { + Movie(Movie<'a>), + TVShow(TVShow<'a>), + Upper(Upper), + Episode(Episode<'a>), } -pub enum ModelWrapper<'a> { - Video(&'a video::Model), - Page(&'a page::Model), +pub struct Movie<'a> { + pub name: &'a str, + pub intro: &'a str, + pub bvid: &'a str, + pub upper_id: i64, + pub upper_name: &'a str, + pub aired: NaiveDateTime, + pub tags: Option>, } -pub struct NFOSerializer<'a>(pub ModelWrapper<'a>, pub NFOMode); +pub struct TVShow<'a> { + pub name: &'a str, + pub intro: &'a str, + pub bvid: &'a str, + pub upper_id: i64, + pub upper_name: &'a str, + pub aired: NaiveDateTime, + pub tags: Option>, +} -/// serde xml 似乎不太好用,先这么裸着写 -/// (真是又臭又长啊 -impl NFOSerializer<'_> { - pub async fn generate_nfo(self, nfo_time_type: &NFOTimeType) -> Result { +pub struct Upper { + pub upper_id: String, + pub pubtime: NaiveDateTime, +} + +pub struct Episode<'a> { + pub name: &'a str, + pub pid: String, +} + +impl NFO<'_> { + pub async fn generate_nfo(self) -> Result { let mut buffer = r#" "# .as_bytes() .to_vec(); - let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer); - let mut writer = Writer::new_with_indent(&mut tokio_buffer, b' ', 4); + let mut tokio_buffer = BufWriter::new(&mut buffer); + let writer = Writer::new_with_indent(&mut tokio_buffer, b' ', 4); match self { - NFOSerializer(ModelWrapper::Video(v), NFOMode::MOVIE) => { - let nfo_time = match nfo_time_type { - NFOTimeType::FavTime => v.favtime, - NFOTimeType::PubTime => v.pubtime, - }; - writer - .create_element("movie") - .write_inner_content_async::<_, _, Error>(|writer| async move { - writer - .create_element("plot") - .write_cdata_content_async(BytesCData::new(Self::format_plot(v))) - .await?; - writer.create_element("outline").write_empty_async().await?; - writer - .create_element("title") - .write_text_content_async(BytesText::new(&v.name)) - .await?; - writer - .create_element("actor") - .write_inner_content_async::<_, _, Error>(|writer| async move { - writer - .create_element("name") - .write_text_content_async(BytesText::new(&v.upper_id.to_string())) - .await?; - writer - .create_element("role") - .write_text_content_async(BytesText::new(&v.upper_name)) - .await?; - Ok(writer) - }) - .await?; - writer - .create_element("year") - .write_text_content_async(BytesText::new(&nfo_time.format("%Y").to_string())) - .await?; - if let Some(tags) = &v.tags { - let tags: Vec = serde_json::from_value(tags.clone()).unwrap_or_default(); - for tag in tags { - writer - .create_element("genre") - .write_text_content_async(BytesText::new(&tag)) - .await?; - } - } - writer - .create_element("uniqueid") - .with_attribute(("type", "bilibili")) - .write_text_content_async(BytesText::new(&v.bvid)) - .await?; - writer - .create_element("aired") - .write_text_content_async(BytesText::new(&nfo_time.format("%Y-%m-%d").to_string())) - .await?; - Ok(writer) - }) - .await?; + NFO::Movie(movie) => { + Self::write_movie_nfo(writer, movie).await?; } - NFOSerializer(ModelWrapper::Video(v), NFOMode::TVSHOW) => { - let nfo_time = match nfo_time_type { - NFOTimeType::FavTime => v.favtime, - NFOTimeType::PubTime => v.pubtime, - }; - writer - .create_element("tvshow") - .write_inner_content_async::<_, _, Error>(|writer| async move { - writer - .create_element("plot") - .write_cdata_content_async(BytesCData::new(Self::format_plot(v))) - .await?; - writer.create_element("outline").write_empty_async().await?; - writer - .create_element("title") - .write_text_content_async(BytesText::new(&v.name)) - .await?; - writer - .create_element("actor") - .write_inner_content_async::<_, _, Error>(|writer| async move { - writer - .create_element("name") - .write_text_content_async(BytesText::new(&v.upper_id.to_string())) - .await?; - writer - .create_element("role") - .write_text_content_async(BytesText::new(&v.upper_name)) - .await?; - Ok(writer) - }) - .await?; - writer - .create_element("year") - .write_text_content_async(BytesText::new(&nfo_time.format("%Y").to_string())) - .await?; - if let Some(tags) = &v.tags { - let tags: Vec = serde_json::from_value(tags.clone()).unwrap_or_default(); - for tag in tags { - writer - .create_element("genre") - .write_text_content_async(BytesText::new(&tag)) - .await?; - } - } - writer - .create_element("uniqueid") - .with_attribute(("type", "bilibili")) - .write_text_content_async(BytesText::new(&v.bvid)) - .await?; - writer - .create_element("aired") - .write_text_content_async(BytesText::new(&nfo_time.format("%Y-%m-%d").to_string())) - .await?; - Ok(writer) - }) - .await?; + NFO::TVShow(tvshow) => { + Self::write_tvshow_nfo(writer, tvshow).await?; } - NFOSerializer(ModelWrapper::Video(v), NFOMode::UPPER) => { - writer - .create_element("person") - .write_inner_content_async::<_, _, Error>(|writer| async move { - writer.create_element("plot").write_empty_async().await?; - writer.create_element("outline").write_empty_async().await?; - writer - .create_element("lockdata") - .write_text_content_async(BytesText::new("false")) - .await?; - writer - .create_element("dateadded") - .write_text_content_async(BytesText::new( - &v.pubtime.format("%Y-%m-%d %H:%M:%S").to_string(), - )) - .await?; - writer - .create_element("title") - .write_text_content_async(BytesText::new(&v.upper_id.to_string())) - .await?; - writer - .create_element("sorttitle") - .write_text_content_async(BytesText::new(&v.upper_id.to_string())) - .await?; - Ok(writer) - }) - .await?; + NFO::Upper(upper) => { + Self::write_upper_nfo(writer, upper).await?; } - NFOSerializer(ModelWrapper::Page(p), NFOMode::EPOSODE) => { - writer - .create_element("episodedetails") - .write_inner_content_async::<_, _, Error>(|writer| async move { - writer.create_element("plot").write_empty_async().await?; - writer.create_element("outline").write_empty_async().await?; - writer - .create_element("title") - .write_text_content_async(BytesText::new(&p.name)) - .await?; - writer - .create_element("season") - .write_text_content_async(BytesText::new("1")) - .await?; - writer - .create_element("episode") - .write_text_content_async(BytesText::new(&p.pid.to_string())) - .await?; - Ok(writer) - }) - .await?; + NFO::Episode(episode) => { + Self::write_episode_nfo(writer, episode).await?; } - _ => unreachable!(), } tokio_buffer.flush().await?; Ok(String::from_utf8(buffer)?) } + async fn write_movie_nfo(mut writer: Writer<&mut BufWriter<&mut Vec>>, movie: Movie<'_>) -> Result<()> { + writer + .create_element("movie") + .write_inner_content_async::<_, _, Error>(|writer| async move { + writer + .create_element("plot") + .write_cdata_content_async(BytesCData::new(Self::format_plot(movie.bvid, movie.intro))) + .await?; + writer.create_element("outline").write_empty_async().await?; + writer + .create_element("title") + .write_text_content_async(BytesText::new(movie.name)) + .await?; + writer + .create_element("actor") + .write_inner_content_async::<_, _, Error>(|writer| async move { + writer + .create_element("name") + .write_text_content_async(BytesText::new(&movie.upper_id.to_string())) + .await?; + writer + .create_element("role") + .write_text_content_async(BytesText::new(movie.upper_name)) + .await?; + Ok(writer) + }) + .await?; + writer + .create_element("year") + .write_text_content_async(BytesText::new(&movie.aired.format("%Y").to_string())) + .await?; + if let Some(tags) = movie.tags { + for tag in tags { + writer + .create_element("genre") + .write_text_content_async(BytesText::new(&tag)) + .await?; + } + } + writer + .create_element("uniqueid") + .with_attribute(("type", "bilibili")) + .write_text_content_async(BytesText::new(movie.bvid)) + .await?; + writer + .create_element("aired") + .write_text_content_async(BytesText::new(&movie.aired.format("%Y-%m-%d").to_string())) + .await?; + Ok(writer) + }) + .await?; + Ok(()) + } + + async fn write_tvshow_nfo(mut writer: Writer<&mut BufWriter<&mut Vec>>, tvshow: TVShow<'_>) -> Result<()> { + writer + .create_element("tvshow") + .write_inner_content_async::<_, _, Error>(|writer| async move { + writer + .create_element("plot") + .write_cdata_content_async(BytesCData::new(Self::format_plot(tvshow.bvid, tvshow.intro))) + .await?; + writer.create_element("outline").write_empty_async().await?; + writer + .create_element("title") + .write_text_content_async(BytesText::new(tvshow.name)) + .await?; + writer + .create_element("actor") + .write_inner_content_async::<_, _, Error>(|writer| async move { + writer + .create_element("name") + .write_text_content_async(BytesText::new(&tvshow.upper_id.to_string())) + .await?; + writer + .create_element("role") + .write_text_content_async(BytesText::new(tvshow.upper_name)) + .await?; + Ok(writer) + }) + .await?; + writer + .create_element("year") + .write_text_content_async(BytesText::new(&tvshow.aired.format("%Y").to_string())) + .await?; + if let Some(tags) = tvshow.tags { + for tag in tags { + writer + .create_element("genre") + .write_text_content_async(BytesText::new(&tag)) + .await?; + } + } + writer + .create_element("uniqueid") + .with_attribute(("type", "bilibili")) + .write_text_content_async(BytesText::new(tvshow.bvid)) + .await?; + writer + .create_element("aired") + .write_text_content_async(BytesText::new(&tvshow.aired.format("%Y-%m-%d").to_string())) + .await?; + Ok(writer) + }) + .await?; + Ok(()) + } + + async fn write_upper_nfo(mut writer: Writer<&mut BufWriter<&mut Vec>>, upper: Upper) -> Result<()> { + writer + .create_element("person") + .write_inner_content_async::<_, _, Error>(|writer| async move { + writer.create_element("plot").write_empty_async().await?; + writer.create_element("outline").write_empty_async().await?; + writer + .create_element("lockdata") + .write_text_content_async(BytesText::new("false")) + .await?; + writer + .create_element("dateadded") + .write_text_content_async(BytesText::new(&upper.pubtime.format("%Y-%m-%d %H:%M:%S").to_string())) + .await?; + writer + .create_element("title") + .write_text_content_async(BytesText::new(&upper.upper_id)) + .await?; + writer + .create_element("sorttitle") + .write_text_content_async(BytesText::new(&upper.upper_id)) + .await?; + Ok(writer) + }) + .await?; + Ok(()) + } + + async fn write_episode_nfo(mut writer: Writer<&mut BufWriter<&mut Vec>>, episode: Episode<'_>) -> Result<()> { + writer + .create_element("episodedetails") + .write_inner_content_async::<_, _, Error>(|writer| async move { + writer.create_element("plot").write_empty_async().await?; + writer.create_element("outline").write_empty_async().await?; + writer + .create_element("title") + .write_text_content_async(BytesText::new(episode.name)) + .await?; + writer + .create_element("season") + .write_text_content_async(BytesText::new("1")) + .await?; + writer + .create_element("episode") + .write_text_content_async(BytesText::new(&episode.pid)) + .await?; + Ok(writer) + }) + .await?; + Ok(()) + } + #[inline] - fn format_plot(model: &video::Model) -> String { + fn format_plot(bvid: &str, intro: &str) -> String { format!( r#"原始视频:{}

{}"#, - model.bvid, model.bvid, model.intro + bvid, bvid, intro, ) } } @@ -236,10 +264,7 @@ mod tests { ..Default::default() }; assert_eq!( - NFOSerializer(ModelWrapper::Video(&video), NFOMode::MOVIE) - .generate_nfo(&NFOTimeType::PubTime) - .await - .unwrap(), + NFO::Movie((&video).into()).generate_nfo().await.unwrap(), r#" BV1nWcSeeEkV

intro]]>
@@ -249,18 +274,15 @@ mod tests { 1 upper_name - 2033 + 2022 tag1 tag2 BV1nWcSeeEkV - 2033-03-03 + 2022-02-02
"#, ); assert_eq!( - NFOSerializer(ModelWrapper::Video(&video), NFOMode::TVSHOW) - .generate_nfo(&NFOTimeType::FavTime) - .await - .unwrap(), + NFO::TVShow((&video).into()).generate_nfo().await.unwrap(), r#" BV1nWcSeeEkV

intro]]>
@@ -278,10 +300,7 @@ mod tests {
"#, ); assert_eq!( - NFOSerializer(ModelWrapper::Video(&video), NFOMode::UPPER) - .generate_nfo(&NFOTimeType::FavTime) - .await - .unwrap(), + NFO::Upper((&video).into()).generate_nfo().await.unwrap(), r#" @@ -298,10 +317,7 @@ mod tests { ..Default::default() }; assert_eq!( - NFOSerializer(ModelWrapper::Page(&page), NFOMode::EPOSODE) - .generate_nfo(&NFOTimeType::FavTime) - .await - .unwrap(), + NFO::Episode((&page).into()).generate_nfo().await.unwrap(), r#" @@ -313,3 +329,61 @@ mod tests { ); } } + +impl<'a> From<&'a video::Model> for Movie<'a> { + fn from(video: &'a video::Model) -> Self { + Self { + name: &video.name, + intro: &video.intro, + bvid: &video.bvid, + upper_id: video.upper_id, + upper_name: &video.upper_name, + aired: match CONFIG.nfo_time_type { + NFOTimeType::FavTime => video.favtime, + NFOTimeType::PubTime => video.pubtime, + }, + tags: video + .tags + .as_ref() + .and_then(|tags| serde_json::from_value(tags.clone()).ok()), + } + } +} + +impl<'a> From<&'a video::Model> for TVShow<'a> { + fn from(video: &'a video::Model) -> Self { + Self { + name: &video.name, + intro: &video.intro, + bvid: &video.bvid, + upper_id: video.upper_id, + upper_name: &video.upper_name, + aired: match CONFIG.nfo_time_type { + NFOTimeType::FavTime => video.favtime, + NFOTimeType::PubTime => video.pubtime, + }, + tags: video + .tags + .as_ref() + .and_then(|tags| serde_json::from_value(tags.clone()).ok()), + } + } +} + +impl<'a> From<&'a video::Model> for Upper { + fn from(video: &'a video::Model) -> Self { + Self { + upper_id: video.upper_id.to_string(), + pubtime: video.pubtime, + } + } +} + +impl<'a> From<&'a page::Model> for Episode<'a> { + fn from(page: &'a page::Model) -> Self { + Self { + name: &page.name, + pid: page.pid.to_string(), + } + } +} diff --git a/crates/bili_sync/src/workflow.rs b/crates/bili_sync/src/workflow.rs index 84a1823..9c97022 100644 --- a/crates/bili_sync/src/workflow.rs +++ b/crates/bili_sync/src/workflow.rs @@ -22,7 +22,7 @@ use crate::utils::model::{ create_pages, create_videos, filter_unfilled_videos, filter_unhandled_video_pages, update_pages_model, update_videos_model, }; -use crate::utils::nfo::{ModelWrapper, NFOMode, NFOSerializer}; +use crate::utils::nfo::NFO; use crate::utils::status::{PageStatus, STATUS_OK, VideoStatus}; /// 完整地处理某个视频来源 @@ -618,12 +618,12 @@ pub async fn generate_page_nfo( return Ok(ExecutionStatus::Skipped); } let single_page = video_model.single_page.context("single_page is null")?; - let nfo_serializer = if single_page { - NFOSerializer(ModelWrapper::Video(video_model), NFOMode::MOVIE) + let nfo = if single_page { + NFO::Movie(video_model.into()) } else { - NFOSerializer(ModelWrapper::Page(page_model), NFOMode::EPOSODE) + NFO::Episode(page_model.into()) }; - generate_nfo(nfo_serializer, nfo_path).await?; + generate_nfo(nfo, nfo_path).await?; Ok(ExecutionStatus::Succeeded) } @@ -663,8 +663,7 @@ pub async fn generate_upper_nfo( if !should_run { return Ok(ExecutionStatus::Skipped); } - let nfo_serializer = NFOSerializer(ModelWrapper::Video(video_model), NFOMode::UPPER); - generate_nfo(nfo_serializer, nfo_path).await?; + generate_nfo(NFO::Upper(video_model.into()), nfo_path).await?; Ok(ExecutionStatus::Succeeded) } @@ -676,21 +675,16 @@ pub async fn generate_video_nfo( if !should_run { return Ok(ExecutionStatus::Skipped); } - let nfo_serializer = NFOSerializer(ModelWrapper::Video(video_model), NFOMode::TVSHOW); - generate_nfo(nfo_serializer, nfo_path).await?; + generate_nfo(NFO::TVShow(video_model.into()), nfo_path).await?; Ok(ExecutionStatus::Succeeded) } /// 创建 nfo_path 的父目录,然后写入 nfo 文件 -async fn generate_nfo(serializer: NFOSerializer<'_>, nfo_path: PathBuf) -> Result<()> { +async fn generate_nfo(nfo: NFO<'_>, nfo_path: PathBuf) -> Result<()> { if let Some(parent) = nfo_path.parent() { fs::create_dir_all(parent).await?; } - fs::write( - nfo_path, - serializer.generate_nfo(&CONFIG.nfo_time_type).await?.as_bytes(), - ) - .await?; + fs::write(nfo_path, nfo.generate_nfo().await?.as_bytes()).await?; Ok(()) }