use std::ffi::OsStr;
use std::path::Path;
use mime::{APPLICATION_OCTET_STREAM, IMAGE_STAR};
use url::Url;
use crate::comments::Comments;
use crate::file_source::FileSource;
use crate::files::{FileRef, Files};
use crate::metadata::Metadata;
use crate::prelude::*;
use crate::teams::Members;
use crate::types::id::{FileId, GameId, ModId};
pub use crate::types::mods::{
CommunityOptions, Dependency, Event, EventType, Image, MaturityOption, Media, Mod, Platform,
Popularity, Ratings, Statistics, Tag, Visibility,
};
pub use crate::types::Logo;
pub use crate::types::Status;
#[derive(Clone)]
pub struct Mods {
modio: Modio,
game: GameId,
}
impl Mods {
pub(crate) fn new(modio: Modio, game: GameId) -> Self {
Self { modio, game }
}
pub fn search(&self, filter: Filter) -> Query<Mod> {
let route = Route::GetMods { game_id: self.game };
Query::new(self.modio.clone(), route, filter)
}
pub fn get(&self, id: ModId) -> ModRef {
ModRef::new(self.modio.clone(), self.game, id)
}
#[allow(clippy::should_implement_trait)]
pub async fn add(self, options: AddModOptions) -> Result<Mod> {
let route = Route::AddMod { game_id: self.game };
self.modio
.request(route)
.multipart(Form::from(options))
.send()
.await
}
pub fn statistics(self, filter: Filter) -> Query<Statistics> {
let route = Route::GetModsStats { game_id: self.game };
Query::new(self.modio, route, filter)
}
pub fn events(self, filter: Filter) -> Query<Event> {
let route = Route::GetModsEvents { game_id: self.game };
Query::new(self.modio, route, filter)
}
}
#[derive(Clone)]
pub struct ModRef {
modio: Modio,
game: GameId,
id: ModId,
}
impl ModRef {
pub(crate) fn new(modio: Modio, game: GameId, id: ModId) -> Self {
Self { modio, game, id }
}
pub async fn get(self) -> Result<Mod> {
let route = Route::GetMod {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).send().await
}
pub fn files(&self) -> Files {
Files::new(self.modio.clone(), self.game, self.id)
}
pub fn file(&self, id: FileId) -> FileRef {
FileRef::new(self.modio.clone(), self.game, self.id, id)
}
pub fn metadata(&self) -> Metadata {
Metadata::new(self.modio.clone(), self.game, self.id)
}
pub fn tags(&self) -> Tags {
Tags::new(self.modio.clone(), self.game, self.id)
}
pub fn comments(&self) -> Comments {
Comments::new(self.modio.clone(), self.game, self.id)
}
pub fn dependencies(&self) -> Dependencies {
Dependencies::new(self.modio.clone(), self.game, self.id)
}
pub async fn statistics(self) -> Result<Statistics> {
let route = Route::GetModStats {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).send().await
}
pub fn events(self, filter: Filter) -> Query<Event> {
let route = Route::GetModEvents {
game_id: self.game,
mod_id: self.id,
};
Query::new(self.modio, route, filter)
}
pub fn members(&self) -> Members {
Members::new(self.modio.clone(), self.game, self.id)
}
pub async fn edit(self, options: EditModOptions) -> Result<Editing<Mod>> {
let route = Route::EditMod {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).form(&options).send().await
}
pub async fn delete(self) -> Result<()> {
let route = Route::DeleteMod {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).send().await
}
pub async fn add_media(self, options: AddMediaOptions) -> Result<()> {
let route = Route::AddModMedia {
game_id: self.game,
mod_id: self.id,
};
self.modio
.request(route)
.multipart(Form::from(options))
.send::<Message>()
.await?;
Ok(())
}
pub async fn delete_media(self, options: DeleteMediaOptions) -> Result<Deletion> {
let route = Route::DeleteModMedia {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).form(&options).send().await
}
pub async fn reorder_media(self, options: ReorderMediaOptions) -> Result<()> {
let route = Route::ReorderModMedia {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).form(&options).send().await
}
pub async fn rate(self, rating: Rating) -> Result<()> {
let route = Route::RateMod {
game_id: self.game,
mod_id: self.id,
};
self.modio
.request(route)
.form(&rating)
.send::<Message>()
.await
.map(|_| ())
.or_else(|err| match (err.status(), err.error_ref()) {
(Some(StatusCode::BAD_REQUEST), Some(15028 | 15043)) => Ok(()),
_ => Err(err),
})
}
pub async fn subscribe(self) -> Result<()> {
let route = Route::SubscribeToMod {
game_id: self.game,
mod_id: self.id,
};
self.modio
.request(route)
.send::<Mod>()
.await
.map(|_| ())
.or_else(|err| match (err.status(), err.error_ref()) {
(Some(StatusCode::BAD_REQUEST), Some(15004)) => Ok(()),
_ => Err(err),
})
}
pub async fn unsubscribe(self) -> Result<()> {
let route = Route::UnsubscribeFromMod {
game_id: self.game,
mod_id: self.id,
};
self.modio.request(route).send().await.or_else(|err| {
match (err.status(), err.error_ref()) {
(Some(StatusCode::BAD_REQUEST), Some(15005)) => Ok(()),
_ => Err(err),
}
})
}
}
#[derive(Clone)]
pub struct Dependencies {
modio: Modio,
game_id: GameId,
mod_id: ModId,
}
impl Dependencies {
fn new(modio: Modio, game_id: GameId, mod_id: ModId) -> Self {
Self {
modio,
game_id,
mod_id,
}
}
pub async fn list(self) -> Result<Vec<Dependency>> {
let route = Route::GetModDependencies {
game_id: self.game_id,
mod_id: self.mod_id,
};
Query::new(self.modio, route, Filter::default())
.collect()
.await
}
#[allow(clippy::iter_not_returning_iterator)]
pub async fn iter(self) -> Result<impl Stream<Item = Result<Dependency>>> {
let route = Route::GetModDependencies {
game_id: self.game_id,
mod_id: self.mod_id,
};
let filter = Filter::default();
Query::new(self.modio, route, filter).iter().await
}
#[allow(clippy::should_implement_trait)]
pub async fn add(self, options: EditDependenciesOptions) -> Result<()> {
let route = Route::AddModDependencies {
game_id: self.game_id,
mod_id: self.mod_id,
};
self.modio
.request(route)
.form(&options)
.send::<Message>()
.await?;
Ok(())
}
pub async fn delete(self, options: EditDependenciesOptions) -> Result<Deletion> {
let route = Route::DeleteModDependencies {
game_id: self.game_id,
mod_id: self.mod_id,
};
self.modio.request(route).form(&options).send().await
}
}
#[derive(Clone)]
pub struct Tags {
modio: Modio,
game_id: GameId,
mod_id: ModId,
}
impl Tags {
fn new(modio: Modio, game_id: GameId, mod_id: ModId) -> Self {
Self {
modio,
game_id,
mod_id,
}
}
pub async fn list(self) -> Result<Vec<Tag>> {
let route = Route::GetModTags {
game_id: self.game_id,
mod_id: self.mod_id,
};
Query::new(self.modio, route, Filter::default())
.collect()
.await
}
#[allow(clippy::iter_not_returning_iterator)]
pub async fn iter(self) -> Result<impl Stream<Item = Result<Tag>>> {
let route = Route::GetModTags {
game_id: self.game_id,
mod_id: self.mod_id,
};
let filter = Filter::default();
Query::new(self.modio, route, filter).iter().await
}
#[allow(clippy::should_implement_trait)]
pub async fn add(self, options: EditTagsOptions) -> Result<()> {
let route = Route::AddModTags {
game_id: self.game_id,
mod_id: self.mod_id,
};
self.modio
.request(route)
.form(&options)
.send::<Message>()
.await?;
Ok(())
}
pub async fn delete(self, options: EditTagsOptions) -> Result<Deletion> {
let route = Route::DeleteModTags {
game_id: self.game_id,
mod_id: self.mod_id,
};
self.modio.request(route).form(&options).send().await
}
}
#[rustfmt::skip]
pub mod filters {
#[doc(inline)]
pub use crate::filter::prelude::Fulltext;
#[doc(inline)]
pub use crate::filter::prelude::Id;
#[doc(inline)]
pub use crate::filter::prelude::Name;
#[doc(inline)]
pub use crate::filter::prelude::NameId;
#[doc(inline)]
pub use crate::filter::prelude::Status;
#[doc(inline)]
pub use crate::filter::prelude::DateAdded;
#[doc(inline)]
pub use crate::filter::prelude::DateUpdated;
#[doc(inline)]
pub use crate::filter::prelude::DateLive;
#[doc(inline)]
pub use crate::filter::prelude::SubmittedBy;
filter!(GameId, GAME_ID, "game_id", Eq, NotEq, In, Cmp, OrderBy);
filter!(Visible, VISIBLE, "visible", Eq);
filter!(MaturityOption, MATURITY_OPTION, "maturity_option", Eq, Cmp, Bit);
filter!(Summary, SUMMARY, "summary", Like);
filter!(Description, DESCRIPTION, "description", Like);
filter!(Homepage, HOMEPAGE, "homepage_url", Eq, NotEq, Like, In);
filter!(Modfile, MODFILE, "modfile", Eq, NotEq, In, Cmp);
filter!(MetadataBlob, METADATA_BLOB, "metadata_blob", Eq, NotEq, Like);
filter!(MetadataKVP, METADATA_KVP, "metadata_kvp", Eq, NotEq, Like);
filter!(Tags, TAGS, "tags", Eq, NotEq, Like, In);
filter!(Downloads, DOWNLOADS, "downloads", OrderBy);
filter!(Popular, POPULAR, "popular", OrderBy);
filter!(Ratings, RATINGS, "ratings", OrderBy);
filter!(Subscribers, SUBSCRIBERS, "subscribers", OrderBy);
pub mod events {
#[doc(inline)]
pub use crate::filter::prelude::Id;
#[doc(inline)]
pub use crate::filter::prelude::ModId;
#[doc(inline)]
pub use crate::filter::prelude::DateAdded;
filter!(UserId, USER_ID, "user_id", Eq, NotEq, In, Cmp, OrderBy);
filter!(EventType, EVENT_TYPE, "event_type", Eq, NotEq, In, OrderBy);
}
pub mod stats {
#[doc(inline)]
pub use crate::filter::prelude::ModId;
filter!(Popularity, POPULARITY, "popularity_rank_position", Eq, NotEq, In, Cmp, OrderBy);
filter!(Downloads, DOWNLOADS, "downloads_total", Eq, NotEq, In, Cmp, OrderBy);
filter!(Subscribers, SUBSCRIBERS, "subscribers_total", Eq, NotEq, In, Cmp, OrderBy);
filter!(RatingsPositive, RATINGS_POSITIVE, "ratings_positive", Eq, NotEq, In, Cmp, OrderBy);
filter!(RatingsNegative, RATINGS_NEGATIVE, "ratings_negative", Eq, NotEq, In, Cmp, OrderBy);
}
}
#[derive(Clone, Copy)]
pub enum Rating {
Positive,
Negative,
None,
}
#[doc(hidden)]
impl serde::ser::Serialize for Rating {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(1))?;
match self {
Rating::Negative => map.serialize_entry("rating", "-1")?,
Rating::Positive => map.serialize_entry("rating", "1")?,
Rating::None => map.serialize_entry("rating", "0")?,
}
map.end()
}
}
pub struct AddModOptions {
visible: Option<Visibility>,
logo: FileSource,
name: String,
name_id: Option<String>,
summary: String,
description: Option<String>,
homepage_url: Option<Url>,
stock: Option<u32>,
maturity_option: Option<MaturityOption>,
metadata_blob: Option<String>,
tags: Option<Vec<String>>,
}
impl AddModOptions {
pub fn new<T, P>(name: T, logo: P, summary: T) -> AddModOptions
where
T: Into<String>,
P: AsRef<Path>,
{
let filename = logo
.as_ref()
.file_name()
.and_then(OsStr::to_str)
.map_or_else(String::new, ToString::to_string);
let logo = FileSource::new_from_file(logo, filename, IMAGE_STAR);
AddModOptions {
name: name.into(),
logo,
summary: summary.into(),
visible: None,
name_id: None,
description: None,
homepage_url: None,
stock: None,
maturity_option: None,
metadata_blob: None,
tags: None,
}
}
#[must_use]
pub fn visible(self, v: bool) -> Self {
Self {
visible: if v {
Some(Visibility::PUBLIC)
} else {
Some(Visibility::HIDDEN)
},
..self
}
}
option!(name_id);
option!(description);
option!(homepage_url: Url);
option!(stock: u32);
option!(maturity_option: MaturityOption);
option!(metadata_blob);
#[must_use]
pub fn tags(self, tags: &[String]) -> Self {
Self {
tags: Some(tags.to_vec()),
..self
}
}
}
#[doc(hidden)]
impl From<AddModOptions> for Form {
fn from(opts: AddModOptions) -> Form {
let mut form = Form::new();
form = form.text("name", opts.name).text("summary", opts.summary);
if let Some(visible) = opts.visible {
form = form.text("visible", visible.to_string());
}
if let Some(name_id) = opts.name_id {
form = form.text("name_id", name_id);
}
if let Some(desc) = opts.description {
form = form.text("description", desc);
}
if let Some(url) = opts.homepage_url {
form = form.text("homepage_url", url.to_string());
}
if let Some(stock) = opts.stock {
form = form.text("stock", stock.to_string());
}
if let Some(maturity_option) = opts.maturity_option {
form = form.text("maturity_option", maturity_option.to_string());
}
if let Some(metadata_blob) = opts.metadata_blob {
form = form.text("metadata_blob", metadata_blob);
}
if let Some(tags) = opts.tags {
for tag in tags {
form = form.text("tags[]", tag);
}
}
form.part("logo", opts.logo.into())
}
}
#[derive(Debug, Default)]
pub struct EditModOptions {
params: std::collections::BTreeMap<&'static str, String>,
}
impl EditModOptions {
option!(status: Status >> "status");
#[must_use]
pub fn visible(self, v: bool) -> Self {
let value = if v {
Visibility::PUBLIC
} else {
Visibility::HIDDEN
};
let mut params = self.params;
params.insert("visible", value.to_string());
Self { params }
}
option!(visibility: Visibility >> "visible");
option!(name >> "name");
option!(name_id >> "name_id");
option!(summary >> "summary");
option!(description >> "description");
option!(homepage_url: Url >> "homepage_url");
option!(stock >> "stock");
option!(maturity_option: MaturityOption >> "maturity_option");
option!(metadata_blob >> "metadata_blob");
}
impl_serialize_params!(EditModOptions >> params);
pub struct EditDependenciesOptions {
dependencies: Vec<ModId>,
}
impl EditDependenciesOptions {
pub fn new(dependencies: &[ModId]) -> Self {
Self {
dependencies: dependencies.to_vec(),
}
}
pub fn one(dependency: ModId) -> Self {
Self {
dependencies: vec![dependency],
}
}
}
#[doc(hidden)]
impl serde::ser::Serialize for EditDependenciesOptions {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.dependencies.len()))?;
for d in &self.dependencies {
map.serialize_entry("dependencies[]", d)?;
}
map.end()
}
}
pub struct EditTagsOptions {
tags: Vec<String>,
}
impl EditTagsOptions {
pub fn new(tags: &[String]) -> Self {
Self {
tags: tags.to_vec(),
}
}
}
#[doc(hidden)]
impl serde::ser::Serialize for EditTagsOptions {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.tags.len()))?;
for t in &self.tags {
map.serialize_entry("tags[]", t)?;
}
map.end()
}
}
#[derive(Default)]
pub struct AddMediaOptions {
logo: Option<FileSource>,
images_zip: Option<FileSource>,
images: Option<Vec<FileSource>>,
youtube: Option<Vec<String>>,
sketchfab: Option<Vec<String>>,
}
impl AddMediaOptions {
#[must_use]
pub fn logo<P: AsRef<Path>>(self, logo: P) -> Self {
let logo = logo.as_ref();
let filename = logo
.file_name()
.and_then(OsStr::to_str)
.map_or_else(String::new, ToString::to_string);
Self {
logo: Some(FileSource::new_from_file(logo, filename, IMAGE_STAR)),
..self
}
}
#[must_use]
pub fn images_zip<P: AsRef<Path>>(self, images: P) -> Self {
Self {
images_zip: Some(FileSource::new_from_file(
images,
"images.zip".into(),
APPLICATION_OCTET_STREAM,
)),
..self
}
}
#[must_use]
pub fn images<P: AsRef<Path>>(self, images: &[P]) -> Self {
Self {
images: Some(
images
.iter()
.map(|p| {
let file = p.as_ref();
let filename = file
.file_name()
.and_then(OsStr::to_str)
.map_or_else(String::new, ToString::to_string);
FileSource::new_from_file(file, filename, IMAGE_STAR)
})
.collect(),
),
..self
}
}
#[must_use]
pub fn youtube(self, urls: &[String]) -> Self {
Self {
youtube: Some(urls.to_vec()),
..self
}
}
#[must_use]
pub fn sketchfab(self, urls: &[String]) -> Self {
Self {
sketchfab: Some(urls.to_vec()),
..self
}
}
}
#[doc(hidden)]
impl From<AddMediaOptions> for Form {
fn from(opts: AddMediaOptions) -> Form {
let mut form = Form::new();
if let Some(logo) = opts.logo {
form = form.part("logo", logo.into());
}
if let Some(zip) = opts.images_zip {
form = form.part("images", zip.into());
}
if let Some(images) = opts.images {
for (i, image) in images.into_iter().enumerate() {
form = form.part(format!("image{i}"), image.into());
}
}
if let Some(youtube) = opts.youtube {
for url in youtube {
form = form.text("youtube[]", url);
}
}
if let Some(sketchfab) = opts.sketchfab {
for url in sketchfab {
form = form.text("sketchfab[]", url);
}
}
form
}
}
#[derive(Default)]
pub struct DeleteMediaOptions {
images: Option<Vec<String>>,
youtube: Option<Vec<String>>,
sketchfab: Option<Vec<String>>,
}
impl DeleteMediaOptions {
#[must_use]
pub fn images(self, images: &[String]) -> Self {
Self {
images: Some(images.to_vec()),
..self
}
}
#[must_use]
pub fn youtube(self, urls: &[String]) -> Self {
Self {
youtube: Some(urls.to_vec()),
..self
}
}
#[must_use]
pub fn sketchfab(self, urls: &[String]) -> Self {
Self {
sketchfab: Some(urls.to_vec()),
..self
}
}
}
#[doc(hidden)]
impl serde::ser::Serialize for DeleteMediaOptions {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeMap;
let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
+ self.youtube.as_ref().map(Vec::len).unwrap_or_default()
+ self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
let mut map = serializer.serialize_map(Some(len))?;
if let Some(ref images) = self.images {
for e in images {
map.serialize_entry("images[]", e)?;
}
}
if let Some(ref urls) = self.youtube {
for e in urls {
map.serialize_entry("youtube[]", e)?;
}
}
if let Some(ref urls) = self.sketchfab {
for e in urls {
map.serialize_entry("sketchfab[]", e)?;
}
}
map.end()
}
}
#[derive(Default)]
pub struct ReorderMediaOptions {
images: Option<Vec<String>>,
youtube: Option<Vec<String>>,
sketchfab: Option<Vec<String>>,
}
impl ReorderMediaOptions {
#[must_use]
pub fn images(self, images: &[String]) -> Self {
Self {
images: Some(images.to_vec()),
..self
}
}
#[must_use]
pub fn youtube(self, urls: &[String]) -> Self {
Self {
youtube: Some(urls.to_vec()),
..self
}
}
#[must_use]
pub fn sketchfab(self, urls: &[String]) -> Self {
Self {
sketchfab: Some(urls.to_vec()),
..self
}
}
}
#[doc(hidden)]
impl serde::ser::Serialize for ReorderMediaOptions {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeMap;
let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
+ self.youtube.as_ref().map(Vec::len).unwrap_or_default()
+ self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
let mut map = serializer.serialize_map(Some(len))?;
if let Some(ref images) = self.images {
for e in images {
map.serialize_entry("images[]", e)?;
}
}
if let Some(ref urls) = self.youtube {
for e in urls {
map.serialize_entry("youtube[]", e)?;
}
}
if let Some(ref urls) = self.sketchfab {
for e in urls {
map.serialize_entry("sketchfab[]", e)?;
}
}
map.end()
}
}