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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}