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 all library items, paginating internally.
159    pub async fn library_items(&mut self, include_metadata: bool) -> Result<Library, EpicAPIError> {
160        let mut library = Library {
161            records: vec![],
162            response_metadata: Default::default(),
163        };
164        let mut cursor: Option<String> = None;
165        loop {
166            let url = match &cursor {
167                None => {
168                    format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}", include_metadata)
169                }
170                Some(c) => {
171                    format!("https://library-service.live.use1a.on.epicgames.com/library/api/public/items?includeMetadata={}&cursor={}", include_metadata, c)
172                }
173            };
174
175            match self.authorized_get_json::<Library>(&url).await {
176                Ok(mut records) => {
177                    library.records.append(records.records.borrow_mut());
178                    match records.response_metadata {
179                        None => {
180                            break;
181                        }
182                        Some(meta) => match meta.next_cursor {
183                            None => {
184                                break;
185                            }
186                            Some(curs) => {
187                                cursor = Some(curs);
188                            }
189                        },
190                    }
191                }
192                Err(e) => {
193                    error!("{:?}", e);
194                    break;
195                }
196            };
197        }
198        Ok(library)
199    }
200
201    /// Fetch paginated catalog items for a namespace.
202    pub async fn catalog_items(
203        &self,
204        namespace: &str,
205        start: i64,
206        count: i64,
207    ) -> Result<CatalogItemPage, EpicAPIError> {
208        let url = format!(
209            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/items?start={}&count={}",
210            namespace, start, count
211        );
212        self.authorized_get_json(&url).await
213    }
214
215    /// Fetch paginated catalog offers for a namespace.
216    pub async fn catalog_offers(
217        &self,
218        namespace: &str,
219        start: i64,
220        count: i64,
221    ) -> Result<CatalogOfferPage, EpicAPIError> {
222        let url = format!(
223            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/namespace/{}/offers?start={}&count={}",
224            namespace, start, count
225        );
226        self.authorized_get_json(&url).await
227    }
228
229    /// Bulk fetch catalog items across multiple namespaces.
230    pub async fn bulk_catalog_items(
231        &self,
232        items: &[(&str, &str)],
233    ) -> Result<HashMap<String, HashMap<String, AssetInfo>>, EpicAPIError> {
234        let body: Vec<serde_json::Value> = items
235            .iter()
236            .map(|(ns, id)| {
237                serde_json::json!({
238                    "id": id,
239                    "namespace": ns,
240                    "includeDLCDetails": true,
241                    "includeMainGameDetails": true,
242                    "country": "us",
243                    "locale": "lc",
244                })
245            })
246            .collect();
247        self.authorized_post_json(
248            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/bulk/namespaces/items",
249            &body,
250        )
251        .await
252    }
253
254    /// Fetch available currencies.
255    pub async fn currencies(
256        &self,
257        start: i64,
258        count: i64,
259    ) -> Result<CurrencyPage, EpicAPIError> {
260        let url = format!(
261            "https://catalog-public-service-prod06.ol.epicgames.com/catalog/api/shared/currencies?start={}&count={}",
262            start, count
263        );
264        self.authorized_get_json(&url).await
265    }
266
267    /// Check the status of a library state token.
268    pub async fn library_state_token_status(
269        &self,
270        token_id: &str,
271    ) -> Result<bool, EpicAPIError> {
272        let url = format!(
273            "https://library-service.live.use1a.on.epicgames.com/library/api/public/stateToken/{}/status",
274            token_id
275        );
276        self.authorized_get_json(&url).await
277    }
278}