1pub mod cli;
2pub mod workflow;
3
4use crate::api::{EpicClient, Game, GameManifest, ManifestParser};
5use crate::auth::AuthToken;
6use crate::{Error, Result};
7use egs_api::api::types::account::UserData;
8use egs_api::EpicGames;
9use serde_json;
10
11#[derive(Debug)]
12pub struct LegendaryClient {
13 client: EpicGames,
14 epic_client: EpicClient,
15}
16
17impl LegendaryClient {
18 pub fn new() -> Result<Self> {
19 Ok(Self {
20 client: EpicGames::new(),
21 epic_client: EpicClient::new()?,
22 })
23 }
24
25 pub async fn exchange_auth_code(&mut self, code: &str) -> Result<AuthToken> {
26 let success = self.client.auth_code(Some(code.to_string()), None).await;
27 if !success {
28 return Err(Error::Auth("Failed to exchange auth code".to_string()));
29 }
30 self.get_token()
31 }
32
33 pub async fn resume_session(&mut self, token: &AuthToken) -> bool {
34 let json = serde_json::json!({
35 "access_token": token.access_token,
36 "refresh_token": token.refresh_token,
37 "account_id": token.account_id,
38 "expires_at": token.expires_at,
39 });
40
41 if let Ok(user_data) = serde_json::from_value::<UserData>(json) {
42 self.client.set_user_details(user_data);
43 return self.client.login().await;
44 }
45 false
46 }
47
48 pub fn get_token(&self) -> Result<AuthToken> {
49 let user_data = self.client.user_details();
50 let val = serde_json::to_value(&user_data)
51 .map_err(|e| Error::Auth(format!("Failed to serialize user data: {}", e)))?;
52
53 Ok(AuthToken {
54 access_token: val["access_token"].as_str().unwrap_or_default().to_string(),
55 refresh_token: val["refresh_token"]
56 .as_str()
57 .unwrap_or_default()
58 .to_string(),
59 account_id: val["account_id"].as_str().unwrap_or_default().to_string(),
60 expires_at: chrono::Utc::now() + chrono::Duration::hours(1), })
62 }
63
64 pub async fn get_games(&mut self) -> Result<Vec<Game>> {
65 let assets = self.client.list_assets(None, None).await;
66 let token = self.get_token()?;
67
68 let mut games = Vec::new();
69 for asset in assets {
70 let is_uuid =
72 asset.app_name.len() == 32 && asset.app_name.chars().all(|c| c.is_alphanumeric());
73
74 if is_uuid {
75 log::debug!(
77 "Enriching UUID game: {} with catalog {}",
78 asset.app_name,
79 asset.catalog_item_id
80 );
81 let enriched = self
82 .epic_client
83 .fetch_catalog_item(&token, &asset.namespace, &asset.catalog_item_id)
84 .await;
85
86 match enriched {
87 Ok(mut game) => {
88 game.app_name = asset.app_name.clone();
89 game.app_version = asset.build_version;
90 game.namespace = Some(asset.namespace);
91 game.catalog_item_id = Some(asset.catalog_item_id);
92 log::info!("Enriched {} -> {}", asset.app_name, game.app_title);
93 games.push(game);
94 }
95 Err(e) => {
96 log::debug!(
97 "Catalog not found for {}: {}, using generic name",
98 asset.app_name,
99 e
100 );
101 let title = format!("Epic Game ({})", &asset.app_name[..6]);
103 games.push(Game {
104 app_name: asset.app_name.clone(),
105 app_title: title,
106 app_version: asset.build_version,
107 install_path: None,
108 image_url: None,
109 namespace: Some(asset.namespace),
110 catalog_item_id: Some(asset.catalog_item_id),
111 });
112 }
113 }
114 } else {
115 games.push(Game {
117 app_name: asset.app_name.clone(),
118 app_title: asset.app_name.clone(),
119 app_version: asset.build_version,
120 install_path: None,
121 image_url: None,
122 namespace: Some(asset.namespace),
123 catalog_item_id: Some(asset.catalog_item_id),
124 });
125 }
126 }
127 Ok(games)
128 }
129 pub async fn enrich_game_details(
132 &self,
133 token: &AuthToken,
134 app_name: &str,
135 namespace: &str,
136 catalog_item_id: &str,
137 ) -> Game {
138 match self
139 .epic_client
140 .fetch_catalog_item(token, namespace, catalog_item_id)
141 .await
142 {
143 Ok(mut game) => {
144 game.app_name = app_name.to_string();
145 game.app_title = self.humanize_app_name(&game.app_title);
146 game
147 }
148 Err(e) => {
149 log::debug!("Could not enrich game {} from catalog: {}", app_name, e);
150 Game {
151 app_name: app_name.to_string(),
152 app_title: self.humanize_app_name(app_name),
153 app_version: "1.0.0".to_string(),
154 install_path: None,
155 image_url: None,
156 namespace: Some(namespace.to_string()),
157 catalog_item_id: Some(catalog_item_id.to_string()),
158 }
159 }
160 }
161 }
162
163 fn humanize_app_name(&self, app_name: &str) -> String {
165 if app_name.len() == 32 && app_name.chars().all(|c| c.is_alphanumeric()) {
166 "Loading...".to_string()
168 } else {
169 app_name.to_string()
171 }
172 }
173
174 pub async fn download_manifest(
177 &mut self,
178 _token: &AuthToken,
179 app_name: &str,
180 ) -> Result<GameManifest> {
181 log::info!("Fetching manifest for: {}", app_name);
182
183 let assets = self.client.list_assets(None, None).await;
185 let asset = assets
186 .iter()
187 .find(|a| a.app_name == app_name)
188 .ok_or_else(|| Error::GameNotFound(app_name.to_string()))?;
189
190 log::debug!("Found asset: {} ({})", asset.app_name, asset.asset_id);
191
192 log::warn!("Using fallback manifest - full CDN integration pending");
198 Self::fallback_manifest(app_name, &asset.build_version)
199 }
200
201 fn fallback_manifest(app_name: &str, version: &str) -> Result<GameManifest> {
202 Ok(GameManifest {
203 manifest_file_version: "21".to_string(),
204 is_file_data: true,
205 app_name: app_name.to_string(),
206 app_version: version.to_string(),
207 launch_exe: if cfg!(windows) {
208 format!("{}.exe", app_name)
209 } else {
210 "start.sh".to_string()
211 },
212 launch_command: String::new(),
213 build_size: 0,
214 file_list: Vec::new(),
215 chunk_hash_list: std::collections::HashMap::new(),
216 chunk_sha_list: std::collections::HashMap::new(),
217 data_group_list: std::collections::HashMap::new(),
218 })
219 }
220
221 #[allow(dead_code)]
222 fn parse_manifest(manifest_data: Vec<u8>, app_name: &str) -> Result<GameManifest> {
223 log::debug!("Parsing manifest data: {} bytes", manifest_data.len());
224
225 match ManifestParser::parse(&manifest_data) {
227 Ok(manifest) => {
228 log::info!(
229 "Successfully parsed manifest for {}: {} files",
230 app_name,
231 manifest.file_list.len()
232 );
233 Ok(manifest)
234 }
235 Err(e) => {
236 log::error!("Failed to parse manifest for {}: {}", app_name, e);
237 Ok(GameManifest {
239 manifest_file_version: "21".to_string(),
240 is_file_data: true,
241 app_name: app_name.to_string(),
242 app_version: "1.0.0".to_string(),
243 launch_exe: if cfg!(windows) {
244 format!("{}.exe", app_name)
245 } else {
246 "start.sh".to_string()
247 },
248 launch_command: String::new(),
249 build_size: manifest_data.len() as u64,
250 file_list: Vec::new(),
251 chunk_hash_list: std::collections::HashMap::new(),
252 chunk_sha_list: std::collections::HashMap::new(),
253 data_group_list: std::collections::HashMap::new(),
254 })
255 }
256 }
257 }
258
259 pub async fn download_chunk(&self, _chunk_guid: &str, _token: &AuthToken) -> Result<Vec<u8>> {
260 Ok(Vec::new())
261 }
262
263 pub async fn check_for_updates(
264 &self,
265 _token: &AuthToken,
266 _app_name: &str,
267 _current_version: &str,
268 ) -> Result<Option<String>> {
269 Ok(None)
270 }
271
272 pub async fn get_cloud_saves(
273 &self,
274 _token: &AuthToken,
275 _app_name: &str,
276 ) -> Result<Vec<crate::api::CloudSave>> {
277 Ok(Vec::new())
278 }
279
280 pub async fn download_cloud_save(&self, _token: &AuthToken, _save_id: &str) -> Result<Vec<u8>> {
281 Ok(Vec::new())
282 }
283
284 pub async fn upload_cloud_save(
285 &self,
286 _token: &AuthToken,
287 _app_name: &str,
288 _data: &[u8],
289 ) -> Result<()> {
290 Ok(())
291 }
292}