1use serde::{Deserialize, Serialize};
2
3use crate::CyberdropError;
4use crate::client::CyberdropClient;
5use crate::files::AlbumFile;
6
7#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct Album {
14 pub id: u64,
16 pub name: String,
18 #[serde(default)]
20 pub timestamp: u64,
21 pub identifier: String,
23 #[serde(default)]
25 pub edited_at: u64,
26 #[serde(default)]
28 pub download: bool,
29 #[serde(default)]
31 pub public: bool,
32 #[serde(default)]
34 pub description: String,
35 #[serde(default)]
37 pub files: u64,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct AlbumFiles {
43 pub files: Vec<AlbumFile>,
45 pub count: u64,
47}
48
49#[derive(Debug, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub(crate) struct CreateAlbumRequest {
52 pub(crate) name: String,
53 pub(crate) description: Option<String>,
54}
55
56#[derive(Debug, Deserialize)]
57pub(crate) struct CreateAlbumResponse {
58 pub(crate) success: Option<bool>,
59 pub(crate) id: Option<u64>,
60 pub(crate) message: Option<String>,
61 pub(crate) description: Option<String>,
62}
63
64#[derive(Debug, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub(crate) struct EditAlbumRequest {
67 pub(crate) id: u64,
68 pub(crate) name: String,
69 pub(crate) description: String,
70 pub(crate) download: bool,
71 pub(crate) public: bool,
72 #[serde(rename = "requestLink")]
73 pub(crate) request_link: bool,
74}
75
76#[derive(Debug, Deserialize)]
77pub(crate) struct EditAlbumResponse {
78 pub(crate) success: Option<bool>,
79 pub(crate) name: Option<String>,
80 pub(crate) identifier: Option<String>,
81 pub(crate) message: Option<String>,
82 pub(crate) description: Option<String>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct EditAlbumResult {
88 pub name: Option<String>,
90 pub identifier: Option<String>,
92}
93
94#[derive(Debug, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub(crate) struct AlbumsResponse {
97 pub(crate) success: Option<bool>,
98 pub(crate) albums: Option<Vec<Album>>,
99}
100
101#[derive(Debug, Deserialize)]
102pub(crate) struct AlbumFilesResponse {
103 pub(crate) success: Option<bool>,
104 pub(crate) files: Option<Vec<AlbumFile>>,
105 pub(crate) count: Option<u64>,
106 pub(crate) message: Option<String>,
107 pub(crate) description: Option<String>,
108}
109
110impl CyberdropClient {
111 pub async fn get_album_by_id(&self, album_id: u64) -> Result<Album, CyberdropError> {
118 let albums = self.list_albums().await?;
119 albums
120 .into_iter()
121 .find(|album| album.id == album_id)
122 .ok_or(CyberdropError::AlbumNotFound(album_id))
123 }
124
125 pub async fn list_albums(&self) -> Result<Vec<Album>, CyberdropError> {
136 let response: AlbumsResponse = self
137 .get_json_with_header("api/albums", true, "Simple", "1")
138 .await?;
139
140 if !response.success.unwrap_or(false) {
141 return Err(CyberdropError::Api("failed to fetch albums".into()));
142 }
143
144 response.albums.ok_or(CyberdropError::MissingField(
145 "albums response missing albums",
146 ))
147 }
148
149 pub async fn list_album_files(&self, album_id: u64) -> Result<AlbumFiles, CyberdropError> {
169 let mut page = 0u64;
170 let mut all_files = Vec::new();
171 let mut total_count = None::<u64>;
172
173 loop {
174 let path = format!("api/album/{album_id}/{page}");
175 let response: AlbumFilesResponse = self.get_json(&path, true).await?;
176
177 if !response.success.unwrap_or(false) {
178 let msg = response
179 .description
180 .or(response.message)
181 .unwrap_or_else(|| "failed to fetch album files".to_string());
182 return Err(CyberdropError::Api(msg));
183 }
184
185 let mut res = AlbumFiles {
186 files: response.files.ok_or(CyberdropError::MissingField(
187 "album files response missing files",
188 ))?,
189 count: response.count.ok_or(CyberdropError::MissingField(
190 "album files response missing count",
191 ))?,
192 };
193
194 if total_count.is_none() {
195 total_count = Some(res.count);
196 }
197
198 if res.files.is_empty() {
199 break;
200 }
201
202 all_files.append(&mut res.files);
203
204 if let Some(total) = total_count
205 && all_files.len() as u64 >= total
206 {
207 break;
208 }
209
210 page += 1;
211 }
212
213 Ok(AlbumFiles {
214 files: all_files,
215 count: total_count.unwrap_or(0),
216 })
217 }
218
219 pub async fn create_album(
233 &self,
234 name: impl Into<String>,
235 description: Option<impl Into<String>>,
236 ) -> Result<u64, CyberdropError> {
237 let payload = CreateAlbumRequest {
238 name: name.into(),
239 description: description.map(Into::into),
240 };
241
242 let response: CreateAlbumResponse = self.post_json("api/albums", &payload, true).await?;
243
244 if response.success.unwrap_or(false) {
245 return response.id.ok_or(CyberdropError::MissingField(
246 "create album response missing id",
247 ));
248 }
249
250 let msg = response
251 .description
252 .or(response.message)
253 .unwrap_or_else(|| "create album failed".to_string());
254
255 if msg.to_lowercase().contains("already an album") {
256 Err(CyberdropError::AlbumAlreadyExists(msg))
257 } else {
258 Err(CyberdropError::Api(msg))
259 }
260 }
261
262 pub async fn edit_album(
282 &self,
283 id: u64,
284 name: impl Into<String>,
285 description: impl Into<String>,
286 download: bool,
287 public: bool,
288 request_new_link: bool,
289 ) -> Result<EditAlbumResult, CyberdropError> {
290 let payload = EditAlbumRequest {
291 id,
292 name: name.into(),
293 description: description.into(),
294 download,
295 public,
296 request_link: request_new_link,
297 };
298
299 let response: EditAlbumResponse = self.post_json("api/albums/edit", &payload, true).await?;
300
301 if !response.success.unwrap_or(false) {
302 let msg = response
303 .description
304 .or(response.message)
305 .unwrap_or_else(|| "edit album failed".to_string());
306 return Err(CyberdropError::Api(msg));
307 }
308
309 if response.name.is_none() && response.identifier.is_none() {
310 return Err(CyberdropError::MissingField(
311 "edit album response missing name/identifier",
312 ));
313 }
314
315 Ok(EditAlbumResult {
316 name: response.name,
317 identifier: response.identifier,
318 })
319 }
320}