Skip to main content

egs_api/api/
fab.rs

1use crate::api::error::EpicAPIError;
2use crate::api::types::download_manifest::DownloadManifest;
3use crate::api::types::fab_asset_manifest::DownloadInfo;
4use crate::api::types::fab_library::FabLibrary;
5use crate::api::EpicAPI;
6use log::{debug, error, warn};
7use std::borrow::BorrowMut;
8use url::Url;
9
10impl EpicAPI {
11    /// Fetch Fab asset manifest with signed distribution points. Returns `FabTimeout` on 403.
12    pub async fn fab_asset_manifest(
13        &self,
14        artifact_id: &str,
15        namespace: &str,
16        asset_id: &str,
17        platform: Option<&str>,
18    ) -> Result<Vec<DownloadInfo>, EpicAPIError> {
19        let url = format!("https://www.fab.com/e/artifacts/{}/manifest", artifact_id);
20        let parsed_url = Url::parse(&url).map_err(|_| EpicAPIError::InvalidParams)?;
21        match self
22            .authorized_post_client(parsed_url)
23            .json(&serde_json::json!({
24                "item_id": asset_id,
25                "namespace": namespace,
26                "platform": platform.unwrap_or("Windows"),
27            }))
28            .send()
29            .await
30        {
31            Ok(response) => {
32                if response.status() == reqwest::StatusCode::OK {
33                    let text = response.text().await.unwrap_or_default();
34                    match serde_json::from_str::<
35                        crate::api::types::fab_asset_manifest::FabAssetManifest,
36                    >(&text)
37                    {
38                        Ok(manifest) => Ok(manifest.download_info),
39                        Err(e) => {
40                            error!("{:?}", e);
41                            debug!("{}", text);
42                            Err(EpicAPIError::DeserializationError(format!("{}", e)))
43                        }
44                    }
45                } else if response.status() == reqwest::StatusCode::FORBIDDEN {
46                    Err(EpicAPIError::FabTimeout)
47                } else {
48                    debug!("{:?}", response.headers());
49                    let status = response.status();
50                    let body = response.text().await.unwrap_or_default();
51                    warn!("{} result: {}", status, body);
52                    Err(EpicAPIError::HttpError { status, body })
53                }
54            }
55            Err(e) => {
56                error!("{:?}", e);
57                Err(EpicAPIError::NetworkError(e))
58            }
59        }
60    }
61
62    /// Download and parse a Fab manifest from a distribution point.
63    pub async fn fab_download_manifest(
64        &self,
65        download_info: DownloadInfo,
66        distribution_point_url: &str,
67    ) -> Result<DownloadManifest, EpicAPIError> {
68        match download_info.get_distribution_point_by_base_url(distribution_point_url) {
69            None => {
70                error!("Distribution point not found");
71                Err(EpicAPIError::InvalidParams)
72            }
73            Some(point) => {
74                if point.signature_expiration < time::OffsetDateTime::now_utc() {
75                    error!("Expired signature");
76                    Err(EpicAPIError::InvalidParams)
77                } else {
78                    let data = self.get_bytes(&point.manifest_url).await?;
79                    match DownloadManifest::parse(data) {
80                        None => {
81                            error!("Unable to parse the Download Manifest");
82                            Err(EpicAPIError::DeserializationError(
83                                "Unable to parse the Download Manifest".to_string(),
84                            ))
85                        }
86                        Some(mut man) => {
87                            man.set_custom_field("SourceURL", distribution_point_url);
88                            Ok(man)
89                        }
90                    }
91                }
92            }
93        }
94    }
95
96    /// Fetch all Fab library items, paginating internally.
97    pub async fn fab_library_items(
98        &mut self,
99        account_id: String,
100    ) -> Result<FabLibrary, EpicAPIError> {
101        let mut library = FabLibrary::default();
102
103        loop {
104            let url = match &library.cursors.next {
105                None => {
106                    format!(
107                        "https://www.fab.com/e/accounts/{}/ue/library?count=100",
108                        account_id
109                    )
110                }
111                Some(c) => {
112                    format!(
113                        "https://www.fab.com/e/accounts/{}/ue/library?cursor={}&count=100",
114                        account_id, c
115                    )
116                }
117            };
118
119            match self.authorized_get_json::<FabLibrary>(&url).await {
120                Ok(mut api_library) => {
121                    library.cursors.next = api_library.cursors.next;
122                    library.results.append(api_library.results.borrow_mut());
123                }
124                Err(e) => {
125                    error!("{:?}", e);
126                    library.cursors.next = None;
127                }
128            }
129            if library.cursors.next.is_none() {
130                break;
131            }
132        }
133
134        Ok(library)
135    }
136
137    /// Fetch download info for a specific file within a Fab listing.
138    pub async fn fab_file_download_info(
139        &self,
140        listing_id: &str,
141        format_id: &str,
142        file_id: &str,
143    ) -> Result<DownloadInfo, EpicAPIError> {
144        let url = format!(
145            "https://www.fab.com/p/egl/listings/{}/asset-formats/{}/files/{}/download-info",
146            listing_id, format_id, file_id
147        );
148        self.authorized_get_json(&url).await
149    }
150
151    /// Search Fab listings. Public endpoint — no auth required.
152    ///
153    /// Use `FabSearchParams` to specify filters, sorting, and pagination.
154    pub async fn fab_search(
155        &self,
156        params: &crate::api::types::fab_search::FabSearchParams,
157    ) -> Result<crate::api::types::fab_search::FabSearchResults, EpicAPIError> {
158        let mut url = "https://www.fab.com/i/listings/search?".to_string();
159        let mut query_parts = Vec::new();
160
161        if let Some(ref q) = params.q {
162            query_parts.push(format!("q={}", q));
163        }
164        if let Some(ref channels) = params.channels {
165            query_parts.push(format!("channels={}", channels));
166        }
167        if let Some(ref listing_types) = params.listing_types {
168            query_parts.push(format!("listing_types={}", listing_types));
169        }
170        if let Some(ref categories) = params.categories {
171            query_parts.push(format!("categories={}", categories));
172        }
173        if let Some(ref sort_by) = params.sort_by {
174            query_parts.push(format!("sort_by={}", sort_by));
175        }
176        if let Some(count) = params.count {
177            query_parts.push(format!("count={}", count));
178        }
179        if let Some(ref cursor) = params.cursor {
180            query_parts.push(format!("cursor={}", cursor));
181        }
182        if let Some(ref aggregate_on) = params.aggregate_on {
183            query_parts.push(format!("aggregate_on={}", aggregate_on));
184        }
185        if let Some(ref in_filter) = params.in_filter {
186            query_parts.push(format!("in={}", in_filter));
187        }
188        if let Some(is_discounted) = params.is_discounted {
189            if is_discounted {
190                query_parts.push("is_discounted=true".to_string());
191            }
192        }
193        if let Some(is_free) = params.is_free {
194            if is_free {
195                query_parts.push("is_free=1".to_string());
196            }
197        }
198        if let Some(pct) = params.min_discount_percentage {
199            query_parts.push(format!("min_discount_percentage={}", pct));
200        }
201        if let Some(ref seller) = params.seller {
202            query_parts.push(format!("seller={}", seller));
203        }
204
205        url.push_str(&query_parts.join("&"));
206        self.get_json(&url).await
207    }
208
209    /// Get full listing detail. Public endpoint — no auth required.
210    pub async fn fab_listing(
211        &self,
212        uid: &str,
213    ) -> Result<crate::api::types::fab_search::FabListingDetail, EpicAPIError> {
214        let url = format!("https://www.fab.com/i/listings/{}", uid);
215        self.get_json(&url).await
216    }
217
218    /// Get UE-specific format details for a listing. Public endpoint.
219    pub async fn fab_listing_ue_formats(
220        &self,
221        uid: &str,
222    ) -> Result<Vec<crate::api::types::fab_search::FabListingUeFormat>, EpicAPIError> {
223        let url = format!(
224            "https://www.fab.com/i/listings/{}/asset-formats/unreal-engine",
225            uid
226        );
227        self.get_json(&url).await
228    }
229
230    /// Get user's listing state (ownership, wishlist, review). Requires Fab session.
231    pub async fn fab_listing_state(
232        &self,
233        uid: &str,
234    ) -> Result<crate::api::types::fab_search::FabListingState, EpicAPIError> {
235        let url = format!("https://www.fab.com/i/users/me/listings-states/{}", uid);
236        self.authorized_get_json(&url).await
237    }
238
239    /// Bulk check listing states for multiple IDs. Requires Fab session.
240    pub async fn fab_listing_states_bulk(
241        &self,
242        listing_ids: &[&str],
243    ) -> Result<Vec<crate::api::types::fab_search::FabListingState>, EpicAPIError> {
244        let ids = listing_ids.join(",");
245        let url = format!(
246            "https://www.fab.com/i/users/me/listings-states?listing_ids={}",
247            ids
248        );
249        self.authorized_get_json(&url).await
250    }
251
252    /// Bulk fetch pricing for multiple offer IDs. Public endpoint.
253    pub async fn fab_bulk_prices(
254        &self,
255        offer_ids: &[&str],
256    ) -> Result<crate::api::types::fab_search::FabBulkPricesResponse, EpicAPIError> {
257        let ids = offer_ids
258            .iter()
259            .map(|id| format!("offer_ids={}", id))
260            .collect::<Vec<_>>()
261            .join("&");
262        let url = format!("https://www.fab.com/i/listings/prices-infos?{}", ids);
263        self.get_json(&url).await
264    }
265
266    /// Get listing ownership info. Requires Fab session.
267    pub async fn fab_listing_ownership(
268        &self,
269        uid: &str,
270    ) -> Result<crate::api::types::fab_search::FabOwnership, EpicAPIError> {
271        let url = format!("https://www.fab.com/i/listings/{}/ownership", uid);
272        self.authorized_get_json(&url).await
273    }
274
275    /// Get pricing for a specific listing. Public endpoint.
276    pub async fn fab_listing_prices(
277        &self,
278        uid: &str,
279    ) -> Result<Vec<crate::api::types::fab_search::FabPriceInfo>, EpicAPIError> {
280        let url = format!("https://www.fab.com/i/listings/{}/prices-infos", uid);
281        self.get_json(&url).await
282    }
283
284    /// Get reviews for a listing. Public endpoint.
285    pub async fn fab_listing_reviews(
286        &self,
287        uid: &str,
288        sort_by: Option<&str>,
289        cursor: Option<&str>,
290    ) -> Result<crate::api::types::fab_search::FabReviewsResponse, EpicAPIError> {
291        let mut query_parts = Vec::new();
292        if let Some(sort) = sort_by {
293            query_parts.push(format!("sort_by={}", sort));
294        }
295        if let Some(c) = cursor {
296            query_parts.push(format!("cursor={}", c));
297        }
298        let url = if query_parts.is_empty() {
299            format!("https://www.fab.com/i/store/listings/{}/reviews", uid)
300        } else {
301            format!(
302                "https://www.fab.com/i/store/listings/{}/reviews?{}",
303                uid,
304                query_parts.join("&")
305            )
306        };
307        self.get_json(&url).await
308    }
309
310    /// Fetch available license types. Public endpoint.
311    pub async fn fab_licenses(
312        &self,
313    ) -> Result<Vec<crate::api::types::fab_taxonomy::FabLicenseType>, EpicAPIError> {
314        self.get_json("https://www.fab.com/i/taxonomy/licenses").await
315    }
316
317    /// Fetch asset format groups. Public endpoint.
318    pub async fn fab_format_groups(
319        &self,
320    ) -> Result<Vec<crate::api::types::fab_taxonomy::FabFormatGroup>, EpicAPIError> {
321        self.get_json("https://www.fab.com/i/taxonomy/asset-format-groups").await
322    }
323
324    /// Fetch tag groups with nested tags. Public endpoint.
325    pub async fn fab_tag_groups(
326        &self,
327    ) -> Result<Vec<crate::api::types::fab_taxonomy::FabTagGroup>, EpicAPIError> {
328        let wrapper: crate::api::types::fab_taxonomy::FabResultsWrapper<
329            crate::api::types::fab_taxonomy::FabTagGroup,
330        > = self.get_json("https://www.fab.com/i/tags/groups").await?;
331        Ok(wrapper.results)
332    }
333
334    /// Fetch available UE versions. Public endpoint.
335    pub async fn fab_ue_versions(
336        &self,
337    ) -> Result<Vec<String>, EpicAPIError> {
338        self.get_json("https://www.fab.com/i/unreal-engine/versions").await
339    }
340
341    /// Fetch channel info by slug. Public endpoint.
342    pub async fn fab_channel(
343        &self,
344        slug: &str,
345    ) -> Result<crate::api::types::fab_taxonomy::FabChannel, EpicAPIError> {
346        let url = format!("https://www.fab.com/i/channels/{}", slug);
347        self.get_json(&url).await
348    }
349
350    /// Search library entitlements with filters and aggregations.
351    /// Uses the browser-path Fab API. Requires Fab session cookies for full results.
352    pub async fn fab_library_entitlements(
353        &self,
354        params: &crate::api::types::fab_entitlement::FabEntitlementSearchParams,
355    ) -> Result<crate::api::types::fab_entitlement::FabEntitlementResults, EpicAPIError> {
356        let mut query_parts = Vec::new();
357        if let Some(ref sort_by) = params.sort_by {
358            query_parts.push(format!("sort_by={}", sort_by));
359        }
360        if let Some(ref cursor) = params.cursor {
361            query_parts.push(format!("cursor={}", cursor));
362        }
363        if let Some(ref listing_types) = params.listing_types {
364            query_parts.push(format!("listing_types={}", listing_types));
365        }
366        if let Some(ref categories) = params.categories {
367            query_parts.push(format!("categories={}", categories));
368        }
369        if let Some(ref tags) = params.tags {
370            query_parts.push(format!("tags={}", tags));
371        }
372        if let Some(ref licenses) = params.licenses {
373            query_parts.push(format!("licenses={}", licenses));
374        }
375        if let Some(ref asset_formats) = params.asset_formats {
376            query_parts.push(format!("asset_formats={}", asset_formats));
377        }
378        if let Some(ref source) = params.source {
379            query_parts.push(format!("source={}", source));
380        }
381        if let Some(ref aggregate_on) = params.aggregate_on {
382            query_parts.push(format!("aggregate_on={}", aggregate_on));
383        }
384        if let Some(count) = params.count {
385            query_parts.push(format!("count={}", count));
386        }
387        if let Some(ref added_since) = params.added_since {
388            query_parts.push(format!("added_since={}", added_since));
389        }
390
391        let url = if query_parts.is_empty() {
392            "https://www.fab.com/i/library/entitlements/search".to_string()
393        } else {
394            format!(
395                "https://www.fab.com/i/library/entitlements/search?{}",
396                query_parts.join("&")
397            )
398        };
399        self.authorized_get_json(&url).await
400    }
401
402    /// Initialize Fab CSRF token. Sets `fab_csrftoken` cookie on the client.
403    pub async fn fab_csrf(&self) -> Result<(), EpicAPIError> {
404        let parsed_url =
405            url::Url::parse("https://www.fab.com/i/csrf").map_err(|_| EpicAPIError::InvalidParams)?;
406        let response = self.client.get(parsed_url).send().await.map_err(|e| {
407            error!("{:?}", e);
408            EpicAPIError::NetworkError(e)
409        })?;
410        if response.status().is_success() {
411            Ok(())
412        } else {
413            let status = response.status();
414            let body = response.text().await.unwrap_or_default();
415            warn!("{} result: {}", status, body);
416            Err(EpicAPIError::HttpError { status, body })
417        }
418    }
419
420    /// Fetch Fab user context (country, currency, feature flags). Works with just CSRF token.
421    pub async fn fab_user_context(
422        &self,
423    ) -> Result<crate::api::types::fab_search::FabUserContext, EpicAPIError> {
424        self.get_json("https://www.fab.com/i/users/context").await
425    }
426
427    /// Add a free listing to the user's library. Returns `Ok(())` on success (HTTP 204).
428    pub async fn fab_add_to_library(&self, listing_uid: &str) -> Result<(), EpicAPIError> {
429        let url = format!(
430            "https://www.fab.com/i/listings/{}/add-to-library",
431            listing_uid
432        );
433        let parsed_url = Url::parse(&url).map_err(|_| EpicAPIError::InvalidParams)?;
434        let response = self
435            .authorized_post_client(parsed_url)
436            .send()
437            .await
438            .map_err(|e| {
439                error!("{:?}", e);
440                EpicAPIError::NetworkError(e)
441            })?;
442        if response.status() == reqwest::StatusCode::NO_CONTENT
443            || response.status() == reqwest::StatusCode::OK
444        {
445            Ok(())
446        } else {
447            let status = response.status();
448            let body = response.text().await.unwrap_or_default();
449            warn!("{} result: {}", status, body);
450            Err(EpicAPIError::HttpError { status, body })
451        }
452    }
453
454    /// Fetch all available asset formats for a listing (UE, Unity, FBX, Blender, etc.).
455    pub async fn fab_listing_formats(
456        &self,
457        listing_uid: &str,
458    ) -> Result<Vec<crate::api::types::fab_search::FabListingFormat>, EpicAPIError> {
459        let url = format!(
460            "https://www.fab.com/i/listings/{}/asset-formats",
461            listing_uid
462        );
463        self.authorized_get_json(&url).await
464    }
465}