Skip to main content

egs_api/api/
egs.rs

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