diff --git a/entry/src/data.rs b/entry/src/data.rs index c65620c..f477707 100644 --- a/entry/src/data.rs +++ b/entry/src/data.rs @@ -1,6 +1,7 @@ use crate::CONFIG; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +use std::fmt::Debug; use std::path::PathBuf; #[derive(Debug)] @@ -41,6 +42,8 @@ pub struct State { pub git_hash: Option, pub master_hash: Option, pub event_id: Option, + #[serde(default)] + pub unlinked_cards: Vec, } impl State { fn load(x: &str) -> Self { @@ -52,6 +55,7 @@ impl State { git_hash: None, master_hash: None, event_id: None, + unlinked_cards: Vec::new(), } } @@ -62,6 +66,7 @@ impl State { self.git_hash = i.git_hash; self.master_hash = i.master_hash; self.event_id = i.event_id; + self.unlinked_cards = i.unlinked_cards; } Ok(()) } @@ -199,6 +204,21 @@ impl Card { _ => "".to_string(), } } + + pub fn build_card_details(&self) -> String { + format!( + "#{id} {attr}{rarity} {skill} [{prefix}]{name}", + id = self.id, + attr = self.attr_short_text(), + rarity = self.rarity_short_text(), + skill = self + .skill_id_short() + .map(|s| format!("({})", s)) + .unwrap_or_default(), + prefix = self.prefix, + name = self.character_name() + ) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -217,7 +237,41 @@ pub struct FetchResultCard { } #[derive(Debug, Clone, Serialize)] -pub struct FetchResult { +pub struct FetchResultEvent { pub event_id: u64, pub cards: Vec, } + +#[derive(Debug, Clone, Serialize)] +pub struct FetchResultUnlinkedCards { + pub cards: Vec, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type", content = "data")] +pub enum FetchResult { + Event(FetchResultEvent), + UnlinkedCard(FetchResultUnlinkedCards), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "type", content = "data")] +pub enum ApiResult +where + T: Debug + Clone + Serialize, +{ + Ok(T), + Err(String), +} + +impl ApiResult +where + T: Debug + Clone + Serialize, +{ + pub fn from(result: anyhow::Result) -> Self { + match result { + Ok(v) => Self::Ok(v), + Err(e) => Self::Err(e.to_string()), + } + } +} diff --git a/entry/src/download.rs b/entry/src/download.rs index a81133a..695a0ec 100644 --- a/entry/src/download.rs +++ b/entry/src/download.rs @@ -1,9 +1,19 @@ -use crate::{CONFIG, OPT_CONFIG}; +use crate::data::{Card, FetchResultCard}; use crate::github::get_master_data; +use crate::{CONFIG, OPT_CONFIG, ending_point}; +use anyhow::anyhow; use assets_updater::core::asset_execution::AssetExecutionContext; -use assets_updater::core::config::{AppConfig, CryptoConfig, ExecutionConfig, RegionConfig, RegionProviderConfig, RegionRuntimeConfig, ToolsConfig}; +use assets_updater::core::config::{ + AppConfig, CryptoConfig, ExecutionConfig, RegionConfig, RegionProviderConfig, + RegionRuntimeConfig, ToolsConfig, +}; use assets_updater::core::errors::AssetExecutionError; +use assets_updater::core::export_pipeline::find_files; use common::updater::SyncContext; +use futures::StreamExt; +use futures::stream::FuturesUnordered; +use log::debug; +use std::fs; use std::path::PathBuf; pub struct Downloader { @@ -21,8 +31,8 @@ impl Downloader { } let app_config = AppConfig { execution, - tools: ToolsConfig{ - asset_studio_cli_path : Some(CONFIG.get().unwrap().as_path.clone()), + tools: ToolsConfig { + asset_studio_cli_path: Some(CONFIG.get().unwrap().as_path.clone()), ..Default::default() }, ..Default::default() @@ -58,3 +68,60 @@ impl Downloader { self.exec.download(bundle_path, &self.app_config).await } } + +pub async fn download_cards(cards: &Vec) -> anyhow::Result> { + if cards.is_empty() { + return Ok(Vec::new()); + } + let downloader = Downloader::new().await?; + ending_point!(ok); + let cache_dir = CONFIG.get().unwrap().cache_dir(); + let mut futures = FuturesUnordered::new(); + for card in cards { + futures.push(async { + ending_point!(ok); + debug!( + "Pulling card {} from bundle {}", + card.id, card.assetbundle_name + ); + let dir = downloader + .pull(&format!("character/member/{}", card.assetbundle_name)) + .await?; + ending_point!(ok_with { + fs::remove_dir_all(dir.0)?; + }); + let files = find_files(&dir.0)?; + let mut local_images = Vec::new(); + for file in files { + ending_point!(ok_with { + fs::remove_dir_all(dir.0)?; + }); + if file.is_file() && file.extension().map(|ext| ext == "png").unwrap_or(false) { + let new_filename = format!( + "{}_{}", + card.assetbundle_name, + file.file_name().unwrap().to_str().unwrap() + ); + let new_file = cache_dir.join(new_filename); + fs::copy(&file, &new_file)?; + fs::remove_file(&file)?; + local_images.push(new_file); + } + } + fs::remove_dir_all(dir.0)?; + Ok::<_, anyhow::Error>((card.clone(), local_images)) + }); + } + let mut images = Vec::new(); + while let Some(res) = futures.next().await { + images.push(res?); + } + images.sort_by(|a, b| a.0.id.cmp(&b.0.id)); // 小在前 + Ok(images + .into_iter() + .map(|(card, path)| FetchResultCard { + text: card.build_card_details(), + path, + }) + .collect()) +} diff --git a/entry/src/lib.rs b/entry/src/lib.rs index eca90be..1f57bb8 100644 --- a/entry/src/lib.rs +++ b/entry/src/lib.rs @@ -9,8 +9,8 @@ mod github; mod main_loop; mod store; -use crate::data::{Config, FetchResult, OptConfig, State}; -use crate::main_loop::main; +use crate::data::{ApiResult, Config, FetchResult, OptConfig, State}; +use crate::main_loop::{main, refetch_event_cards}; use crate::store::Store; use chrono::Local; use fern::colors::{Color, ColoredLevelConfig}; @@ -25,12 +25,13 @@ use tokio::sync::Notify; pub static CONFIG: OnceLock = OnceLock::new(); pub static OPT_CONFIG: RwLock = RwLock::new(OptConfig::new()); -pub static PROVIDE: RwLock> = RwLock::new(None); +pub static PROVIDE: RwLock>> = RwLock::new(None); pub static STORE: RwLock = RwLock::new(Store::new()); pub static STATE: RwLock = RwLock::new(State::new()); pub static THREAD: RwLock>> = RwLock::new(None); pub static KILLING: AtomicBool = AtomicBool::new(false); pub static NOTIFY: Notify = Notify::const_new(); + #[macro_export] macro_rules! ending_point { (ok) => { @@ -194,6 +195,23 @@ pub unsafe extern "C" fn fetch_data() -> *mut c_char { c_str.into_raw() } +/// # Safety +#[unsafe(no_mangle)] +pub unsafe extern "C" fn refetch_event() -> *mut c_char { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to create Tokio runtime"); + + let result = rt.block_on(refetch_event_cards()); + let result = ApiResult::from(result); + + let json_string = serde_json::to_string(&result).unwrap(); + + let c_str = CString::new(json_string).unwrap(); + c_str.into_raw() +} + /// # Safety #[unsafe(no_mangle)] pub unsafe extern "C" fn free_string(s: *mut c_char) { diff --git a/entry/src/main_loop.rs b/entry/src/main_loop.rs index a3eec37..3dd4016 100644 --- a/entry/src/main_loop.rs +++ b/entry/src/main_loop.rs @@ -1,13 +1,11 @@ -use crate::data::{FetchResult, FetchResultCard, State}; -use crate::download::Downloader; +use crate::data::{ + FetchResult, FetchResultEvent, FetchResultUnlinkedCards, State, +}; +use crate::download::{download_cards}; use crate::github::{fetch_last_hash, sync_data}; -use crate::{CONFIG, PROVIDE, STATE, STORE, ending_point}; +use crate::{PROVIDE, STATE, STORE, ending_point}; use anyhow::anyhow; -use assets_updater::core::export_pipeline::find_files; -use futures::StreamExt; -use futures::stream::FuturesUnordered; use log::{debug, info}; -use std::fs; pub async fn main() -> anyhow::Result<()> { let current_hash = fetch_last_hash().await?; @@ -38,6 +36,53 @@ pub async fn main() -> anyhow::Result<()> { ending_point!(ok); sync_data().await?; + + // unlinked + let unlinked_cards = { + STORE + .read() + .map_err(|e| anyhow!("{}", e))? + .find_unlinked_cards() + }; + { + let new_cards: Vec<_> = { + let state = STATE.read().map_err(|e| anyhow!("{}", e))?; + if !state.unlinked_cards.is_empty() { + unlinked_cards + .iter() + .filter(|c| !state.unlinked_cards.contains(&c.id)) + .cloned() + .collect() + } else { + Vec::new() + } + }; + if !new_cards.is_empty() { + ending_point!(ok); + let images = download_cards(&new_cards).await?; + info!("collected {} unlinked cards", images.len()); + ending_point!(ok); + let result = FetchResultUnlinkedCards { cards: images }; + debug!("Providing result: {:?}", result); + { + let mut provide = PROVIDE.write().map_err(|e| anyhow!("{}", e))?; + if provide.is_none() { + *provide = Some(Vec::new()); + } + provide + .as_mut() + .unwrap() + .push(FetchResult::UnlinkedCard(result)); + } + ending_point!(ok); + } + + let mut state = STATE.write().map_err(|e| anyhow!("{}", e))?; + state.unlinked_cards = unlinked_cards.iter().map(|c| c.id).collect(); + state.save(); + } + + // event ending_point!(ok); let store = STORE.read().map_err(|e| anyhow!("{}", e))?; let event = store.find_last_event(); @@ -61,79 +106,24 @@ pub async fn main() -> anyhow::Result<()> { let cards = store.find_cards_by_event(event.event_id); drop(store); ending_point!(ok); - let downloader = Downloader::new().await?; - ending_point!(ok); - let cache_dir = CONFIG.get().unwrap().cache_dir(); - let mut futures = FuturesUnordered::new(); - for card in &cards { - futures.push(async { - ending_point!(ok); - debug!( - "Pulling card {} from bundle {}", - card.id, card.assetbundle_name - ); - let dir = downloader - .pull(&format!("character/member/{}", card.assetbundle_name)) - .await?; - ending_point!(ok_with { - fs::remove_dir_all(dir.0)?; - }); - let files = find_files(&dir.0)?; - let mut local_images = Vec::new(); - for file in files { - ending_point!(ok_with { - fs::remove_dir_all(dir.0)?; - }); - if file.is_file() && file.extension().map(|ext| ext == "png").unwrap_or(false) { - let new_filename = format!( - "{}_{}", - card.assetbundle_name, - file.file_name().unwrap().to_str().unwrap() - ); - let new_file = cache_dir.join(new_filename); - fs::copy(&file, &new_file)?; - fs::remove_file(&file)?; - local_images.push(new_file); - } - } - fs::remove_dir_all(dir.0)?; - Ok::<_, anyhow::Error>((card.clone(), local_images)) - }); - } - let mut images = Vec::new(); - while let Some(res) = futures.next().await { - images.push(res?); - } - images.sort_by(|a, b| a.0.id.cmp(&b.0.id)); // 小在前 - let images: Vec<_> = images - .into_iter() - .map(|(card, path)| FetchResultCard { - text: format!( - "#{id} {attr}{rarity} {skill} [{prefix}]{name}", - id = card.id, - attr = card.attr_short_text(), - rarity = card.rarity_short_text(), - skill = card - .skill_id_short().map(|s| format!("({})", s)) - .unwrap_or_default(), - prefix = card.prefix, - name = card.character_name() - ), - path, - }) - .collect(); - + let images = download_cards(&cards).await?; info!( "In event {} collected {} cards", event.event_id, images.len() ); - let result = FetchResult { + let result = FetchResultEvent { event_id: event.event_id, cards: images, }; debug!("Providing result: {:?}", result); - *PROVIDE.write().map_err(|e| anyhow!("{}", e))? = Some(result); + { + let mut provide = PROVIDE.write().map_err(|e| anyhow!("{}", e))?; + if provide.is_none() { + *provide = Some(Vec::new()); + } + provide.as_mut().unwrap().push(FetchResult::Event(result)); + } let mut state = STATE.write().map_err(|e| anyhow!("{}", e))?; state.git_hash = Some(current_hash); state.event_id = Some(event.event_id); @@ -141,3 +131,16 @@ pub async fn main() -> anyhow::Result<()> { Ok(()) } + +pub async fn refetch_event_cards() -> anyhow::Result { + let event_id = { STATE.read().map_err(|e| anyhow!("{}", e))?.event_id }; + let event_id = event_id.ok_or(anyhow!("Event ID not found"))?; + let store = STORE.read().map_err(|e| anyhow!("{}", e))?; + let cards = store.find_cards_by_event(event_id); + ending_point!(ok); + let images = download_cards(&cards).await?; + Ok(FetchResultEvent { + event_id, + cards: images, + }) +} diff --git a/entry/src/store.rs b/entry/src/store.rs index 24de17e..8cf2026 100644 --- a/entry/src/store.rs +++ b/entry/src/store.rs @@ -85,11 +85,19 @@ impl Store { self.event_cards .iter() .filter(|e| e.event_id == event_id) - .filter_map(|c| { - self.cards + .filter_map(|c| self.cards.iter().rev().find(|e| e.id == c.card_id)) + .cloned() + .collect() + } + + pub fn find_unlinked_cards(&self) -> Vec { + self.cards + .iter() + .filter(|c| { + self.event_cards .iter() - .rev() - .find(|e| e.id == c.card_id) + .find(|e| e.card_id == c.id) + .is_none() }) .cloned() .collect()