add createmodlist
This commit is contained in:
parent
3321ae370e
commit
f4e2694e43
11 changed files with 383 additions and 1 deletions
|
@ -19,7 +19,14 @@ pub struct Manifest {
|
|||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct Meta {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub contributors: Vec<Contributor>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub website_url: Option<Url>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
|
@ -60,6 +67,8 @@ pub struct Relation {
|
|||
pub file: Option<File>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub versions: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
#[serde(rename = "type")]
|
||||
pub relation_type: RelationType,
|
||||
pub options: HashSet<FileOpt>,
|
||||
|
|
|
@ -22,6 +22,7 @@ reqwest = { version = "0.11.4", features = ["stream"] }
|
|||
serde = { version = "1.0.129", features = ["derive"] }
|
||||
serde_json = "1.0.67"
|
||||
structopt = "0.3.22"
|
||||
tera = "1.12.1"
|
||||
thiserror = "1.0.28"
|
||||
tokio = { version = "1.10.1", features = ["rt-multi-thread", "macros", "fs"] }
|
||||
toml = "0.5.8"
|
||||
|
|
121
mpt/assets/modlist.html.tera
Normal file
121
mpt/assets/modlist.html.tera
Normal file
|
@ -0,0 +1,121 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Mod List</title>
|
||||
<style type="text/css" media="screen">
|
||||
a:link {
|
||||
color: #bd93f9;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #ff79c6;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #282a36;
|
||||
color: #f8f8f2;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
td {
|
||||
border: #6272a4 3px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#header div {
|
||||
margin-right: 10px;
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header">
|
||||
<div>
|
||||
<p><b>Name</b></p>
|
||||
{% if pack_meta.website_url %}
|
||||
<a href="{{ pack_meta.website_url }}" target="_blank">
|
||||
<p>{{ pack_meta.website_url }}</p>
|
||||
</a>
|
||||
{% else %}
|
||||
<p>{{ pack_meta.name }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p><b>Contributors</b></p>
|
||||
<ul>
|
||||
{% for contributor in pack_meta.contributors %}
|
||||
<li>{{ contributor.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if pack_meta.description %}
|
||||
<div>
|
||||
<p><b>Description</b></p>
|
||||
<p>{{ pack_meta.description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td><b>Name</b></td>
|
||||
<td><b>Contributors</b></td>
|
||||
<td><b>Description</b></td>
|
||||
</tr>
|
||||
|
||||
{% for m in metas %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if m.icon_url %}
|
||||
{% if m.website_url %}
|
||||
<a href="{{ m.website_url }}" target="_blank"><img class="img" src="{{ m.icon_url }}"></a>
|
||||
{% else %}
|
||||
<img class="img" src="{{ m.icon_url }}">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a{% if m.website_url %} href="{{ m.website_url }}" target="_blank" {% endif %}>
|
||||
{{ m.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for cont in m.contributors %}
|
||||
<li>{{ cont.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>
|
||||
{% if m.description %}
|
||||
<p>{{ m.description }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
182
mpt/src/commands/createmodlist.rs
Normal file
182
mpt/src/commands/createmodlist.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use crate::{config::Config, util, util::CliStyle};
|
||||
use addonscript::manifest::{
|
||||
Contributor,
|
||||
File,
|
||||
Manifest,
|
||||
Meta,
|
||||
Relation,
|
||||
Repository,
|
||||
RepositoryType,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use crossterm::style::Stylize;
|
||||
use futures::{stream, StreamExt};
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Arc};
|
||||
use tera::Tera;
|
||||
use twitch::api::AddonInfoResponse;
|
||||
|
||||
const TEMPLATE: &str = include_str!("../../assets/modlist.html.tera");
|
||||
|
||||
pub async fn run(
|
||||
(config, mut manifest): (Config, Manifest),
|
||||
outfile: PathBuf,
|
||||
) -> anyhow::Result<()> {
|
||||
let version = manifest
|
||||
.versions
|
||||
.pop()
|
||||
.context("Manifest has no versions!")?;
|
||||
let repos = util::repo_map(manifest.repositories);
|
||||
|
||||
let mut tera = Tera::default();
|
||||
tera.add_raw_template("modlist", TEMPLATE)?;
|
||||
|
||||
let http = Arc::new(Client::new());
|
||||
|
||||
let len = version.relations.len();
|
||||
|
||||
let mut futures = stream::iter(version.relations.into_iter())
|
||||
.map(|rel| get_meta(Arc::clone(&http), rel, &repos))
|
||||
.buffer_unordered(config.downloads.max_threads);
|
||||
|
||||
let mut metas = vec![];
|
||||
let pb = ProgressBar::new(len as u64)
|
||||
.with_style(util::progress_style())
|
||||
.with_prefix("Resolving metadata");
|
||||
while let Some(meta) = futures.next().await {
|
||||
pb.inc(1);
|
||||
let meta = meta?;
|
||||
pb.println(format!(
|
||||
"{} {}",
|
||||
"Got meta for".green(),
|
||||
AsRef::<str>::as_ref(&meta.name).cyan().bold()
|
||||
));
|
||||
metas.push(meta);
|
||||
}
|
||||
pb.finish();
|
||||
|
||||
metas.sort_by_key(|m| m.name.to_ascii_lowercase());
|
||||
|
||||
println!("{}", "Rendering modlist.".info());
|
||||
|
||||
let rendered = tera.render(
|
||||
"modlist",
|
||||
&tera::Context::from_serialize(&ModListContent {
|
||||
metas,
|
||||
pack_meta: manifest.meta,
|
||||
})?,
|
||||
)?;
|
||||
|
||||
println!("{}", "Writing file.".info());
|
||||
if let Some(parent) = outfile.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
tokio::fs::write(outfile, rendered).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_meta(
|
||||
http: Arc<Client>,
|
||||
rel: Relation,
|
||||
repos: &HashMap<String, Repository>,
|
||||
) -> anyhow::Result<Meta> {
|
||||
if let Some(meta) = rel.meta {
|
||||
return Ok(meta);
|
||||
}
|
||||
|
||||
if rel.file.is_none() {
|
||||
return Ok(Meta {
|
||||
name: rel.id.clone(),
|
||||
contributors: vec![],
|
||||
description: rel.versions.map(|v| format!("version {}", v)),
|
||||
icon_url: None,
|
||||
website_url: None,
|
||||
});
|
||||
}
|
||||
|
||||
let file = rel.file.unwrap();
|
||||
|
||||
match file {
|
||||
File::Link { link, id, .. } => {
|
||||
Ok(Meta {
|
||||
name: if let Some(id) = id {
|
||||
id
|
||||
} else {
|
||||
util::link_file_name(&link)?
|
||||
},
|
||||
contributors: vec![],
|
||||
// this is fine, since if a link relation is used you should probably add a local
|
||||
// meta object anyways, in which case this code won't be reached.
|
||||
description: Some(format!("{:?}", &link)),
|
||||
icon_url: None,
|
||||
website_url: None,
|
||||
})
|
||||
},
|
||||
File::Maven {
|
||||
repository,
|
||||
artifact,
|
||||
..
|
||||
} => {
|
||||
let repo = repos
|
||||
.get(&repository)
|
||||
.context("File references unknown repository!")?;
|
||||
|
||||
match repo.repo_type {
|
||||
RepositoryType::Maven => Ok(Meta {
|
||||
name: artifact,
|
||||
contributors: vec![],
|
||||
description: None,
|
||||
icon_url: None,
|
||||
website_url: None,
|
||||
}),
|
||||
RepositoryType::Curseforge => {
|
||||
let (p_id, _) = util::parse_curseforge_artifact(&artifact)?;
|
||||
let resp = http
|
||||
.get(format!(
|
||||
"https://addons-ecs.forgesvc.net/api/v2/addon/{}",
|
||||
p_id
|
||||
))
|
||||
.send()
|
||||
.await?
|
||||
.bytes()
|
||||
.await?;
|
||||
|
||||
let resp = serde_json::from_slice::<AddonInfoResponse>(&resp)
|
||||
.context("Failed to deserialize addon info!")?;
|
||||
|
||||
let icon_url = resp.attachments.into_iter().find_map(|a| {
|
||||
if a.is_default {
|
||||
Some(a.url.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Meta {
|
||||
name: resp.name,
|
||||
contributors: resp
|
||||
.authors
|
||||
.into_iter()
|
||||
.map(|a| Contributor {
|
||||
name: a.name,
|
||||
roles: vec![],
|
||||
})
|
||||
.collect(),
|
||||
description: Some(resp.summary),
|
||||
icon_url,
|
||||
website_url: Some(resp.website_url),
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ModListContent {
|
||||
metas: Vec<Meta>,
|
||||
pack_meta: Meta,
|
||||
}
|
|
@ -122,6 +122,7 @@ pub async fn run(
|
|||
mcver = mcversion,
|
||||
forgever = ver,
|
||||
)),
|
||||
meta: None,
|
||||
})
|
||||
} else {
|
||||
execute!(
|
||||
|
@ -166,6 +167,9 @@ pub async fn run(
|
|||
roles: vec!["Owner".to_owned()],
|
||||
name: author_name,
|
||||
}],
|
||||
description: None,
|
||||
icon_url: None,
|
||||
website_url: None,
|
||||
},
|
||||
})?;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod buildtwitch;
|
||||
pub mod clean;
|
||||
pub mod createmodlist;
|
||||
pub mod downloadmods;
|
||||
pub mod init;
|
||||
|
|
|
@ -45,6 +45,18 @@ enum Command {
|
|||
|
||||
#[structopt(about = "Deletes artifacts and temporary files")]
|
||||
Clean,
|
||||
|
||||
#[structopt(
|
||||
name = "createmodlist",
|
||||
about = "Creates a HTML list of the pack's mods."
|
||||
)]
|
||||
CreateModList {
|
||||
#[structopt(
|
||||
default_value = "build/modlist.html",
|
||||
help = "File to write the mod list to"
|
||||
)]
|
||||
outfile: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
@ -67,6 +79,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
},
|
||||
|
||||
Command::Clean => commands::clean::run(util::parse_config().await?).await?,
|
||||
|
||||
Command::CreateModList { outfile } => {
|
||||
commands::createmodlist::run(util::parse_config_and_manifest().await?, outfile).await?
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use addonscript::manifest::{Manifest, Repository};
|
||||
use addonscript::manifest::{link::Link, Manifest, Repository};
|
||||
use anyhow::Context;
|
||||
use crossterm::style::{Attribute, Attributes, Color, ContentStyle, Stylize};
|
||||
use indicatif::ProgressStyle;
|
||||
|
@ -156,6 +156,25 @@ pub fn url_file_name(url: &Url) -> Result<String, UrlFileNameError> {
|
|||
Ok(String::from_utf8(bytes)?)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LinkFileNameError {
|
||||
#[error("Empty path has no file name!")]
|
||||
EmptyPath,
|
||||
#[error("Failed to get file name of URL link: {0}")]
|
||||
Url(#[from] UrlFileNameError),
|
||||
}
|
||||
|
||||
pub fn link_file_name(link: &Link) -> Result<String, LinkFileNameError> {
|
||||
match link {
|
||||
Link::File(path) => Ok(path
|
||||
.file_name()
|
||||
.ok_or(LinkFileNameError::EmptyPath)?
|
||||
.to_string_lossy()
|
||||
.to_string()),
|
||||
Link::Http(url) => Ok(url_file_name(url)?),
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies a directory inclding all files
|
||||
pub async fn copy_dir(from: PathBuf, to: PathBuf) -> io::Result<()> {
|
||||
for file in WalkDir::new(&from) {
|
||||
|
|
|
@ -9,3 +9,4 @@ edition = "2018"
|
|||
addonscript = { path = "../addonscript" }
|
||||
serde = { version = "1.0.130", features = ["derive"] }
|
||||
thiserror = "1.0.28"
|
||||
url = { version = "2.2.2", features = ["serde"] }
|
||||
|
|
27
twitch/src/api.rs
Normal file
27
twitch/src/api.rs
Normal file
|
@ -0,0 +1,27 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
/// API Response for https://addons-ecs.forgesvc.net/api/v2/addon/{addonID}.
|
||||
///
|
||||
/// Only includes fields which are used currently.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddonInfoResponse {
|
||||
pub name: String,
|
||||
pub authors: Vec<Author>,
|
||||
pub attachments: Vec<Attachment>,
|
||||
pub summary: String,
|
||||
pub website_url: Url,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Author {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Attachment {
|
||||
pub is_default: bool,
|
||||
pub url: Url,
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
pub mod api;
|
||||
pub mod manifest;
|
||||
|
|
Loading…
Reference in a new issue