add createmodlist

This commit is contained in:
LordMZTE 2021-09-01 19:24:26 +02:00
parent 3321ae370e
commit f4e2694e43
11 changed files with 383 additions and 1 deletions

View file

@ -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>,

View file

@ -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"

View 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>

View 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,
}

View file

@ -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,
},
})?;

View file

@ -1,4 +1,5 @@
pub mod buildtwitch;
pub mod clean;
pub mod createmodlist;
pub mod downloadmods;
pub mod init;

View file

@ -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(())

View file

@ -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) {

View file

@ -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
View 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,
}

View file

@ -1 +1,2 @@
pub mod api;
pub mod manifest;