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