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, Dependency, Event, EventType, Image, MaturityOption, Media, Mod, Platform,
18    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    metadata_blob: Option<String>,
579    tags: Option<Vec<String>>,
580}
581
582impl AddModOptions {
583    pub fn new<T, P>(name: T, logo: P, summary: T) -> AddModOptions
584    where
585        T: Into<String>,
586        P: AsRef<Path>,
587    {
588        let filename = logo
589            .as_ref()
590            .file_name()
591            .and_then(OsStr::to_str)
592            .map_or_else(String::new, ToString::to_string);
593
594        let logo = FileSource::new_from_file(logo, filename, IMAGE_STAR);
595
596        AddModOptions {
597            name: name.into(),
598            logo,
599            summary: summary.into(),
600            visible: None,
601            name_id: None,
602            description: None,
603            homepage_url: None,
604            stock: None,
605            maturity_option: None,
606            metadata_blob: None,
607            tags: None,
608        }
609    }
610
611    #[must_use]
612    pub fn visible(self, v: bool) -> Self {
613        Self {
614            visible: if v {
615                Some(Visibility::PUBLIC)
616            } else {
617                Some(Visibility::HIDDEN)
618            },
619            ..self
620        }
621    }
622
623    option!(name_id);
624    option!(description);
625    option!(homepage_url: Url);
626    option!(stock: u32);
627    option!(maturity_option: MaturityOption);
628    option!(metadata_blob);
629
630    #[must_use]
631    pub fn tags(self, tags: &[String]) -> Self {
632        Self {
633            tags: Some(tags.to_vec()),
634            ..self
635        }
636    }
637}
638
639#[doc(hidden)]
640impl From<AddModOptions> for Form {
641    fn from(opts: AddModOptions) -> Form {
642        let mut form = Form::new();
643
644        form = form.text("name", opts.name).text("summary", opts.summary);
645
646        if let Some(visible) = opts.visible {
647            form = form.text("visible", visible.to_string());
648        }
649        if let Some(name_id) = opts.name_id {
650            form = form.text("name_id", name_id);
651        }
652        if let Some(desc) = opts.description {
653            form = form.text("description", desc);
654        }
655        if let Some(url) = opts.homepage_url {
656            form = form.text("homepage_url", url.to_string());
657        }
658        if let Some(stock) = opts.stock {
659            form = form.text("stock", stock.to_string());
660        }
661        if let Some(maturity_option) = opts.maturity_option {
662            form = form.text("maturity_option", maturity_option.to_string());
663        }
664        if let Some(metadata_blob) = opts.metadata_blob {
665            form = form.text("metadata_blob", metadata_blob);
666        }
667        if let Some(tags) = opts.tags {
668            for tag in tags {
669                form = form.text("tags[]", tag);
670            }
671        }
672        form.part("logo", opts.logo.into())
673    }
674}
675
676#[derive(Debug, Default)]
677pub struct EditModOptions {
678    params: std::collections::BTreeMap<&'static str, String>,
679}
680
681impl EditModOptions {
682    option!(status: Status >> "status");
683
684    #[must_use]
685    pub fn visible(self, v: bool) -> Self {
686        let value = if v {
687            Visibility::PUBLIC
688        } else {
689            Visibility::HIDDEN
690        };
691        let mut params = self.params;
692        params.insert("visible", value.to_string());
693        Self { params }
694    }
695
696    option!(visibility: Visibility >> "visible");
697    option!(name >> "name");
698    option!(name_id >> "name_id");
699    option!(summary >> "summary");
700    option!(description >> "description");
701    option!(homepage_url: Url >> "homepage_url");
702    option!(stock >> "stock");
703    option!(maturity_option: MaturityOption >> "maturity_option");
704    option!(metadata_blob >> "metadata_blob");
705}
706
707impl_serialize_params!(EditModOptions >> params);
708
709pub struct EditDependenciesOptions {
710    dependencies: Vec<ModId>,
711}
712
713impl EditDependenciesOptions {
714    pub fn new(dependencies: &[ModId]) -> Self {
715        Self {
716            dependencies: dependencies.to_vec(),
717        }
718    }
719
720    pub fn one(dependency: ModId) -> Self {
721        Self {
722            dependencies: vec![dependency],
723        }
724    }
725}
726
727#[doc(hidden)]
728impl serde::ser::Serialize for EditDependenciesOptions {
729    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
730    where
731        S: serde::ser::Serializer,
732    {
733        use serde::ser::SerializeMap;
734
735        let mut map = serializer.serialize_map(Some(self.dependencies.len()))?;
736        for d in &self.dependencies {
737            map.serialize_entry("dependencies[]", d)?;
738        }
739        map.end()
740    }
741}
742
743pub struct EditTagsOptions {
744    tags: Vec<String>,
745}
746
747impl EditTagsOptions {
748    pub fn new(tags: &[String]) -> Self {
749        Self {
750            tags: tags.to_vec(),
751        }
752    }
753}
754
755#[doc(hidden)]
756impl serde::ser::Serialize for EditTagsOptions {
757    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
758    where
759        S: serde::ser::Serializer,
760    {
761        use serde::ser::SerializeMap;
762
763        let mut map = serializer.serialize_map(Some(self.tags.len()))?;
764        for t in &self.tags {
765            map.serialize_entry("tags[]", t)?;
766        }
767        map.end()
768    }
769}
770
771#[derive(Default)]
772pub struct AddMediaOptions {
773    logo: Option<FileSource>,
774    images_zip: Option<FileSource>,
775    images: Option<Vec<FileSource>>,
776    youtube: Option<Vec<String>>,
777    sketchfab: Option<Vec<String>>,
778}
779
780impl AddMediaOptions {
781    #[must_use]
782    pub fn logo<P: AsRef<Path>>(self, logo: P) -> Self {
783        let logo = logo.as_ref();
784        let filename = logo
785            .file_name()
786            .and_then(OsStr::to_str)
787            .map_or_else(String::new, ToString::to_string);
788
789        Self {
790            logo: Some(FileSource::new_from_file(logo, filename, IMAGE_STAR)),
791            ..self
792        }
793    }
794
795    #[must_use]
796    pub fn images_zip<P: AsRef<Path>>(self, images: P) -> Self {
797        Self {
798            images_zip: Some(FileSource::new_from_file(
799                images,
800                "images.zip".into(),
801                APPLICATION_OCTET_STREAM,
802            )),
803            ..self
804        }
805    }
806
807    #[must_use]
808    pub fn images<P: AsRef<Path>>(self, images: &[P]) -> Self {
809        Self {
810            images: Some(
811                images
812                    .iter()
813                    .map(|p| {
814                        let file = p.as_ref();
815                        let filename = file
816                            .file_name()
817                            .and_then(OsStr::to_str)
818                            .map_or_else(String::new, ToString::to_string);
819
820                        FileSource::new_from_file(file, filename, IMAGE_STAR)
821                    })
822                    .collect(),
823            ),
824            ..self
825        }
826    }
827
828    #[must_use]
829    pub fn youtube(self, urls: &[String]) -> Self {
830        Self {
831            youtube: Some(urls.to_vec()),
832            ..self
833        }
834    }
835
836    #[must_use]
837    pub fn sketchfab(self, urls: &[String]) -> Self {
838        Self {
839            sketchfab: Some(urls.to_vec()),
840            ..self
841        }
842    }
843}
844
845#[doc(hidden)]
846impl From<AddMediaOptions> for Form {
847    fn from(opts: AddMediaOptions) -> Form {
848        let mut form = Form::new();
849        if let Some(logo) = opts.logo {
850            form = form.part("logo", logo.into());
851        }
852        if let Some(zip) = opts.images_zip {
853            form = form.part("images", zip.into());
854        }
855        if let Some(images) = opts.images {
856            for (i, image) in images.into_iter().enumerate() {
857                form = form.part(format!("image{i}"), image.into());
858            }
859        }
860        if let Some(youtube) = opts.youtube {
861            for url in youtube {
862                form = form.text("youtube[]", url);
863            }
864        }
865        if let Some(sketchfab) = opts.sketchfab {
866            for url in sketchfab {
867                form = form.text("sketchfab[]", url);
868            }
869        }
870        form
871    }
872}
873
874#[derive(Default)]
875pub struct DeleteMediaOptions {
876    images: Option<Vec<String>>,
877    youtube: Option<Vec<String>>,
878    sketchfab: Option<Vec<String>>,
879}
880
881impl DeleteMediaOptions {
882    #[must_use]
883    pub fn images(self, images: &[String]) -> Self {
884        Self {
885            images: Some(images.to_vec()),
886            ..self
887        }
888    }
889
890    #[must_use]
891    pub fn youtube(self, urls: &[String]) -> Self {
892        Self {
893            youtube: Some(urls.to_vec()),
894            ..self
895        }
896    }
897
898    #[must_use]
899    pub fn sketchfab(self, urls: &[String]) -> Self {
900        Self {
901            sketchfab: Some(urls.to_vec()),
902            ..self
903        }
904    }
905}
906
907#[doc(hidden)]
908impl serde::ser::Serialize for DeleteMediaOptions {
909    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
910    where
911        S: serde::ser::Serializer,
912    {
913        use serde::ser::SerializeMap;
914
915        let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
916            + self.youtube.as_ref().map(Vec::len).unwrap_or_default()
917            + self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
918        let mut map = serializer.serialize_map(Some(len))?;
919        if let Some(ref images) = self.images {
920            for e in images {
921                map.serialize_entry("images[]", e)?;
922            }
923        }
924        if let Some(ref urls) = self.youtube {
925            for e in urls {
926                map.serialize_entry("youtube[]", e)?;
927            }
928        }
929        if let Some(ref urls) = self.sketchfab {
930            for e in urls {
931                map.serialize_entry("sketchfab[]", e)?;
932            }
933        }
934        map.end()
935    }
936}
937
938#[derive(Default)]
939pub struct ReorderMediaOptions {
940    images: Option<Vec<String>>,
941    youtube: Option<Vec<String>>,
942    sketchfab: Option<Vec<String>>,
943}
944
945impl ReorderMediaOptions {
946    #[must_use]
947    pub fn images(self, images: &[String]) -> Self {
948        Self {
949            images: Some(images.to_vec()),
950            ..self
951        }
952    }
953
954    #[must_use]
955    pub fn youtube(self, urls: &[String]) -> Self {
956        Self {
957            youtube: Some(urls.to_vec()),
958            ..self
959        }
960    }
961
962    #[must_use]
963    pub fn sketchfab(self, urls: &[String]) -> Self {
964        Self {
965            sketchfab: Some(urls.to_vec()),
966            ..self
967        }
968    }
969}
970
971#[doc(hidden)]
972impl serde::ser::Serialize for ReorderMediaOptions {
973    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
974    where
975        S: serde::ser::Serializer,
976    {
977        use serde::ser::SerializeMap;
978
979        let len = self.images.as_ref().map(Vec::len).unwrap_or_default()
980            + self.youtube.as_ref().map(Vec::len).unwrap_or_default()
981            + self.sketchfab.as_ref().map(Vec::len).unwrap_or_default();
982        let mut map = serializer.serialize_map(Some(len))?;
983        if let Some(ref images) = self.images {
984            for e in images {
985                map.serialize_entry("images[]", e)?;
986            }
987        }
988        if let Some(ref urls) = self.youtube {
989            for e in urls {
990                map.serialize_entry("youtube[]", e)?;
991            }
992        }
993        if let Some(ref urls) = self.sketchfab {
994            for e in urls {
995                map.serialize_entry("sketchfab[]", e)?;
996            }
997        }
998        map.end()
999    }
1000}