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