Handle NXM URIs #150
11 changed files with 296 additions and 43 deletions
|
@ -18,6 +18,7 @@
|
|||
- sdk: implement decompiling Lua files
|
||||
- dtmm: fetch cover image for Nexus mods
|
||||
- dtmm: fetch file version for Nexus mods
|
||||
- dtmm: handle `nxm://` URIs via IPC and import the corresponding mod
|
||||
|
||||
=== Fixed
|
||||
|
||||
|
|
30
Cargo.lock
generated
30
Cargo.lock
generated
|
@ -218,6 +218,15 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.64.0"
|
||||
|
@ -895,6 +904,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"ansi-parser",
|
||||
"async-recursion",
|
||||
"bincode",
|
||||
"bitflags 1.3.2",
|
||||
"clap",
|
||||
"color-eyre",
|
||||
|
@ -904,6 +914,7 @@ dependencies = [
|
|||
"druid-widget-nursery",
|
||||
"dtmt-shared",
|
||||
"futures",
|
||||
"interprocess",
|
||||
"lazy_static",
|
||||
"luajit2-sys",
|
||||
"minijinja",
|
||||
|
@ -1833,6 +1844,19 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interprocess"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"rustc_version",
|
||||
"to_method",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intl-memoizer"
|
||||
version = "0.5.1"
|
||||
|
@ -3517,6 +3541,12 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "to_method"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.33.0"
|
||||
|
|
|
@ -6,33 +6,35 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ansi-parser = "0.9.0"
|
||||
async-recursion = "1.0.5"
|
||||
bincode = "1.3.3"
|
||||
bitflags = "1.3.2"
|
||||
clap = { version = "4.0.15", features = ["color", "derive", "std", "cargo", "string", "unicode"] }
|
||||
color-eyre = "0.6.2"
|
||||
colors-transform = "0.2.11"
|
||||
confy = "0.5.1"
|
||||
druid = { version = "0.8", features = ["im", "serde", "image", "png", "jpeg", "bmp", "webp", "svg"] }
|
||||
druid-widget-nursery = "0.1"
|
||||
dtmt-shared = { path = "../../lib/dtmt-shared", version = "*" }
|
||||
futures = "0.3.25"
|
||||
oodle = { path = "../../lib/oodle", version = "*" }
|
||||
sdk = { path = "../../lib/sdk", version = "*" }
|
||||
interprocess = { version = "1.2.1", default-features = false }
|
||||
lazy_static = "1.4.0"
|
||||
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" }
|
||||
minijinja = "1.0.10"
|
||||
nexusmods = { path = "../../lib/nexusmods", version = "*" }
|
||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||
oodle = { path = "../../lib/oodle", version = "*" }
|
||||
path-slash = "0.2.1"
|
||||
sdk = { path = "../../lib/sdk", version = "*" }
|
||||
serde = { version = "1.0.152", features = ["derive", "rc"] }
|
||||
serde_sjson = { path = "../../lib/serde_sjson", version = "*" }
|
||||
string_template = "0.2.1"
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }
|
||||
tokio = { version = "1.23.0", features = ["rt", "fs", "tracing", "sync"] }
|
||||
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
zip = "0.6.4"
|
||||
tokio-stream = { version = "0.1.12", features = ["fs"] }
|
||||
path-slash = "0.2.1"
|
||||
time = { version = "0.3.20", features = ["serde", "serde-well-known", "local-offset"] }
|
||||
strip-ansi-escapes = "0.1.1"
|
||||
lazy_static = "1.4.0"
|
||||
colors-transform = "0.2.11"
|
||||
usvg = "0.25.0"
|
||||
druid-widget-nursery = "0.1"
|
||||
ansi-parser = "0.9.0"
|
||||
string_template = "0.2.1"
|
||||
luajit2-sys = { path = "../../lib/luajit2-sys", version = "*" }
|
||||
async-recursion = "1.0.5"
|
||||
minijinja = "1.0.10"
|
||||
zip = "0.6.4"
|
||||
|
|
10
crates/dtmm/assets/dtmm.desktop
Normal file
10
crates/dtmm/assets/dtmm.desktop
Normal file
|
@ -0,0 +1,10 @@
|
|||
[Desktop Entry]
|
||||
Name=DTMM
|
||||
GenericName=Mod Manager
|
||||
Comment=A graphical mod manager for Warhammer 40,000: Darktide
|
||||
Exec=dtmm %u
|
||||
Type=Application
|
||||
Keywords=Mod;
|
||||
StartupNotify=true
|
||||
Categories=Utility;
|
||||
MimeType=x-scheme-handler/nxm;
|
|
@ -405,11 +405,10 @@ fn extract_legacy_mod<R: Read + Seek>(
|
|||
}
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<ModInfo> {
|
||||
pub(crate) async fn import_from_file(state: ActionState, info: FileInfo) -> Result<ModInfo> {
|
||||
let data = fs::read(&info.path)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to read file {}", info.path.display()))?;
|
||||
let data = Cursor::new(data);
|
||||
|
||||
let nexus = if let Some((_, id, version, timestamp)) = info
|
||||
.path
|
||||
|
@ -450,6 +449,32 @@ pub(crate) async fn import_mod(state: ActionState, info: FileInfo) -> Result<Mod
|
|||
|
||||
tracing::trace!(?nexus);
|
||||
|
||||
import_mod(state, nexus, data).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub(crate) async fn import_from_nxm(state: ActionState, uri: String) -> Result<ModInfo> {
|
||||
let url = uri
|
||||
.parse()
|
||||
.wrap_err_with(|| format!("Invalid Uri '{}'", uri))?;
|
||||
|
||||
let api = NexusApi::new(state.nexus_api_key.to_string())?;
|
||||
let (mod_info, file_info, data) = api
|
||||
.handle_nxm(url)
|
||||
.await
|
||||
.wrap_err_with(|| format!("Failed to download mod from NXM uri '{}'", uri))?;
|
||||
|
||||
let nexus = NexusInfo::from(mod_info);
|
||||
import_mod(state, Some((nexus, file_info.version)), data).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(state, data), fields(data = data.len()))]
|
||||
pub(crate) async fn import_mod(
|
||||
state: ActionState,
|
||||
nexus: Option<(NexusInfo, String)>,
|
||||
data: Vec<u8>,
|
||||
) -> Result<ModInfo> {
|
||||
let data = Cursor::new(data);
|
||||
let mut archive = ZipArchive::new(data).wrap_err("Failed to open ZIP archive")?;
|
||||
|
||||
if tracing::enabled!(tracing::Level::DEBUG) {
|
||||
|
|
|
@ -15,7 +15,7 @@ use tokio::sync::RwLock;
|
|||
use crate::controller::app::*;
|
||||
use crate::controller::deploy::deploy_mods;
|
||||
use crate::controller::game::*;
|
||||
use crate::controller::import::import_mod;
|
||||
use crate::controller::import::*;
|
||||
use crate::state::AsyncAction;
|
||||
use crate::state::ACTION_FINISH_CHECK_UPDATE;
|
||||
use crate::state::ACTION_FINISH_LOAD_INITIAL;
|
||||
|
@ -57,7 +57,7 @@ async fn handle_action(
|
|||
.expect("failed to send command");
|
||||
}),
|
||||
AsyncAction::AddMod(state, info) => tokio::spawn(async move {
|
||||
match import_mod(state, info)
|
||||
match import_from_file(state, info)
|
||||
.await
|
||||
.wrap_err("Failed to import mod")
|
||||
{
|
||||
|
@ -186,6 +186,28 @@ async fn handle_action(
|
|||
let _ = f.write_all(&line).await;
|
||||
}
|
||||
}),
|
||||
AsyncAction::NxmDownload(state, uri) => tokio::spawn(async move {
|
||||
match import_from_nxm(state, uri)
|
||||
.await
|
||||
.wrap_err("Failed to handle NXM URI")
|
||||
{
|
||||
Ok(mod_info) => {
|
||||
event_sink
|
||||
.write()
|
||||
.await
|
||||
.submit_command(
|
||||
ACTION_FINISH_ADD_MOD,
|
||||
SingleUse::new(Arc::new(mod_info)),
|
||||
Target::Auto,
|
||||
)
|
||||
.expect("failed to send command");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
send_error(event_sink.clone(), err).await;
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,12 +10,13 @@ use std::sync::Arc;
|
|||
use clap::parser::ValueSource;
|
||||
use clap::{command, value_parser, Arg};
|
||||
use color_eyre::eyre::{self, Context};
|
||||
use color_eyre::{Report, Result};
|
||||
use color_eyre::{Report, Result, Section};
|
||||
use druid::AppLauncher;
|
||||
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::controller::worker::work_thread;
|
||||
use crate::state::AsyncAction;
|
||||
use crate::state::{AsyncAction, ACTION_HANDLE_NXM};
|
||||
use crate::state::{Delegate, State};
|
||||
use crate::ui::theme;
|
||||
use crate::util::log::LogLevel;
|
||||
|
@ -29,6 +30,37 @@ mod util {
|
|||
}
|
||||
mod ui;
|
||||
|
||||
// As explained in https://docs.rs/interprocess/latest/interprocess/local_socket/enum.NameTypeSupport.html
|
||||
// namespaces are supported on both platforms we care about: Windows and Linux.
|
||||
const IPC_ADDRESS: &str = "@dtmm.sock";
|
||||
|
||||
#[tracing::instrument]
|
||||
fn notify_nxm_download(
|
||||
uri: impl AsRef<str> + std::fmt::Debug,
|
||||
level: Option<LogLevel>,
|
||||
) -> Result<()> {
|
||||
util::log::create_tracing_subscriber(level, None);
|
||||
|
||||
tracing::debug!("Received Uri '{}', sending to main process.", uri.as_ref());
|
||||
|
||||
let mut stream = LocalSocketStream::connect(IPC_ADDRESS)
|
||||
.wrap_err_with(|| format!("Failed to connect to '{}'", IPC_ADDRESS))
|
||||
.suggestion("Make sure the main window is open.")?;
|
||||
|
||||
tracing::debug!("Connected to main process at '{}'", IPC_ADDRESS);
|
||||
|
||||
bincode::serialize_into(&mut stream, uri.as_ref()).wrap_err("Failed to send URI")?;
|
||||
|
||||
// We don't really care what the message is, we just need an acknowledgement.
|
||||
let _: String = bincode::deserialize_from(&mut stream).wrap_err("Failed to receive reply")?;
|
||||
|
||||
tracing::info!(
|
||||
"Notified DTMM with uri '{}'. Check the main window.",
|
||||
uri.as_ref()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
@ -53,15 +85,25 @@ fn main() -> Result<()> {
|
|||
.value_parser(value_parser!(LogLevel))
|
||||
.default_value("info"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("nxm")
|
||||
.help("An `nxm://` URI to download")
|
||||
.required(false),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let level = if matches.value_source("log-level") == Some(ValueSource::DefaultValue) {
|
||||
None
|
||||
} else {
|
||||
matches.get_one::<LogLevel>("log-level").cloned()
|
||||
};
|
||||
util::log::create_tracing_subscriber(log_tx, level);
|
||||
|
||||
if let Some(uri) = matches.get_one::<String>("nxm") {
|
||||
return notify_nxm_download(uri, level).wrap_err("Failed to send NXM Uri to main window.");
|
||||
}
|
||||
|
||||
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
util::log::create_tracing_subscriber(level, Some(log_tx));
|
||||
|
||||
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
|
||||
|
@ -84,6 +126,59 @@ fn main() -> Result<()> {
|
|||
|
||||
let event_sink = launcher.get_external_handle();
|
||||
|
||||
{
|
||||
let span = tracing::info_span!(IPC_ADDRESS, "nxm-socket");
|
||||
let _guard = span.enter();
|
||||
|
||||
let event_sink = event_sink.clone();
|
||||
let server =
|
||||
LocalSocketListener::bind(IPC_ADDRESS).wrap_err("Failed to create IPC listener")?;
|
||||
|
||||
tracing::debug!("IPC server listening on '{}'", IPC_ADDRESS);
|
||||
|
||||
// Drop the guard here, so that we can re-enter the same span in the thread.
|
||||
drop(_guard);
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("nxm-socket".into())
|
||||
.spawn(move || {
|
||||
let _guard = span.enter();
|
||||
|
||||
loop {
|
||||
let res = server.accept().wrap_err_with(|| {
|
||||
format!("IPC server failed to listen on '{}'", IPC_ADDRESS)
|
||||
});
|
||||
|
||||
match res {
|
||||
Ok(mut stream) => {
|
||||
let res = bincode::deserialize_from(&mut stream)
|
||||
.wrap_err("Failed to read message")
|
||||
.and_then(|uri: String| {
|
||||
tracing::trace!(uri, "Received NXM uri");
|
||||
|
||||
event_sink
|
||||
.submit_command(ACTION_HANDLE_NXM, uri, druid::Target::Auto)
|
||||
.wrap_err("Failed to start NXM download")
|
||||
});
|
||||
match res {
|
||||
Ok(()) => {
|
||||
let _ = bincode::serialize_into(&mut stream, "Ok");
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
let _ = bincode::serialize_into(&mut stream, "Error");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to receive client connection: {:?}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.wrap_err("Failed to create thread")?;
|
||||
}
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("work-thread".into())
|
||||
.spawn(move || {
|
||||
|
@ -97,7 +192,7 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
})
|
||||
.wrap_err("Work thread panicked")?;
|
||||
.wrap_err("Failed to create thread")?;
|
||||
|
||||
launcher.launch(State::new()).map_err(Report::new)
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ impl From<NexusMod> for NexusInfo {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Data, Debug, Lens)]
|
||||
#[derive(Clone, Data, Lens)]
|
||||
pub(crate) struct ModInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
|
@ -118,6 +118,39 @@ pub(crate) struct ModInfo {
|
|||
pub nexus: Option<NexusInfo>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ModInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ModInfo")
|
||||
.field("id", &self.id)
|
||||
.field("name", &self.name)
|
||||
.field("summary", &self.summary)
|
||||
.field(
|
||||
"description",
|
||||
&(match &self.description {
|
||||
Some(desc) => format!("Some(String[0..{}])", desc.len()),
|
||||
None => "None".to_string(),
|
||||
}),
|
||||
)
|
||||
.field("categories", &self.categories)
|
||||
.field("author", &self.author)
|
||||
.field(
|
||||
"image",
|
||||
&(match &self.image {
|
||||
Some(image) => format!("Some(ImageBuf[{}x{}])", image.width(), image.height()),
|
||||
None => "None".to_string(),
|
||||
}),
|
||||
)
|
||||
.field("version", &self.version)
|
||||
.field("enabled", &self.enabled)
|
||||
.field("packages", &format!("Vec[0..{}]", self.packages.len()))
|
||||
.field("resources", &self.resources)
|
||||
.field("depends", &self.depends)
|
||||
.field("bundled", &self.bundled)
|
||||
.field("nexus", &self.nexus)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModInfo {
|
||||
pub fn new(
|
||||
cfg: ModConfig,
|
||||
|
|
|
@ -32,6 +32,7 @@ pub(crate) const ACTION_START_RESET_DEPLOYMENT: Selector =
|
|||
pub(crate) const ACTION_FINISH_RESET_DEPLOYMENT: Selector =
|
||||
Selector::new("dtmm.action.finish-reset-deployment");
|
||||
|
||||
pub(crate) const ACTION_HANDLE_NXM: Selector<String> = Selector::new("dtmm.action.handle-nxm");
|
||||
pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action.add-mod");
|
||||
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<Arc<ModInfo>>> =
|
||||
Selector::new("dtmm.action.finish-add-mod");
|
||||
|
@ -97,6 +98,7 @@ pub(crate) enum AsyncAction {
|
|||
CheckUpdates(ActionState),
|
||||
LoadInitial((PathBuf, bool)),
|
||||
Log((ActionState, Vec<u8>)),
|
||||
NxmDownload(ActionState, String),
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AsyncAction {
|
||||
|
@ -116,6 +118,9 @@ impl std::fmt::Debug for AsyncAction {
|
|||
path, is_default
|
||||
),
|
||||
AsyncAction::Log(_) => write!(f, "AsyncAction::Log(_)"),
|
||||
AsyncAction::NxmDownload(_, uri) => {
|
||||
write!(f, "AsyncAction::NxmDownload(_state, {})", uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -250,6 +255,20 @@ impl AppDelegate<State> for Delegate {
|
|||
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_HANDLE_NXM) => {
|
||||
let uri = cmd
|
||||
.get(ACTION_HANDLE_NXM)
|
||||
.expect("command type match but didn't contain the expected value");
|
||||
|
||||
if self
|
||||
.sender
|
||||
.send(AsyncAction::NxmDownload(state.clone().into(), uri.clone()))
|
||||
.is_err()
|
||||
{
|
||||
tracing::error!("Failed to queue action to download NXM mod");
|
||||
}
|
||||
Handled::Yes
|
||||
}
|
||||
cmd if cmd.is(ACTION_ADD_MOD) => {
|
||||
let info = cmd
|
||||
.get(ACTION_ADD_MOD)
|
||||
|
|
|
@ -8,7 +8,7 @@ use tracing_subscriber::layer::SubscriberExt;
|
|||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Clone, Copy, ValueEnum)]
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
|
@ -55,7 +55,7 @@ impl std::io::Write for ChannelWriter {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_tracing_subscriber(tx: UnboundedSender<Vec<u8>>, level: Option<LogLevel>) {
|
||||
pub fn create_tracing_subscriber(level: Option<LogLevel>, tx: Option<UnboundedSender<Vec<u8>>>) {
|
||||
let mut env_layer = if let Some(level) = level {
|
||||
EnvFilter::from(level)
|
||||
} else if cfg!(debug_assertions) {
|
||||
|
@ -78,11 +78,13 @@ pub fn create_tracing_subscriber(tx: UnboundedSender<Vec<u8>>, level: Option<Log
|
|||
|
||||
let stdout_layer = fmt::layer().pretty();
|
||||
|
||||
let channel_layer = fmt::layer()
|
||||
.event_format(dtmt_shared::Formatter)
|
||||
.fmt_fields(debug_fn(dtmt_shared::format_fields))
|
||||
.with_writer(move || ChannelWriter::new(tx.clone()))
|
||||
.with_filter(FilterFn::new(dtmt_shared::filter_fields));
|
||||
let channel_layer = tx.map(|tx| {
|
||||
fmt::layer()
|
||||
.event_format(dtmt_shared::Formatter)
|
||||
.fmt_fields(debug_fn(dtmt_shared::format_fields))
|
||||
.with_writer(move || ChannelWriter::new(tx.clone()))
|
||||
.with_filter(FilterFn::new(dtmt_shared::filter_fields))
|
||||
});
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_layer)
|
||||
|
|
|
@ -136,6 +136,13 @@ impl Api {
|
|||
.map_err(From::from)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_file_by_id(&self, mod_id: u64, file_id: u64) -> Result<File> {
|
||||
let url = BASE_URL_GAME.join(&format!("mods/{mod_id}/files/{file_id}.json"))?;
|
||||
let req = self.client.get(url);
|
||||
self.send(req).await
|
||||
}
|
||||
|
||||
pub fn parse_file_name<S: AsRef<str>>(
|
||||
name: S,
|
||||
) -> Option<(String, u64, String, OffsetDateTime)> {
|
||||
|
@ -174,7 +181,7 @@ impl Api {
|
|||
self.send(req).await
|
||||
}
|
||||
|
||||
pub async fn handle_nxm(&self, url: Url) -> Result<(Mod, Vec<u8>)> {
|
||||
pub async fn handle_nxm(&self, url: Url) -> Result<(Mod, File, Vec<u8>)> {
|
||||
let nxm = Self::parse_nxm(url.clone())?;
|
||||
|
||||
let user = self.user_validate().await?;
|
||||
|
@ -183,8 +190,9 @@ impl Api {
|
|||
return Err(Error::InvalidNXM("user_id mismtach", url));
|
||||
}
|
||||
|
||||
let (mod_data, download_info) = futures::try_join!(
|
||||
let (mod_data, file_info, download_info) = futures::try_join!(
|
||||
self.mods_id(nxm.mod_id),
|
||||
self.get_file_by_id(nxm.mod_id, nxm.file_id),
|
||||
self.mods_download_link(nxm.mod_id, nxm.file_id, nxm.key, nxm.expires)
|
||||
)?;
|
||||
|
||||
|
@ -195,7 +203,7 @@ impl Api {
|
|||
let req = self.client.get(download_url);
|
||||
let data = req.send().await?.bytes().await?;
|
||||
|
||||
Ok((mod_data, data.to_vec()))
|
||||
Ok((mod_data, file_info, data.to_vec()))
|
||||
}
|
||||
|
||||
pub fn parse_nxm(nxm: Url) -> Result<Nxm> {
|
||||
|
@ -204,17 +212,20 @@ impl Api {
|
|||
}
|
||||
|
||||
// Now it makes sense, why Nexus calls this field `game_domain_name`, when it's just
|
||||
// another path segmentin the regular API calls.
|
||||
// another path segment in the regular API calls.
|
||||
if nxm.host_str() != Some(GAME_ID) {
|
||||
return Err(Error::InvalidNXM("Invalid game domain name", nxm));
|
||||
}
|
||||
|
||||
let Some(mut segments) = nxm.path_segments() else {
|
||||
return Err(Error::InvalidNXM("Cannot be a base", nxm));
|
||||
return Err(Error::InvalidNXM("Missing path segments", nxm));
|
||||
};
|
||||
|
||||
if segments.next() != Some("mods") {
|
||||
return Err(Error::InvalidNXM("Unexpected path segment", nxm));
|
||||
return Err(Error::InvalidNXM(
|
||||
"Unexpected path segment, expected 'mods'",
|
||||
nxm,
|
||||
));
|
||||
}
|
||||
|
||||
let Some(mod_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
||||
|
@ -222,7 +233,10 @@ impl Api {
|
|||
};
|
||||
|
||||
if segments.next() != Some("files") {
|
||||
return Err(Error::InvalidNXM("Unexpected path segment", nxm));
|
||||
return Err(Error::InvalidNXM(
|
||||
"Unexpected path segment, expected 'files'",
|
||||
nxm,
|
||||
));
|
||||
}
|
||||
|
||||
let Some(file_id) = segments.next().and_then(|id| id.parse().ok()) else {
|
||||
|
@ -237,7 +251,7 @@ impl Api {
|
|||
}
|
||||
|
||||
let Some(key) = query.get("key") else {
|
||||
return Err(Error::InvalidNXM("Missing 'key'", nxm));
|
||||
return Err(Error::InvalidNXM("Missing query field 'key'", nxm));
|
||||
};
|
||||
|
||||
let expires = query
|
||||
|
@ -245,12 +259,12 @@ impl Api {
|
|||
.and_then(|expires| expires.parse().ok())
|
||||
.and_then(|expires| OffsetDateTime::from_unix_timestamp(expires).ok());
|
||||
let Some(expires) = expires else {
|
||||
return Err(Error::InvalidNXM("Missing 'expires'", nxm));
|
||||
return Err(Error::InvalidNXM("Missing query field 'expires'", nxm));
|
||||
};
|
||||
|
||||
let user_id = query.get("user_id").and_then(|id| id.parse().ok());
|
||||
let Some(user_id) = user_id else {
|
||||
return Err(Error::InvalidNXM("Missing 'user_id'", nxm));
|
||||
return Err(Error::InvalidNXM("Missing query field 'user_id'", nxm));
|
||||
};
|
||||
|
||||
Ok(Nxm {
|
||||
|
|
Loading…
Add table
Reference in a new issue