非event卡推送,以及手动查看

This commit is contained in:
2026-10-21 18:16:26 +08:00
parent 39e4c497c2
commit 3f88d44c3e
5 changed files with 233 additions and 83 deletions
+55 -1
View File
@@ -1,6 +1,7 @@
use crate::CONFIG; use crate::CONFIG;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::fmt::Debug;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug)] #[derive(Debug)]
@@ -41,6 +42,8 @@ pub struct State {
pub git_hash: Option<String>, pub git_hash: Option<String>,
pub master_hash: Option<String>, pub master_hash: Option<String>,
pub event_id: Option<u64>, pub event_id: Option<u64>,
#[serde(default)]
pub unlinked_cards: Vec<u64>,
} }
impl State { impl State {
fn load(x: &str) -> Self { fn load(x: &str) -> Self {
@@ -52,6 +55,7 @@ impl State {
git_hash: None, git_hash: None,
master_hash: None, master_hash: None,
event_id: None, event_id: None,
unlinked_cards: Vec::new(),
} }
} }
@@ -62,6 +66,7 @@ impl State {
self.git_hash = i.git_hash; self.git_hash = i.git_hash;
self.master_hash = i.master_hash; self.master_hash = i.master_hash;
self.event_id = i.event_id; self.event_id = i.event_id;
self.unlinked_cards = i.unlinked_cards;
} }
Ok(()) Ok(())
} }
@@ -199,6 +204,21 @@ impl Card {
_ => "".to_string(), _ => "".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)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -217,7 +237,41 @@ pub struct FetchResultCard {
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct FetchResult { pub struct FetchResultEvent {
pub event_id: u64, pub event_id: u64,
pub cards: Vec<FetchResultCard>, pub cards: Vec<FetchResultCard>,
} }
#[derive(Debug, Clone, Serialize)]
pub struct FetchResultUnlinkedCards {
pub cards: Vec<FetchResultCard>,
}
#[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<T>
where
T: Debug + Clone + Serialize,
{
Ok(T),
Err(String),
}
impl<T> ApiResult<T>
where
T: Debug + Clone + Serialize,
{
pub fn from(result: anyhow::Result<T>) -> Self {
match result {
Ok(v) => Self::Ok(v),
Err(e) => Self::Err(e.to_string()),
}
}
}
+71 -4
View File
@@ -1,9 +1,19 @@
use crate::{CONFIG, OPT_CONFIG}; use crate::data::{Card, FetchResultCard};
use crate::github::get_master_data; 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::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::errors::AssetExecutionError;
use assets_updater::core::export_pipeline::find_files;
use common::updater::SyncContext; use common::updater::SyncContext;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
use log::debug;
use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
pub struct Downloader { pub struct Downloader {
@@ -21,8 +31,8 @@ impl Downloader {
} }
let app_config = AppConfig { let app_config = AppConfig {
execution, execution,
tools: ToolsConfig{ tools: ToolsConfig {
asset_studio_cli_path : Some(CONFIG.get().unwrap().as_path.clone()), asset_studio_cli_path: Some(CONFIG.get().unwrap().as_path.clone()),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
@@ -58,3 +68,60 @@ impl Downloader {
self.exec.download(bundle_path, &self.app_config).await self.exec.download(bundle_path, &self.app_config).await
} }
} }
pub async fn download_cards(cards: &Vec<Card>) -> anyhow::Result<Vec<FetchResultCard>> {
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())
}
+21 -3
View File
@@ -9,8 +9,8 @@ mod github;
mod main_loop; mod main_loop;
mod store; mod store;
use crate::data::{Config, FetchResult, OptConfig, State}; use crate::data::{ApiResult, Config, FetchResult, OptConfig, State};
use crate::main_loop::main; use crate::main_loop::{main, refetch_event_cards};
use crate::store::Store; use crate::store::Store;
use chrono::Local; use chrono::Local;
use fern::colors::{Color, ColoredLevelConfig}; use fern::colors::{Color, ColoredLevelConfig};
@@ -25,12 +25,13 @@ use tokio::sync::Notify;
pub static CONFIG: OnceLock<Config> = OnceLock::new(); pub static CONFIG: OnceLock<Config> = OnceLock::new();
pub static OPT_CONFIG: RwLock<OptConfig> = RwLock::new(OptConfig::new()); pub static OPT_CONFIG: RwLock<OptConfig> = RwLock::new(OptConfig::new());
pub static PROVIDE: RwLock<Option<FetchResult>> = RwLock::new(None); pub static PROVIDE: RwLock<Option<Vec<FetchResult>>> = RwLock::new(None);
pub static STORE: RwLock<Store> = RwLock::new(Store::new()); pub static STORE: RwLock<Store> = RwLock::new(Store::new());
pub static STATE: RwLock<State> = RwLock::new(State::new()); pub static STATE: RwLock<State> = RwLock::new(State::new());
pub static THREAD: RwLock<Option<thread::JoinHandle<()>>> = RwLock::new(None); pub static THREAD: RwLock<Option<thread::JoinHandle<()>>> = RwLock::new(None);
pub static KILLING: AtomicBool = AtomicBool::new(false); pub static KILLING: AtomicBool = AtomicBool::new(false);
pub static NOTIFY: Notify = Notify::const_new(); pub static NOTIFY: Notify = Notify::const_new();
#[macro_export] #[macro_export]
macro_rules! ending_point { macro_rules! ending_point {
(ok) => { (ok) => {
@@ -194,6 +195,23 @@ pub unsafe extern "C" fn fetch_data() -> *mut c_char {
c_str.into_raw() 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 /// # Safety
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn free_string(s: *mut c_char) { pub unsafe extern "C" fn free_string(s: *mut c_char) {
+74 -71
View File
@@ -1,13 +1,11 @@
use crate::data::{FetchResult, FetchResultCard, State}; use crate::data::{
use crate::download::Downloader; FetchResult, FetchResultEvent, FetchResultUnlinkedCards, State,
};
use crate::download::{download_cards};
use crate::github::{fetch_last_hash, sync_data}; 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 anyhow::anyhow;
use assets_updater::core::export_pipeline::find_files;
use futures::StreamExt;
use futures::stream::FuturesUnordered;
use log::{debug, info}; use log::{debug, info};
use std::fs;
pub async fn main() -> anyhow::Result<()> { pub async fn main() -> anyhow::Result<()> {
let current_hash = fetch_last_hash().await?; let current_hash = fetch_last_hash().await?;
@@ -38,6 +36,53 @@ pub async fn main() -> anyhow::Result<()> {
ending_point!(ok); ending_point!(ok);
sync_data().await?; 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); ending_point!(ok);
let store = STORE.read().map_err(|e| anyhow!("{}", e))?; let store = STORE.read().map_err(|e| anyhow!("{}", e))?;
let event = store.find_last_event(); 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); let cards = store.find_cards_by_event(event.event_id);
drop(store); drop(store);
ending_point!(ok); ending_point!(ok);
let downloader = Downloader::new().await?; let images = download_cards(&cards).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();
info!( info!(
"In event {} collected {} cards", "In event {} collected {} cards",
event.event_id, event.event_id,
images.len() images.len()
); );
let result = FetchResult { let result = FetchResultEvent {
event_id: event.event_id, event_id: event.event_id,
cards: images, cards: images,
}; };
debug!("Providing result: {:?}", result); 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))?; let mut state = STATE.write().map_err(|e| anyhow!("{}", e))?;
state.git_hash = Some(current_hash); state.git_hash = Some(current_hash);
state.event_id = Some(event.event_id); state.event_id = Some(event.event_id);
@@ -141,3 +131,16 @@ pub async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
pub async fn refetch_event_cards() -> anyhow::Result<FetchResultEvent> {
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,
})
}
+11 -3
View File
@@ -85,11 +85,19 @@ impl Store {
self.event_cards self.event_cards
.iter() .iter()
.filter(|e| e.event_id == event_id) .filter(|e| e.event_id == event_id)
.filter_map(|c| { .filter_map(|c| self.cards.iter().rev().find(|e| e.id == c.card_id))
.cloned()
.collect()
}
pub fn find_unlinked_cards(&self) -> Vec<Card> {
self.cards self.cards
.iter() .iter()
.rev() .filter(|c| {
.find(|e| e.id == c.card_id) self.event_cards
.iter()
.find(|e| e.card_id == c.id)
.is_none()
}) })
.cloned() .cloned()
.collect() .collect()