1use std::time::Duration;
9
10use super::errors::{RequestContext, classify_http_issue, classify_network_issue};
11use super::{
12 AssetDetails, ImmichApiClient, LibraryAlbum, LibraryAsset, SortOrder, ThumbnailSize,
13 TransferProgressCallback,
14};
15
16impl ImmichApiClient {
17 pub async fn fetch_library_albums(&self) -> Result<Vec<LibraryAlbum>, String> {
19 let base_url = self
20 .get_active_url()
21 .await
22 .ok_or_else(|| "No active connection".to_string())?;
23 let settings = self.settings_snapshot();
24 let url = format!("{}/api/albums", base_url);
25
26 match self
27 .client
28 .get(&url)
29 .header("x-api-key", &settings.api_key)
30 .header("Accept", "application/json")
31 .timeout(Duration::from_secs(10))
32 .send()
33 .await
34 {
35 Ok(resp) if resp.status().is_success() => {
36 let albums = resp
37 .json::<Vec<LibraryAlbum>>()
38 .await
39 .map_err(|err| err.to_string())?;
40 self.clear_issue().await;
41 Ok(albums)
42 }
43 Ok(resp) => {
44 self.set_issue(classify_http_issue(
45 RequestContext::Albums,
46 resp.status().as_u16(),
47 None,
48 ))
49 .await;
50 Err(format!("HTTP {}", resp.status()))
51 }
52 Err(err) => {
53 *self.active_url.lock().await = None;
54 self.set_issue(classify_network_issue(RequestContext::Albums, &err))
55 .await;
56 Err(err.to_string())
57 }
58 }
59 }
60
61 pub async fn fetch_album_assets(
63 &self,
64 album_id: &str,
65 page: u32,
66 size: u32,
67 order: Option<SortOrder>,
68 ) -> Result<(Vec<LibraryAsset>, bool), String> {
69 let mut body = serde_json::json!({
70 "albumIds": [album_id],
71 "page": page,
72 "size": size.max(1),
73 });
74 if let Some(order) = order
75 && let Some(obj) = body.as_object_mut()
76 {
77 obj.insert(
78 "order".into(),
79 serde_json::to_value(order).unwrap_or(serde_json::Value::Null),
80 );
81 }
82 self.fetch_search_assets(
83 "/api/search/metadata",
84 body,
85 RequestContext::AssetList,
86 Some(album_id),
87 )
88 .await
89 }
90
91 pub async fn fetch_thumbnail(
93 &self,
94 asset_id: &str,
95 size: ThumbnailSize,
96 ) -> Result<Vec<u8>, String> {
97 let _permit = self
98 .thumbnail_semaphore
99 .clone()
100 .acquire_owned()
101 .await
102 .map_err(|err| err.to_string())?;
103 let base_url = self
104 .get_active_url()
105 .await
106 .ok_or_else(|| "No active connection".to_string())?;
107 let settings = self.settings_snapshot();
108 let url = format!(
109 "{}/api/assets/{}/thumbnail?size={}",
110 base_url,
111 asset_id,
112 size.as_str()
113 );
114
115 match self
116 .client
117 .get(&url)
118 .header("x-api-key", &settings.api_key)
119 .header("Accept", "application/octet-stream")
120 .timeout(Duration::from_secs(10))
121 .send()
122 .await
123 {
124 Ok(resp) if resp.status().is_success() => {
125 let bytes = resp.bytes().await.map_err(|err| err.to_string())?;
126 self.clear_issue().await;
127 Ok(bytes.to_vec())
128 }
129 Ok(resp) => {
130 self.set_issue(classify_http_issue(
131 RequestContext::ThumbnailFetch,
132 resp.status().as_u16(),
133 Some(asset_id),
134 ))
135 .await;
136 Err(format!("HTTP {}", resp.status()))
137 }
138 Err(err) => {
139 *self.active_url.lock().await = None;
140 self.set_issue(classify_network_issue(RequestContext::ThumbnailFetch, &err))
141 .await;
142 Err(err.to_string())
143 }
144 }
145 }
146
147 async fn fetch_asset_generic<T: serde::de::DeserializeOwned>(
149 &self,
150 asset_id: &str,
151 ) -> Result<T, String> {
152 let base_url = self
153 .get_active_url()
154 .await
155 .ok_or_else(|| "No active connection".to_string())?;
156 let settings = self.settings_snapshot();
157 let url = format!("{}/api/assets/{}", base_url, asset_id);
158 match self
159 .client
160 .get(&url)
161 .header("x-api-key", &settings.api_key)
162 .header("Accept", "application/json")
163 .timeout(Duration::from_secs(10))
164 .send()
165 .await
166 {
167 Ok(resp) if resp.status().is_success() => {
168 resp.json::<T>().await.map_err(|err| err.to_string())
169 }
170 Ok(resp) => Err(format!("HTTP {}", resp.status())),
171 Err(err) => Err(err.to_string()),
172 }
173 }
174
175 pub async fn fetch_asset_details(&self, asset_id: &str) -> Result<AssetDetails, String> {
177 self.fetch_asset_generic(asset_id).await
178 }
179
180 pub async fn fetch_asset_by_id(&self, asset_id: &str) -> Result<LibraryAsset, String> {
182 self.fetch_asset_generic(asset_id).await
183 }
184
185 pub async fn download_original_to_file(
187 &self,
188 asset_id: &str,
189 output_path: &std::path::Path,
190 progress: Option<TransferProgressCallback>,
191 ) -> Result<(), String> {
192 let base_url = self
193 .get_active_url()
194 .await
195 .ok_or_else(|| "No active connection".to_string())?;
196 let settings = self.settings_snapshot();
197 let url = format!("{}/api/assets/{}/original", base_url, asset_id);
198
199 match self
200 .client
201 .get(&url)
202 .header("x-api-key", &settings.api_key)
203 .header("Accept", "application/octet-stream")
204 .timeout(Duration::from_secs(300))
205 .send()
206 .await
207 {
208 Ok(mut resp) if resp.status().is_success() => {
209 use tokio::io::AsyncWriteExt;
210 let total_bytes = resp.content_length();
211 let mut written = 0_u64;
212 let mut file = tokio::fs::File::create(output_path)
213 .await
214 .map_err(|e| e.to_string())?;
215 while let Some(chunk) = resp.chunk().await.map_err(|e| e.to_string())? {
216 file.write_all(&chunk).await.map_err(|e| e.to_string())?;
217 written = written.saturating_add(chunk.len() as u64);
218 if let Some(callback) = &progress {
219 callback(written, total_bytes);
220 }
221 }
222 self.clear_issue().await;
223 Ok(())
224 }
225 Ok(resp) => {
226 self.set_issue(classify_http_issue(
227 RequestContext::AssetDownload,
228 resp.status().as_u16(),
229 Some(asset_id),
230 ))
231 .await;
232 Err(format!("HTTP {}", resp.status()))
233 }
234 Err(err) => {
235 *self.active_url.lock().await = None;
236 self.set_issue(classify_network_issue(RequestContext::AssetDownload, &err))
237 .await;
238 Err(err.to_string())
239 }
240 }
241 }
242
243 pub async fn fetch_current_user_id(&self) -> Result<String, String> {
245 let base_url = self
246 .get_active_url()
247 .await
248 .ok_or_else(|| "No active connection".to_string())?;
249 let settings = self.settings_snapshot();
250 let url = format!("{}/api/users/me", base_url);
251 match self
252 .client
253 .get(&url)
254 .header("x-api-key", &settings.api_key)
255 .header("Accept", "application/json")
256 .timeout(Duration::from_secs(10))
257 .send()
258 .await
259 {
260 Ok(resp) if resp.status().is_success() => {
261 let json: serde_json::Value = resp.json().await.map_err(|err| err.to_string())?;
262 json.get("id")
263 .and_then(|v| v.as_str())
264 .map(|s| s.to_string())
265 .ok_or_else(|| "Missing id in /users/me response".to_string())
266 }
267 Ok(resp) => Err(format!("HTTP {}", resp.status())),
268 Err(err) => Err(err.to_string()),
269 }
270 }
271
272 pub async fn delete_assets(&self, asset_ids: &[String]) -> Result<(), String> {
274 if asset_ids.is_empty() {
275 return Ok(());
276 }
277 let base_url = self
278 .get_active_url()
279 .await
280 .ok_or_else(|| "No active connection".to_string())?;
281 let settings = self.settings_snapshot();
282 let url = format!("{}/api/assets", base_url);
283 let body = serde_json::json!({
284 "ids": asset_ids,
285 "force": false,
286 });
287 match self
288 .client
289 .delete(&url)
290 .header("x-api-key", &settings.api_key)
291 .header("Content-Type", "application/json")
292 .header("Accept", "application/json")
293 .timeout(Duration::from_secs(15))
294 .body(body.to_string())
295 .send()
296 .await
297 {
298 Ok(resp) if resp.status().is_success() => {
299 self.clear_issue().await;
300 Ok(())
301 }
302 Ok(resp) => Err(format!("HTTP {}", resp.status())),
303 Err(err) => Err(err.to_string()),
304 }
305 }
306}