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 channels) = params.channels {
162            query_parts.push(format!("channels={}", channels));
163        }
164        if let Some(ref listing_types) = params.listing_types {
165            query_parts.push(format!("listing_types={}", listing_types));
166        }
167        if let Some(ref categories) = params.categories {
168            query_parts.push(format!("categories={}", categories));
169        }
170        if let Some(ref sort_by) = params.sort_by {
171            query_parts.push(format!("sort_by={}", sort_by));
172        }
173        if let Some(count) = params.count {
174            query_parts.push(format!("count={}", count));
175        }
176        if let Some(ref cursor) = params.cursor {
177            query_parts.push(format!("cursor={}", cursor));
178        }
179        if let Some(ref aggregate_on) = params.aggregate_on {
180            query_parts.push(format!("aggregate_on={}", aggregate_on));
181        }
182        if let Some(ref in_filter) = params.in_filter {
183            query_parts.push(format!("in={}", in_filter));
184        }
185        if let Some(is_discounted) = params.is_discounted {
186            if is_discounted {
187                query_parts.push("is_discounted=true".to_string());
188            }
189        }
190
191        url.push_str(&query_parts.join("&"));
192        self.get_json(&url).await
193    }
194
195    /// Get full listing detail. Public endpoint — no auth required.
196    pub async fn fab_listing(
197        &self,
198        uid: &str,
199    ) -> Result<crate::api::types::fab_search::FabListingDetail, EpicAPIError> {
200        let url = format!("https://www.fab.com/i/listings/{}", uid);
201        self.get_json(&url).await
202    }
203
204    /// Get UE-specific format details for a listing. Public endpoint.
205    pub async fn fab_listing_ue_formats(
206        &self,
207        uid: &str,
208    ) -> Result<Vec<crate::api::types::fab_search::FabListingUeFormat>, EpicAPIError> {
209        let url = format!(
210            "https://www.fab.com/i/listings/{}/asset-formats/unreal-engine",
211            uid
212        );
213        self.get_json(&url).await
214    }
215
216    /// Get user's listing state (ownership, wishlist, review). Requires Fab session.
217    pub async fn fab_listing_state(
218        &self,
219        uid: &str,
220    ) -> Result<crate::api::types::fab_search::FabListingState, EpicAPIError> {
221        let url = format!("https://www.fab.com/i/users/me/listings-states/{}", uid);
222        self.authorized_get_json(&url).await
223    }
224
225    /// Bulk check listing states for multiple IDs. Requires Fab session.
226    pub async fn fab_listing_states_bulk(
227        &self,
228        listing_ids: &[&str],
229    ) -> Result<Vec<crate::api::types::fab_search::FabListingState>, EpicAPIError> {
230        let ids = listing_ids.join(",");
231        let url = format!(
232            "https://www.fab.com/i/users/me/listings-states?listing_ids={}",
233            ids
234        );
235        self.authorized_get_json(&url).await
236    }
237
238    /// Bulk fetch pricing for multiple offer IDs. Public endpoint.
239    pub async fn fab_bulk_prices(
240        &self,
241        offer_ids: &[&str],
242    ) -> Result<Vec<crate::api::types::fab_search::FabPriceInfo>, EpicAPIError> {
243        let ids = offer_ids
244            .iter()
245            .map(|id| format!("offer_ids={}", id))
246            .collect::<Vec<_>>()
247            .join("&");
248        let url = format!("https://www.fab.com/i/listings/prices-infos?{}", ids);
249        self.get_json(&url).await
250    }
251
252    /// Get listing ownership info. Requires Fab session.
253    pub async fn fab_listing_ownership(
254        &self,
255        uid: &str,
256    ) -> Result<crate::api::types::fab_search::FabOwnership, EpicAPIError> {
257        let url = format!("https://www.fab.com/i/listings/{}/ownership", uid);
258        self.authorized_get_json(&url).await
259    }
260}