Fetch file version from Nexus #147

Merged
lucas merged 5 commits from feat/nexus-file-version into master 2023-11-29 18:53:14 +01:00
4 changed files with 71 additions and 3 deletions

View file

@ -17,6 +17,7 @@
- dtmm: reset dtkit-patch installations - dtmm: reset dtkit-patch installations
- sdk: implement decompiling Lua files - sdk: implement decompiling Lua files
- dtmm: fetch cover image for Nexus mods - dtmm: fetch cover image for Nexus mods
- dtmm: fetch file version for Nexus mods
=== Fixed === Fixed

View file

@ -411,7 +411,7 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?; .wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
let data = Cursor::new(data); let data = Cursor::new(data);
let nexus = if let Some((_, id, version, _)) = info let nexus = if let Some((_, id, version, timestamp)) = info
.path .path
.file_name() .file_name()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
@ -423,9 +423,23 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
.mods_id(id) .mods_id(id)
.await .await
.wrap_err_with(|| format!("Failed to query mod {} from Nexus", id))?; .wrap_err_with(|| format!("Failed to query mod {} from Nexus", id))?;
let info = NexusInfo::from(mod_info);
tracing::debug!("{:?}", info); let version = match api.file_version(id, timestamp).await {
Ok(version) => version,
Err(err) => {
let err = Report::new(err);
tracing::warn!(
"Failed to fetch version for Nexus download. \
Falling back to file name:\n{:?}",
err
);
version
}
};
let info = NexusInfo::from(mod_info);
tracing::debug!(version, ?info);
Some((info, version)) Some((info, version))
} else { } else {
None None

View file

@ -39,6 +39,8 @@ pub enum Error {
Infallible(#[from] Infallible), Infallible(#[from] Infallible),
#[error("invalid NXM URL '{}': {0}", .1.as_str())] #[error("invalid NXM URL '{}': {0}", .1.as_str())]
InvalidNXM(&'static str, Url), InvalidNXM(&'static str, Url),
#[error("{0}")]
Custom(String),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -102,6 +104,28 @@ impl Api {
self.send(req).await self.send(req).await
} }
#[tracing::instrument(skip(self))]
pub async fn file_version<T>(&self, id: u64, timestamp: T) -> Result<String>
where
T: std::fmt::Debug,
OffsetDateTime: PartialEq<T>,
{
let url = BASE_URL_GAME.join(&format!("mods/{id}/files.json"))?;
let req = self.client.get(url);
let files: FileList = self.send(req).await?;
let Some(file) = files
.files
.into_iter()
.find(|file| file.uploaded_timestamp == timestamp)
else {
let err = Error::Custom("Timestamp does not match any file".into());
return Err(err);
};
Ok(file.version)
}
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub async fn picture(&self, url: impl IntoUrl + std::fmt::Debug) -> Result<Vec<u8>> { pub async fn picture(&self, url: impl IntoUrl + std::fmt::Debug) -> Result<Vec<u8>> {
let res = self.client.get(url).send().await?.error_for_status()?; let res = self.client.get(url).send().await?.error_for_status()?;

View file

@ -64,6 +64,35 @@ pub struct Mod {
// pub contains_adult_content: bool, // pub contains_adult_content: bool,
} }
#[derive(Debug, Deserialize)]
pub struct File {
pub id: Vec<u64>,
pub uid: u64,
pub file_id: u64,
pub name: String,
pub version: String,
pub category_id: u64,
pub category_name: String,
pub is_primary: bool,
pub size: u64,
pub file_name: String,
#[serde(with = "time::serde::timestamp")]
pub uploaded_timestamp: OffsetDateTime,
pub mod_version: String,
pub external_virus_scan_url: String,
pub description: String,
pub size_kb: u64,
pub size_in_bytes: u64,
pub changelog_html: Option<String>,
pub content_preview_link: String,
}
#[derive(Debug, Deserialize)]
pub struct FileList {
pub files: Vec<File>,
// pub file_updates: Vec<serde_json::Value>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct DownloadLink { pub struct DownloadLink {
pub name: String, pub name: String,