Skip to main content

egs_api/api/
egs.rs

1use crate::api::error::EpicAPIError;
2use crate::api::types::asset_info::{AssetInfo, GameToken, OwnershipToken};
3use crate::api::types::asset_manifest::AssetManifest;
4use crate::api::types::catalog_item::CatalogItemPage;
5use crate::api::types::catalog_offer::CatalogOfferPage;
6use crate::api::types::currency::CurrencyPage;
7use crate::api::types::download_manifest::DownloadManifest;
8use crate::api::types::epic_asset::EpicAsset;
9use crate::api::types::library::Library;
10use crate::api::EpicAPI;
11use log::{debug, error};
12use std::borrow::BorrowMut;
13use std::collections::HashMap;
14
15impl EpicAPI {
16    /// Fetch all owned assets for the given platform and label.
17    pub async fn assets(
18        &mut self,
19        platform: Option<String>,
20        label: Option<String>,
21    ) -> Result<Vec<EpicAsset>, EpicAPIError> {
22        let plat = platform.unwrap_or_else(|| "Windows".to_string());
23        let lab = label.unwrap_or_else(|| "Live".to_string());
24        let url = format!("https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/{}?label={}", plat, lab);
25        self.authorized_get_json(&url).await
26    }
27
28    /// Fetch the asset manifest with CDN download URLs.
29    pub async fn asset_manifest(
30        &self,
31        platform: Option<String>,
32        label: Option<String>,
33        namespace: Option<String>,
34        item_id: Option<String>,
35        app: Option<String>,
36    ) -> Result<AssetManifest, EpicAPIError> {
37        if namespace.is_none() {
38            return Err(EpicAPIError::InvalidParams);
39        };
40        if item_id.is_none() {
41            return Err(EpicAPIError::InvalidParams);
42        };
43        if app.is_none() {
44            return Err(EpicAPIError::InvalidParams);
45        };
46        let url = format!("https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/{}/namespace/{}/catalogItem/{}/app/{}/label/{}",
47                          platform.as_deref().unwrap_or("Windows"), namespace.as_deref().unwrap(), item_id.as_deref().unwrap(), app.as_deref().unwrap(), label.as_deref().unwrap_or("Live"));
48        let mut manifest: AssetManifest = self.authorized_get_json(&url).await?;
49        manifest.platform = platform;
50        manifest.label = label;
51        manifest.namespace = namespace;
52        manifest.item_id = item_id;
53        manifest.app = app;
54        Ok(manifest)
55    }
56
57    /// Download and parse manifests from all CDN mirrors in the asset manifest.
58    pub async fn asset_download_manifests(
59        &self,
60        asset_manifest: AssetManifest,
61    ) -> Vec<DownloadManifest> {
62        let base_urls = asset_manifest.url_csv();
63        let mut result: Vec<DownloadManifest> = Vec::new();
64        for elem in asset_manifest.elements {
65            for manifest in elem.manifests {
66                let mut queries: Vec<String> = Vec::new();
67                debug!("{:?}", manifest);
68                for query in manifest.query_params {
69                    queries.push(format!("{}={}", query.name, query.value));
70                }
71                let url = format!("{}?{}", manifest.uri, queries.join("&"));
72                match self.get_bytes(&url).await {
73                    Ok(data) => match DownloadManifest::parse(data) {
74                        None => {
75                            error!("Unable to parse the Download Manifest");
76                        }
77                        Some(mut man) => {
78                            let mut url = manifest.uri.clone();
79                            url.set_path(&match url.path_segments() {
80                                None => "".to_string(),
81                                Some(segments) => {
82                                    let mut vec: Vec<&str> = segments.collect();
83                                    vec.remove(vec.len() - 1);
84                                    vec.join("/")
85                                }
86                            });
87                            url.set_query(None);
88                            url.set_fragment(None);
89                            man.set_custom_field("BaseUrl", &base_urls);
90
91                            if let Some(id) = asset_manifest.item_id.as_deref() {
92                                man.set_custom_field("CatalogItemId", id);
93                            }
94                            if let Some(label) = asset_manifest.label.as_deref() {
95                                man.set_custom_field("BuildLabel", label);
96                            }
97                            if let Some(ns) = asset_manifest.namespace.as_deref() {
98                                man.set_custom_field("CatalogNamespace", ns);
99                            }
100
101                            if let Some(app) = asset_manifest.app.as_deref() {
102                                man.set_custom_field("CatalogAssetName", app);
103                            }
104
105                            let source_url = url.to_string();
106                            man.set_custom_field("SourceURL", &source_url);
107                            result.push(man)
108                        }
109                    },
110                    Err(e) => {
111                        error!("{:?}", e);
112                    }
113                }
114            }
115        }
116        result
117    }
118
119    /// Fetch catalog metadata for an asset, including DLC details.
120    pub async fn asset_info(
121        &self,
122        asset: &EpicAsset,
123    ) -> Result<HashMap<String, AssetInfo>, EpicAPIError> {
124        let url = format!("https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/bulk/items?id={}&includeDLCDetails=true&includeMainGameDetails=true&country=us&locale=lc",
125                          asset.namespace, asset.catalog_item_id);
126        self.authorized_get_json(&url).await
127    }
128
129    /// Fetch a short-lived game exchange token.
130    pub async fn game_token(&self) -> Result<GameToken, EpicAPIError> {
131        self.authorized_get_json(
132            "https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange",
133        )
134        .await
135    }
136
137    /// Fetch a JWT ownership token for the given asset.
138    pub async fn ownership_token(&self, asset: &EpicAsset) -> Result<OwnershipToken, EpicAPIError> {
139        let url = match &self.user_data.account_id {
140            None => {
141                return Err(EpicAPIError::InvalidCredentials);
142            }
143            Some(id) => {
144                format!("https://ecommerceintegration-public-service-ecomprod02.ol.epicgames.com/ecommerceintegration/api/public/platforms/EPIC/identities/{}/ownershipToken",
145                        id)
146            }
147        };
148        self.authorized_post_form_json(
149            &url,
150            &[(
151                "nsCatalogItemId".to_string(),
152                format!("{}:{}", asset.namespace, asset.catalog_item_id),
153            )],
154        )
155        .await
156    }
157
158    /// Fetch an artifact service ticket for EOS Helper manifest retrieval.
159    ///
160    /// The `sandbox_id` is typically the same as the game's namespace,
161    /// and `artifact_id` is the same as the app name.
162    pub async fn artifact_service_ticket(
163        &self,
164        sandbox_id: &str,
165        artifact_id: &str,
166        label: Option<&str>,
167        platform: Option<&str>,
168    ) -> Result<crate::api::types::artifact_service::ArtifactServiceTicket, EpicAPIError> {
169        let url = format!(
170            "https://artifact-public-service-prod.beee.live.use1a.on.epicgames.com/artifact-service/api/public/v1/dependency/sandbox/{}/artifact/{}/ticket",
171            sandbox_id, artifact_id
172        );
173        let body = serde_json::json!({
174            "label": label.unwrap_or("Live"),
175            "expiresInSeconds": 300,
176            "platform": platform.unwrap_or("Windows"),
177        });
178        self.authorized_post_json(&url, &body).await
179    }
180
181    /// Fetch a game manifest using a signed artifact service ticket.
182    ///
183    /// This is an alternative to `asset_manifest` that uses ticket-based auth
184    /// from the EOS Helper service rather than the standard OAuth flow.
185    pub async fn game_manifest_by_ticket(
186        &self,
187        artifact_id: &str,
188        signed_ticket: &str,
189        label: Option<&str>,
190        platform: Option<&str>,
191    ) -> Result<AssetManifest, EpicAPIError> {
192        let url = format!(
193            "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/by-ticket/app/{}",
194            artifact_id
195        );
196        let body = serde_json::json!({
197            "platform": platform.unwrap_or("Windows"),
198            "label": label.unwrap_or("Live"),
199            "signedTicket": signed_ticket,
200        });
201        self.authorized_post_json(&url, &body).await
202    }
203
204    /// Fetch launcher manifests for self-update checks.
205    ///
206    /// Returns the launcher's own asset manifest for a given platform.
207    pub async fn launcher_manifests(
208        &self,
209        platform: Option<&str>,
210        label: Option<&str>,
211    ) -> Result<AssetManifest, EpicAPIError> {
212        let url = format!(
213            "https://launcher-public-service-prod06.ol.epicgames.com/launcher/api/public/assets/v2/platform/{}/launcher?label={}",
214            platform.unwrap_or("Windows"),
215            label.unwrap_or("Live-EternalKnight"),
216        );
217        self.authorized_get_json(&url).await
218    }
219
220    /// Try to fetch a delta manifest for optimized patching between builds.
221    ///
222    /// Delta manifests reduce download size when updating from one version to another.
223    /// Returns `None` if no delta manifest is available (most games don't have them).
224    pub async fn delta_manifest(
225        &self,
226        base_url: &str,
227        old_build_id: &str,
228        new_build_id: &str,
229    ) -> Option<Vec<u8>> {
230        if old_build_id == new_build_id {
231            return None;
232        }
233        let url = format!("{}/Deltas/{}/{}.delta", base_url, new_build_id, old_build_id);
234        self.get_bytes(&url).await.ok()
235    }
236
237    /// Fetch all library items, paginating internally.
238    pub async fn library_items(&mut self, include_metadata: bool) -> Result<Library, EpicAPIError> {
239        let mut library = Library {
240            records: vec![],
241            response_metadata: Default::default(),
242        };
243        let mut cursor: Option<String> = None;
244        loop {
245            let url = match &cursor {
246                None => {
247                    format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}", include_metadata)
248                }
249                Some(c) => {
250                    format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}&cursor={}", include_metadata, c)
251                }
252            };
253
254            match self.authorized_get_json::<Library>(&url).await {
255                Ok(mut records) => {
256                    library.records.append(records.records.borrow_mut());
257                    match records.response_metadata {
258                        None => {
259                            break;
260                        }
261                        Some(meta) => match meta.next_cursor {
262                            None => {
263                                break;
264                            }
265                            Some(curs) => {
266                                cursor = Some(curs);
267                            }
268                        },
269                    }
270                }
271                Err(e) => {
272                    error!("{:?}", e);
273                    break;
274                }
275            };
276        }
277        Ok(library)
278    }
279
280    /// Fetch paginated catalog items for a namespace.
281    pub async fn catalog_items(
282        &self,
283        namespace: &str,
284        start: i64,
285        count: i64,
286    ) -> Result<CatalogItemPage, EpicAPIError> {
287        let url = format!(
288            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/items?start={}&count={}",
289            namespace, start, count
290        );
291        self.authorized_get_json(&url).await
292    }
293
294    /// Fetch paginated catalog offers for a namespace.
295    pub async fn catalog_offers(
296        &self,
297        namespace: &str,
298        start: i64,
299        count: i64,
300    ) -> Result<CatalogOfferPage, EpicAPIError> {
301        let url = format!(
302            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/offers?start={}&count={}",
303            namespace, start, count
304        );
305        self.authorized_get_json(&url).await
306    }
307
308    /// Bulk fetch catalog items across multiple namespaces.
309    pub async fn bulk_catalog_items(
310        &self,
311        items: &[(&str, &str)],
312    ) -> Result<HashMap<String, HashMap<String, AssetInfo>>, EpicAPIError> {
313        let body: Vec<serde_json::Value> = items
314            .iter()
315            .map(|(ns, id)| {
316                serde_json::json!({
317                    "id": id,
318                    "namespace": ns,
319                    "includeDLCDetails": true,
320                    "includeMainGameDetails": true,
321                    "country": "us",
322                    "locale": "lc",
323                })
324            })
325            .collect();
326        self.authorized_post_json(
327            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/bulk/namespaces/items",
328            &body,
329        )
330        .await
331    }
332
333    /// Fetch available currencies.
334    pub async fn currencies(
335        &self,
336        start: i64,
337        count: i64,
338    ) -> Result<CurrencyPage, EpicAPIError> {
339        let url = format!(
340            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/currencies?start={}&count={}",
341            start, count
342        );
343        self.authorized_get_json(&url).await
344    }
345
346    /// Check the status of a library state token.
347    pub async fn library_state_token_status(
348        &self,
349        token_id: &str,
350    ) -> Result<bool, EpicAPIError> {
351        let url = format!(
352            "https://library-service.live.use1a.on.epicgames.com/library/api/public/stateToken/{}/status",
353            token_id
354        );
355        self.authorized_get_json(&url).await
356    }
357}