modio/
mods.rs

1//! Mods Interface
2use std::ffi::OsStr;
3use std::path::Path;
4
5use mime::{APPLICATION_OCTET_STREAM, IMAGE_STAR};
6use url::Url;
7
8use crate::comments::Comments;
9use crate::file_source::FileSource;
10use crate::files::{FileRef, Files};
11use crate::metadata::Metadata;
12use crate::prelude::*;
13use crate::teams::Members;
14use crate::types::id::{FileId, GameId, ModId};
15
16pub use crate::types::mods::{
17    CommunityOptions, CreditOptions, Dependency, Event, EventType, Image, MaturityOption, Media,
18    Mod, Platform, Popularity, Ratings, Statistics, Tag, Visibility,
19};
20pub use crate::types::Logo;
21pub use crate::types::Status;
22
23/// Interface for mods of a game.
24#[derive(Clone)]
25pub struct Mods {
26    modio: Modio,
27    game: GameId,
28}
29
30impl Mods {
31    pub(crate) fn new(modio: Modio, game: GameId) -> Self {
32        Self { modio, game }
33    }
34
35    /// Returns a `Query` interface to retrieve mods.
36    ///
37    /// See [Filters and sorting](filters).
38    pub fn search(&self, filter: Filter) -> Query<Mod> {
39        let route = Route::GetMods { game_id: self.game };
40        Query::new(self.modio.clone(), route, filter)
41    }
42
43    /// Return a reference to a mod.
44    pub fn get(&self, id: ModId) -> ModRef {
45        ModRef::new(self.modio.clone(), self.game, id)
46    }
47
48    /// Add a mod and return the newly created Modio mod object. [required: token]
49    #[allow(clippy::should_implement_trait)]
50    pub async fn add(self, options: AddModOptions) -> Result<Mod> {
51        let route = Route::AddMod { game_id: self.game };
52        self.modio
53            .request(route)
54            .multipart(Form::from(options))
55            .send()
56            .await
57    }
58
59    /// Returns a `Query` interface to retrieve the statistics for all mods of a game.
60    ///
61    /// See [Filters and sorting](filters::stats).
62    pub fn statistics(self, filter: Filter) -> Query<Statistics> {
63        let route = Route::GetModsStats { game_id: self.game };
64        Query::new(self.modio, route, filter)
65    }
66
67    /// Returns a `Query` interface to retrieve the event log of all mods of the game sorted by
68    /// latest event first.
69    ///
70    /// See [Filters and sorting](filters::events).
71    pub fn events(self, filter: Filter) -> Query<Event> {
72        let route = Route::GetModsEvents { game_id: self.game };
73        Query::new(self.modio, route, filter)
74    }
75}
76
77/// Reference interface of a mod.
78#[derive(Clone)]
79pub struct ModRef {
80    modio: Modio,
81    game: GameId,
82    id: ModId,
83}
84
85impl ModRef {
86    pub(crate) fn new(modio: Modio, game: GameId, id: ModId) -> Self {
87        Self { modio, game, id }
88    }
89
90    /// Get a reference to the Modio mod object that this `ModRef` refers to.
91    pub async fn get(self) -> Result<Mod> {
92        let route = Route::GetMod {
93            game_id: self.game,
94            mod_id: self.id,
95        };
96        self.modio.request(route).send().await
97    }
98
99    /// Return a reference to an interface that provides access to the files of a mod.
100    pub fn files(&self) -> Files {
101        Files::new(self.modio.clone(), self.game, self.id)
102    }
103
104    /// Return a reference to a file of a mod.
105    pub fn file(&self, id: FileId) -> FileRef {
106        FileRef::new(self.modio.clone(), self.game, self.id, id)
107    }
108
109    /// Return a reference to an interface to manage metadata key value pairs of a mod.
110    pub fn metadata(&self) -> Metadata {
111        Metadata::new(self.modio.clone(), self.game, self.id)
112    }
113
114    /// Return a reference to an interface to manage the tags of a mod.
115    pub fn tags(&self) -> Tags {
116        Tags::new(self.modio.clone(), self.game, self.id)
117    }
118
119    /// Return a reference to an interface that provides access to the comments of a mod.
120    pub fn comments(&self) -> Comments {
121        Comments::new(self.modio.clone(), self.game, self.id)
122    }
123
124    /// Return a reference to an interface to manage the dependencies of a mod.
125    pub fn dependencies(&self) -> Dependencies {
126        Dependencies::new(self.modio.clone(), self.game, self.id)
127    }
128
129    /// Return the statistics for a mod.
130    pub async fn statistics(self) -> Result<Statistics> {
131        let route = Route::GetModStats {
132            game_id: self.game,
133            mod_id: self.id,
134        };
135        self.modio.request(route).send().await
136    }
137
138    /// Returns a `Query` interface to retrieve the event log for a mod sorted by latest event first.
139    ///
140    /// See [Filters and sorting](filters::events).
141    pub fn events(self, filter: Filter) -> Query<Event> {
142        let route = Route::GetModEvents {
143            game_id: self.game,
144            mod_id: self.id,
145        };
146        Query::new(self.modio, route, filter)
147    }
148
149    /// Return a reference to an interface to manage team members of a mod.
150    pub fn members(&self) -> Members {
151        Members::new(self.modio.clone(), self.game, self.id)
152    }
153
154    /// Edit details for a mod. [required: token]
155    pub async fn edit(self, options: EditModOptions) -> Result<Editing<Mod>> {
156        let route = Route::EditMod {
157            game_id: self.game,
158            mod_id: self.id,
159        };
160        self.modio.request(route).form(&options).send().await
161    }
162
163    /// Delete a mod. [required: token]
164    pub async fn delete(self) -> Result<()> {
165        let route = Route::DeleteMod {
166            game_id: self.game,
167            mod_id: self.id,
168        };
169        self.modio.request(route).send().await
170    }
171
172    /// Add new media to a mod. [required: token]
173    pub async fn add_media(self, options: AddMediaOptions) -> Result<()> {
174        let route = Route::AddModMedia {
175            game_id: self.game,
176            mod_id: self.id,
177        };
178        self.modio
179            .request(route)
180            .multipart(Form::from(options))
181            .send::<Message>()
182            .await?;
183
184        Ok(())
185    }
186
187    /// Delete media from a mod. [required: token]
188    pub async fn delete_media(self, options: DeleteMediaOptions) -> Result<Deletion> {
189        let route = Route::DeleteModMedia {
190            game_id: self.game,
191            mod_id: self.id,
192        };
193        self.modio.request(route).form(&options).send().await
194    }
195
196    /// Reorder images, sketchfab or youtube links from a mod profile. [required: token]
197    pub async fn reorder_media(self, options: ReorderMediaOptions) -> Result<()> {
198        let route = Route::ReorderModMedia {
199            game_id: self.game,
200            mod_id: self.id,
201        };
202        self.modio.request(route).form(&options).send().await
203    }
204
205    /// Submit a positive or negative rating for a mod. [required: token]
206    pub async fn rate(self, rating: Rating) -> Result<()> {
207        let route = Route::RateMod {
208            game_id: self.game,
209            mod_id: self.id,
210        };
211        self.modio
212            .request(route)
213            .form(&rating)
214            .send::<Message>()
215            .await
216            .map(|_| ())
217            .or_else(|err| match (err.status(), err.error_ref()) {
218                (Some(StatusCode::BAD_REQUEST), Some(15028 | 15043)) => Ok(()),
219                _ => Err(err),
220            })
221    }
222
223    /// Subscribe the authenticated user to a mod. [required: token]
224    pub async fn subscribe(self) -> Result<()> {
225        let route = Route::SubscribeToMod {
226            game_id: self.game,
227            mod_id: self.id,
228        };
229        self.modio
230            .request(route)
231            .send::<Mod>()
232            .await
233            .map(|_| ())
234            .or_else(|err| match (err.status(), err.error_ref()) {
235                (Some(StatusCode::BAD_REQUEST), Some(15004)) => Ok(()),
236                _ => Err(err),
237            })
238    }
239
240    /// Unsubscribe the authenticated user from a mod. [required: token]
241    pub async fn unsubscribe(self) -> Result<()> {
242        let route = Route::UnsubscribeFromMod {
243            game_id: self.game,
244            mod_id: self.id,
245        };
246        self.modio.request(route).send().await.or_else(|err| {
247            match (err.status(), err.error_ref()) {
248                (Some(StatusCode::BAD_REQUEST), Some(15005)) => Ok(()),
249                _ => Err(err),
250            }
251        })
252    }
253}
254
255/// Interface for dependencies.
256#[derive(Clone)]
257pub struct Dependencies {
258    modio: Modio,
259    game_id: GameId,
260    mod_id: ModId,
261}
262
263impl Dependencies {
264    fn new(modio: Modio, game_id: GameId, mod_id: ModId) -> Self {
265        Self {
266            modio,
267            game_id,
268            mod_id,
269        }
270    }
271
272    /// List mod dependencies.
273    pub async fn list(self) -> Result<Vec<Dependency>> {
274        let route = Route::GetModDependencies {
275            game_id: self.game_id,
276            mod_id: self.mod_id,
277        };
278        Query::new(self.modio, route, Filter::default())
279            .collect()
280            .await
281    }
282
283    /// Provides a stream over all mod dependencies.
284    #[allow(clippy::iter_not_returning_iterator)]
285    pub async fn iter(self) -> Result<impl Stream<Item = Result<Dependency>>> {
286        let route = Route::GetModDependencies {
287            game_id: self.game_id,
288            mod_id: self.mod_id,
289        };
290        let filter = Filter::default();
291        Query::new(self.modio, route, filter).iter().await
292    }
293
294    /// Add mod dependencies. [required: token]
295    #[allow(clippy::should_implement_trait)]
296    pub async fn add(self, options: EditDependenciesOptions) -> Result<()> {
297        let route = Route::AddModDependencies {
298            game_id: self.game_id,
299            mod_id: self.mod_id,
300        };
301        self.modio
302            .request(route)
303            .form(&options)
304            .send::<Message>()
305            .await?;
306        Ok(())
307    }
308
309    /// Delete mod dependencies. [required: token]
310    pub async fn delete(self, options: EditDependenciesOptions) -> Result<Deletion> {
311        let route = Route::DeleteModDependencies {
312            game_id: self.game_id,
313            mod_id: self.mod_id,
314        };
315        self.modio.request(route).form(&options).send().await
316    }
317}
318
319/// Interface for tags.
320#[derive(Clone)]
321pub struct Tags {
322    modio: Modio,
323    game_id: GameId,
324    mod_id: ModId,
325}
326
327impl Tags {
328    fn new(modio: Modio, game_id: GameId, mod_id: ModId) -> Self {
329        Self {
330            modio,
331            game_id,
332            mod_id,
333        }
334    }
335
336    /// List all mod tags.
337    pub async fn list(self) -> Result<Vec<Tag>> {
338        let route = Route::GetModTags {
339            game_id: self.game_id,
340            mod_id: self.mod_id,
341        };
342        Query::new(self.modio, route, Filter::default())
343            .collect()
344            .await
345    }
346
347    /// Provides a stream over all mod tags.
348    #[allow(clippy::iter_not_returning_iterator)]
349    pub async fn iter(self) -> Result<impl Stream<Item = Result<Tag>>> {
350        let route = Route::GetModTags {
351            game_id: self.game_id,
352            mod_id: self.mod_id,
353        };
354        let filter = Filter::default();
355        Query::new(self.modio, route, filter).iter().await
356    }
357
358    /// Add mod tags. [required: token]
359    #[allow(clippy::should_implement_trait)]
360    pub async fn add(self, options: EditTagsOptions) -> Result<()> {
361        let route = Route::AddModTags {
362            game_id: self.game_id,
363            mod_id: self.mod_id,
364        };
365        self.modio
366            .request(route)
367            .form(&options)
368            .send::<Message>()
369            .await?;
370        Ok(())
371    }
372
373    /// Delete mod tags. [required: token]
374    pub async fn delete(self, options: EditTagsOptions) -> Result<Deletion> {
375        let route = Route::DeleteModTags {
376            game_id: self.game_id,
377            mod_id: self.mod_id,
378        };
379        self.modio.request(route).form(&options).send().await
380    }
381}
382
383/// Mod filters & sorting
384///
385/// # Filters
386/// - `Fulltext`
387/// - `Id`
388/// - `GameId`
389/// - `Status`
390/// - `Visible`
391/// - `SubmittedBy`
392/// - `DateAdded`
393/// - `DateUpdated`
394/// - `DateLive`
395/// - `MaturityOption`
396/// - `Name`
397/// - `NameId`
398/// - `Summary`
399/// - `Description`
400/// - `Homepage`
401/// - `Modfile`
402/// - `MetadataBlob`
403/// - `MetadataKVP`
404/// - `Tags`
405///
406/// # Sorting
407/// - `Id`
408/// - `Name`
409/// - `Downloads`
410/// - `Popular`
411/// - `Ratings`
412/// - `Subscribers`
413///
414/// See the [modio docs](https://docs.mod.io/restapiref/#get-mods) for more information.
415///
416/// By default this returns up to `100` items. you can limit the result by using `limit` and
417/// `offset`.
418///
419/// # Example
420/// ```
421/// use modio::filter::prelude::*;
422/// use modio::mods::filters::Id;
423/// use modio::mods::filters::GameId;
424/// use modio::mods::filters::Tags;
425///
426/// let filter = Id::_in(vec![1, 2]).order_by(Id::desc());
427///
428/// let filter = GameId::eq(6).and(Tags::eq("foobar")).limit(10);
429/// ```
430#[rustfmt::skip]
431pub mod filters {
432    #[doc(inline)]
433    pub use crate::filter::prelude::Fulltext;
434    #[doc(inline)]
435    pub use crate::filter::prelude::Id;
436    #[doc(inline)]
437    pub use crate::filter::prelude::Name;
438    #[doc(inline)]
439    pub use crate::filter::prelude::NameId;
440    #[doc(inline)]
441    pub use crate::filter::prelude::Status;
442    #[doc(inline)]
443    pub use crate::filter::prelude::DateAdded;
444    #[doc(inline)]
445    pub use crate::filter::prelude::DateUpdated;
446    #[doc(inline)]
447    pub use crate::filter::prelude::DateLive;
448    #[doc(inline)]
449    pub use crate::filter::prelude::SubmittedBy;
450
451    filter!(GameId, GAME_ID, "game_id", Eq, NotEq, In, Cmp, OrderBy);
452    filter!(Visible, VISIBLE, "visible", Eq);
453    filter!(MaturityOption, MATURITY_OPTION, "maturity_option", Eq, Cmp, Bit);
454    filter!(Summary, SUMMARY, "summary", Like);
455    filter!(Description, DESCRIPTION, "description", Like);
456    filter!(Homepage, HOMEPAGE, "homepage_url", Eq, NotEq, Like, In);
457    filter!(Modfile, MODFILE, "modfile", Eq, NotEq, In, Cmp);
458    filter!(MetadataBlob, METADATA_BLOB, "metadata_blob", Eq, NotEq, Like);
459    filter!(MetadataKVP, METADATA_KVP, "metadata_kvp", Eq, NotEq, Like);
460    filter!(Tags, TAGS, "tags", Eq, NotEq, Like, In);
461
462    filter!(Downloads, DOWNLOADS, "downloads", OrderBy);
463    filter!(Popular, POPULAR, "popular", OrderBy);
464    filter!(Ratings, RATINGS, "ratings", OrderBy);
465    filter!(Subscribers, SUBSCRIBERS, "subscribers", OrderBy);
466
467    /// Mod event filters and sorting.
468    ///
469    /// # Filters
470    /// - `Id`
471    /// - `ModId`
472    /// - `UserId`
473    /// - `DateAdded`
474    /// - `EventType`
475    ///
476    /// # Sorting
477    /// - `Id`
478    /// - `DateAdded`
479    ///
480    /// See the [modio docs](https://docs.mod.io/restapiref/#events) for more information.
481    ///
482    /// By default this returns up to `100` items. You can limit the result by using `limit` and
483    /// `offset`.
484    ///
485    /// # Example
486    /// ```
487    /// use modio::filter::prelude::*;
488    /// use modio::mods::filters::events::EventType as Filter;
489    /// use modio::mods::EventType;
490    ///
491    /// let filter = Id::gt(1024).and(Filter::eq(EventType::MODFILE_CHANGED));
492    /// ```
493    pub mod events {
494        #[doc(inline)]
495        pub use crate::filter::prelude::Id;
496        #[doc(inline)]
497        pub use crate::filter::prelude::ModId;
498        #[doc(inline)]
499        pub use crate::filter::prelude::DateAdded;
500
501        filter!(UserId, USER_ID, "user_id", Eq, NotEq, In, Cmp, OrderBy);
502        filter!(EventType, EVENT_TYPE, "event_type", Eq, NotEq, In, OrderBy);
503    }
504
505    /// Mod statistics filters & sorting
506    ///
507    /// # Filters
508    /// - `ModId`
509    /// - `Popularity`
510    /// - `Downloads`
511    /// - `Subscribers`
512    /// - `RatingsPositive`
513    /// - `RatingsNegative`
514    ///
515    /// # Sorting
516    /// - `ModId`
517    /// - `Popularity`
518    /// - `Downloads`
519    /// - `Subscribers`
520    /// - `RatingsPositive`
521    /// - `RatingsNegative`
522    ///
523    /// # Example
524    /// ```
525    /// use modio::filter::prelude::*;
526    /// use modio::mods::filters::stats::ModId;
527    /// use modio::mods::filters::stats::Popularity;
528    ///
529    /// let filter = ModId::_in(vec![1, 2]).order_by(Popularity::desc());
530    /// ```
531    pub mod stats {
532        #[doc(inline)]
533        pub use crate::filter::prelude::ModId;
534
535        filter!(Popularity, POPULARITY, "popularity_rank_position", Eq, NotEq, In, Cmp, OrderBy);
536        filter!(Downloads, DOWNLOADS, "downloads_total", Eq, NotEq, In, Cmp, OrderBy);
537        filter!(Subscribers, SUBSCRIBERS, "subscribers_total", Eq, NotEq, In, Cmp, OrderBy);
538        filter!(RatingsPositive, RATINGS_POSITIVE, "ratings_positive", Eq, NotEq, In, Cmp, OrderBy);
539        filter!(RatingsNegative, RATINGS_NEGATIVE, "ratings_negative", Eq, NotEq, In, Cmp, OrderBy);
540    }
541}
542
543#[derive(Clone, Copy)]
544pub enum Rating {
545    Positive,
546    Negative,
547    None,
548}
549
550#[doc(hidden)]
551impl serde::ser::Serialize for Rating {
552    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
553    where
554        S: serde::ser::Serializer,
555    {
556        use serde::ser::SerializeMap;
557
558        let mut map = serializer.serialize_map(Some(1))?;
559        match self {
560            Rating::Negative => map.serialize_entry("rating", "-1")?,
561            Rating::Positive => map.serialize_entry("rating", "1")?,
562            Rating::None => map.serialize_entry("rating", "0")?,
563        }
564        map.end()
565    }
566}
567
568pub struct AddModOptions {
569    visible: Option<Visibility>,
570    logo: FileSource,
571    name: String,
572    name_id: Option<String>,
573    summary: String,
574    description: Option<String>,
575    homepage_url: Option<Url>,
576    stock: Option<u32>,
577    maturity_option: Option<MaturityOption>,
578    community_options: Option<CommunityOptions>,
579    credit_options: Option<CreditOptions>,
580    metadata_blob: Option<String>,
581    tags: Option<Vec<String>>,
582}
583
584impl AddModOptions {
585    pub fn new<T, P>(name: T, logo: P, summary: T) -> AddModOptions
586    where
587        T: Into<String>,
588        P: AsRef<Path>,
589    {
590        let filename = logo
591            .as_ref()
592            .file_name()
593            .and_then(OsStr::to_str)
594            .map_or_else(String::new, ToString::to_string);
595
596        let logo = FileSource::new_from_file(logo, filename, IMAGE_STAR);
597
598        AddModOptions {
599            name: name.into(),
600            logo,
601            summary: summary.into(),
602            visible: None,
603            name_id: None,
604            description: None,
605            homepage_url: None,
606            stock: None,
607            maturity_option: None,
608            community_options: None,
609            credit_options: None,
610            metadata_blob: None,
611            tags: None,
612        }
613    }
614
615    #[must_use]
616    pub fn visible(self, v: bool) -> Self {
617        Self {
618            visible: if v {
619                Some(Visibility::PUBLIC)
620            } else {
621                Some(Visibility::HIDDEN)
622            },
623            ..self
624        }
625    }
626
627    option!(name_id);
628    option!(description);
629    option!(homepage_url: Url);
630    option!(stock: u32);
631    option!(maturity_option: MaturityOption);
632    option!(community_options: CommunityOptions);
633    option!(credit_options: CreditOptions);
634    option!(metadata_blob);
635
636    #[must_use]
637    pub fn tags(self, tags: &[String]) -> Self {
638        Self {
639            tags: Some(tags.to_vec()),
640            ..self
641        }
642    }
643}
644
645#[doc(hidden)]
646impl From<AddModOptions> for Form {
647    fn from(opts: AddModOptions) -> Form {
648        let mut form = Form::new();
649
650        form = form.text("name", opts.name).text("summary", opts.summary);
651
652        if let Some(visible) = opts.visible {
653            form = form.text("visible", visible.to_string());
654        }
655        if let Some(name_id) = opts.name_id {
656            form = form.text("name_id", name_id);
657        }
658        if let Some(desc) = opts.description {
659            form = form.text("description", desc);
660        }
661        if let Some(url) = opts.homepage_url {
662            form = form.text("homepage_url", url.to_string());
663        }
664        if let Some(stock) = opts.stock {
665            form = form.text("stock", stock.to_string());
666        }
667        if let Some(maturity_option) = opts.maturity_option {
668            form = form.text("maturity_option", maturity_option.to_string());
669        }
670        if let Some(community_options) = opts.community_options {
671            form = form.text("community_options", community_options.to_string());
672        }
673        if let Some(credit_options) = opts.credit_options {
674            form = form.text("credit_options", credit_options.to_string());
675        }
676        if let Some(metadata_blob) = opts.metadata_blob {
677            form = form.text("metadata_blob", metadata_blob);
678        }
679        if let Some(tags) = opts.tags {
680            for tag in tags {
681                form = form.text("tags[]", tag);
682            }
683        }
684        form.part("logo", opts.logo.into())
685    }
686}
687
688#[derive(Debug, Default)]
689pub struct EditModOptions {
690    params: std::collections::BTreeMap<&'static str, String>,
691}
692
693impl EditModOptions {
694    option!(status: Status >> "status");
695
696    #[must_use]
697    pub fn visible(self, v: bool) -> Self {
698        let value = if v {
699            Visibility::PUBLIC
700        } else {
701            Visibility::HIDDEN
702        };
703        let mut params = self.params;
704        params.insert("visible", value.to_string());
705        Self { params }
706    }
707
708    option!(visibility: Visibility >> "visible");
709    option!(name >> "name");
710    option!(name_id >> "name_id");
711    option!(summary >> "summary");
712    option!(description >> "description");
713    option!(homepage_url: Url >> "homepage_url");
714    option!(stock >> "stock");
715    option!(maturity_option: MaturityOption >> "maturity_option");
716    option!(community_options: CommunityOptions >> "community_options");
717    option!(credit_options: CreditOptions >> "credit_options");
718    option!(metadata_blob >> "metadata_blob");
719}
720
721impl_serialize_params!(EditModOptions >> params);
722
723pub struct EditDependenciesOptions {
724    dependencies: Vec<ModId>,
725}
726
727impl EditDependenciesOptions {
728    pub fn new(dependencies: &[ModId]) -> Self {
729        Self {
730            dependencies: dependencies.to_vec(),
731        }
732    }
733
734    pub fn one(dependency: ModId) -> Self {
735        Self {
736            dependencies: vec![dependency],
737        }
738    }
739}
740
741#[doc(hidden)]
742impl serde::ser::Serialize for EditDependenciesOptions {
743    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
744    where
745        S: serde::ser::Serializer,
746    {
747        use serde::ser::SerializeMap;
748
749        let mut map = serializer.serialize_map(Some(self.dependencies.len()))?;
750        for d in &self.dependencies {
751            map.serialize_entry("dependencies[]", d)?;
752        }
753        map.end()
754    }
755}
756
757pub struct EditTagsOptions {
758    tags: Vec<String>,
759}
760
761impl EditTagsOptions {
762    pub fn new(tags: &[String]) -> Self {
763        Self {
764            tags: tags.to_vec(),
765        }
766    }
767}
768
769#[doc(hidden)]
770impl serde::ser::Serialize for EditTagsOptions {
771    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
772    where
773        S: serde::ser::Serializer,
774    {
775        use serde::ser::SerializeMap;
776
777        let mut map = serializer.serialize_map(Some(self.tags.len()))?;
778        for t in &self.tags {
779            map.serialize_entry("tags[]", t)?;
780        }
781        map.end()
782    }
783}
784
785#[derive(Default)]
786pub struct AddMediaOptions {
787    sync: Option<bool>,
788    logo: Option<FileSource>,
789    images_zip: Option<FileSource>,
790    images: Option<Vec<FileSource>>,
791    youtube: Option<Vec<String>>,
792    sketchfab: Option<Vec<String>>,
793}
794
795impl AddMediaOptions {
796    #[must_use]
797    pub fn sync(self, value: bool) -> Self {
798        Self {
799            sync: Some(value),
800            ..self
801        }
802    }
803
804    #[must_use]
805    pub fn logo<P: AsRef<Path>>(self, logo: P) -> Self {
806        let logo = logo.as_ref();
807        let filename = logo
808            .file_name()
809            .and_then(OsStr::to_str)
810            .map_or_else(String::new, ToString::to_string);
811
812        Self {
813            logo: Some(FileSource::new_from_file(logo, filename, IMAGE_STAR)),
814            ..self
815        }
816    }
817
818    #[must_use]
819    pub fn images_zip<P: AsRef<Path>>(self, images: P) -> Self {
820        Self {
821            images_zip: Some(FileSource::new_from_file(
822                images,
823                "images.zip".into(),
824                APPLICATION_OCTET_STREAM,
825            )),
826            ..self
827        }
828    }
829
830    #[must_use]
831    pub fn images<P: AsRef<Path>>(self, images: &[P]) -> Self {
832        Self {
833            images: Some(
834                images
835                    .iter()
836                    .map(|p| {
837                        let file = p.as_ref();
838                        let filename = file
839                            .file_name()
840                            .and_then(OsStr::to_str)
841                            .map_or_else(String::new, ToString::to_string);
842
843                        FileSource::new_from_file(file, filename, IMAGE_STAR)
844                    })
845                    .collect(),
846            ),
847            ..self
848        }
849    }
850
851    #[must_use]
852    pub fn youtube(self, urls: &[String]) -> Self {
853        Self {
854            youtube: Some(urls.to_vec()),
855            ..self
856        }
857    }
858
859    #[must_use]
860    pub fn sketchfab(self, urls: &[String]) -> Self {
861        Self {
862            sketchfab: Some(urls.to_vec()),
863            ..self
864        }
865    }
866}
867
868#[doc(hidden)]
869impl From<AddMediaOptions> for Form {
870    fn from(opts: AddMediaOptions) -> Form {
871        let mut form = Form::new();
872        if let Some(sync) = opts.sync {
873            form = form.text("sync", sync.to_string());
874        }
875        if let Some(logo) = opts.logo {
876            form = form.part("logo", logo.into());
877        }
878        if let Some(zip) = opts.images_zip {
879            form = form.part("images", zip.into());
880        }
881        if let Some(images) = opts.images {
882            for (i, image) in images.into_iter().enumerate() {
883                form = form.part(format!("image{i}"), image.into());
884            }
885        }
886        if let Some(youtube) = opts.youtube {
887            for url in youtube {
888                form = form.text("youtube[]", url);
889            }
890        }
891        if let Some(sketchfab) = opts.sketchfab {
892            for url in sketchfab {
893                form = form.text("sketchfab[]", url);
894            }
895        }
896        form
897    }
898}
899
900#[derive(Default)]
901pub struct DeleteMediaOptions {
902    images: Option<Vec<String>>,
903    youtube: Option<Vec<String>>,
904    sketchfab: Option<Vec<String>>,
905}
906
907impl DeleteMediaOptions {
908    #[must_use]
909    pub fn images(self, images: &[String]) -> Self {
910        Self {
911            images: Some(images.to_vec()),
912            ..self
913        }
914    }
915
916    #[must_use]
917    pub fn youtube(self, urls: &[String]) -> Self {
918        Self {
919            youtube: Some(urls.to_vec()),
920            ..self
921        }
922    }
923
924    #[must_use]
925    pub fn sketchfab(self, urls: &[String]) -> Self {
926        Self {
927            sketchfab: Some(urls.to_vec()),
928            ..self
929        }
930    }
931}
932
933#[doc(hidden)]
934impl serde::ser::Serialize for DeleteMediaOptions {
935    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
936    where
937        S: serde::ser::Serializer,
938    {
939        use serde::ser::SerializeMap;
940
941        let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
942            + self.youtube.as_ref().map(Vec::len).unwrap_or_default()
943            + self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
944        let mut map = serializer.serialize_map(Some(len))?;
945        if let Some(ref images) = self.images {
946            for e in images {
947                map.serialize_entry("images[]", e)?;
948            }
949        }
950        if let Some(ref urls) = self.youtube {
951            for e in urls {
952                map.serialize_entry("youtube[]", e)?;
953            }
954        }
955        if let Some(ref urls) = self.sketchfab {
956            for e in urls {
957                map.serialize_entry("sketchfab[]", e)?;
958            }
959        }
960        map.end()
961    }
962}
963
964#[derive(Default)]
965pub struct ReorderMediaOptions {
966    images: Option<Vec<String>>,
967    youtube: Option<Vec<String>>,
968    sketchfab: Option<Vec<String>>,
969}
970
971impl ReorderMediaOptions {
972    #[must_use]
973    pub fn images(self, images: &[String]) -> Self {
974        Self {
975            images: Some(images.to_vec()),
976            ..self
977        }
978    }
979
980    #[must_use]
981    pub fn youtube(self, urls: &[String]) -> Self {
982        Self {
983            youtube: Some(urls.to_vec()),
984            ..self
985        }
986    }
987
988    #[must_use]
989    pub fn sketchfab(self, urls: &[String]) -> Self {
990        Self {
991            sketchfab: Some(urls.to_vec()),
992            ..self
993        }
994    }
995}
996
997#[doc(hidden)]
998impl serde::ser::Serialize for ReorderMediaOptions {
999    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1000    where
1001        S: serde::ser::Serializer,
1002    {
1003        use serde::ser::SerializeMap;
1004
1005        let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
1006            + self.youtube.as_ref().map(Vec::len).unwrap_or_default()
1007            + self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
1008        let mut map = serializer.serialize_map(Some(len))?;
1009        if let Some(ref images) = self.images {
1010            for e in images {
1011                map.serialize_entry("images[]", e)?;
1012            }
1013        }
1014        if let Some(ref urls) = self.youtube {
1015            for e in urls {
1016                map.serialize_entry("youtube[]", e)?;
1017            }
1018        }
1019        if let Some(ref urls) = self.sketchfab {
1020            for e in urls {
1021                map.serialize_entry("sketchfab[]", e)?;
1022            }
1023        }
1024        map.end()
1025    }
1026}