1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
use serde::{Deserialize, Serialize};
use crate::CyberdropError;
use crate::client::CyberdropClient;
use crate::files::AlbumFile;
/// Album metadata as returned by the Cyberdrop API.
///
/// Field semantics (timestamps/flags) are intentionally documented minimally: values are exposed
/// as returned by the service without additional interpretation.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Album {
/// Album numeric ID.
pub id: u64,
/// Display name.
pub name: String,
/// Service-provided timestamp value.
#[serde(default)]
pub timestamp: u64,
/// Service-provided identifier string.
pub identifier: String,
/// Service-provided "edited at" timestamp value.
#[serde(default)]
pub edited_at: u64,
/// Service-provided download flag.
#[serde(default)]
pub download: bool,
/// Service-provided public flag.
#[serde(default)]
pub public: bool,
/// Album description (may be empty).
#[serde(default)]
pub description: String,
/// Number of files in the album.
#[serde(default)]
pub files: u64,
}
/// Files returned by the album listing endpoint.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AlbumFiles {
/// Files collected for the album.
pub files: Vec<AlbumFile>,
/// Total number of files in the album.
pub count: u64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CreateAlbumRequest {
pub(crate) name: String,
pub(crate) description: Option<String>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct CreateAlbumResponse {
pub(crate) success: Option<bool>,
pub(crate) id: Option<u64>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct EditAlbumRequest {
pub(crate) id: u64,
pub(crate) name: String,
pub(crate) description: String,
pub(crate) download: bool,
pub(crate) public: bool,
#[serde(rename = "requestLink")]
pub(crate) request_link: bool,
}
#[derive(Debug, Deserialize)]
pub(crate) struct EditAlbumResponse {
pub(crate) success: Option<bool>,
pub(crate) name: Option<String>,
pub(crate) identifier: Option<String>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
/// Result of editing an album via [`crate::CyberdropClient::edit_album`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditAlbumResult {
/// Updated name if the API returned it.
pub name: Option<String>,
/// New identifier if `request_new_link` was set and the API returned it.
pub identifier: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct AlbumsResponse {
pub(crate) success: Option<bool>,
pub(crate) albums: Option<Vec<Album>>,
}
#[derive(Debug, Deserialize)]
pub(crate) struct AlbumFilesResponse {
pub(crate) success: Option<bool>,
pub(crate) files: Option<Vec<AlbumFile>>,
pub(crate) count: Option<u64>,
pub(crate) message: Option<String>,
pub(crate) description: Option<String>,
}
impl CyberdropClient {
/// Fetch an album from [`CyberdropClient::list_albums`] by numeric ID.
///
/// # Errors
///
/// Returns [`CyberdropError::AlbumNotFound`] if the authenticated account has no album with
/// `album_id`; otherwise returns the same errors as [`CyberdropClient::list_albums`].
pub async fn get_album_by_id(&self, album_id: u64) -> Result<Album, CyberdropError> {
let albums = self.list_albums().await?;
albums
.into_iter()
.find(|album| album.id == album_id)
.ok_or(CyberdropError::AlbumNotFound(album_id))
}
/// List albums for the authenticated user.
///
/// Requires an auth token (see [`CyberdropClient::with_auth_token`]).
///
/// # Errors
///
/// - [`CyberdropError::MissingAuthToken`] if the client has no configured token
/// - [`CyberdropError::AuthenticationFailed`] / [`CyberdropError::RequestFailed`] for non-2xx statuses
/// - [`CyberdropError::MissingField`] if expected fields are missing in the response body
/// - [`CyberdropError::Http`] for transport failures (including timeouts)
pub async fn list_albums(&self) -> Result<Vec<Album>, CyberdropError> {
let response: AlbumsResponse = self
.get_json_with_header("api/albums", true, "Simple", "1")
.await?;
if !response.success.unwrap_or(false) {
return Err(CyberdropError::Api("failed to fetch albums".into()));
}
response.albums.ok_or(CyberdropError::MissingField(
"albums response missing albums",
))
}
/// List all files in an album ("folder") by iterating pages until exhaustion.
///
/// Starts at `page = 0` and stops when:
/// - enough files have been collected to satisfy the API-reported `count`, or
/// - a page returns zero files.
///
/// Requires an auth token (see [`CyberdropClient::with_auth_token`]).
///
/// # Returns
///
/// An [`AlbumFiles`] value containing all collected files and the API-reported total count.
///
/// # Errors
///
/// - [`CyberdropError::MissingAuthToken`] if the client has no configured token
/// - [`CyberdropError::AuthenticationFailed`] / [`CyberdropError::RequestFailed`] for non-2xx statuses
/// - [`CyberdropError::Api`] for service-reported failures
/// - [`CyberdropError::MissingField`] if expected fields are missing in the response body
/// - [`CyberdropError::Http`] for transport failures (including timeouts)
pub async fn list_album_files(&self, album_id: u64) -> Result<AlbumFiles, CyberdropError> {
let mut page = 0u64;
let mut all_files = Vec::new();
let mut total_count = None::<u64>;
loop {
let path = format!("api/album/{album_id}/{page}");
let response: AlbumFilesResponse = self.get_json(&path, true).await?;
if !response.success.unwrap_or(false) {
let msg = response
.description
.or(response.message)
.unwrap_or_else(|| "failed to fetch album files".to_string());
return Err(CyberdropError::Api(msg));
}
let mut res = AlbumFiles {
files: response.files.ok_or(CyberdropError::MissingField(
"album files response missing files",
))?,
count: response.count.ok_or(CyberdropError::MissingField(
"album files response missing count",
))?,
};
if total_count.is_none() {
total_count = Some(res.count);
}
if res.files.is_empty() {
break;
}
all_files.append(&mut res.files);
if let Some(total) = total_count
&& all_files.len() as u64 >= total
{
break;
}
page += 1;
}
Ok(AlbumFiles {
files: all_files,
count: total_count.unwrap_or(0),
})
}
/// Create a new album and return its numeric ID.
///
/// Requires an auth token. If the service reports that an album with a similar name already
/// exists, this returns [`CyberdropError::AlbumAlreadyExists`].
///
/// # Errors
///
/// - [`CyberdropError::MissingAuthToken`] if the client has no configured token
/// - [`CyberdropError::AuthenticationFailed`] / [`CyberdropError::RequestFailed`] for non-2xx statuses
/// - [`CyberdropError::AlbumAlreadyExists`] if the service indicates an album already exists
/// - [`CyberdropError::Api`] for other service-reported failures
/// - [`CyberdropError::MissingField`] if expected fields are missing in the response body
/// - [`CyberdropError::Http`] for transport failures (including timeouts)
pub async fn create_album(
&self,
name: impl Into<String>,
description: Option<impl Into<String>>,
) -> Result<u64, CyberdropError> {
let payload = CreateAlbumRequest {
name: name.into(),
description: description.map(Into::into),
};
let response: CreateAlbumResponse = self.post_json("api/albums", &payload, true).await?;
if response.success.unwrap_or(false) {
return response.id.ok_or(CyberdropError::MissingField(
"create album response missing id",
));
}
let msg = response
.description
.or(response.message)
.unwrap_or_else(|| "create album failed".to_string());
if msg.to_lowercase().contains("already an album") {
Err(CyberdropError::AlbumAlreadyExists(msg))
} else {
Err(CyberdropError::Api(msg))
}
}
/// Edit an existing album ("folder").
///
/// This endpoint updates album metadata such as name/description and visibility flags.
/// It can also request a new link identifier.
///
/// Requires an auth token.
///
/// # Returns
///
/// The API returns either a `name` (typical edits) or an `identifier` (when requesting a new
/// link). This crate exposes both as optional fields on [`EditAlbumResult`].
///
/// # Errors
///
/// - [`CyberdropError::MissingAuthToken`] if the client has no configured token
/// - [`CyberdropError::AuthenticationFailed`] / [`CyberdropError::RequestFailed`] for non-2xx statuses
/// - [`CyberdropError::Api`] for service-reported failures
/// - [`CyberdropError::MissingField`] if the response is missing expected fields
/// - [`CyberdropError::Http`] for transport failures (including timeouts)
pub async fn edit_album(
&self,
id: u64,
name: impl Into<String>,
description: impl Into<String>,
download: bool,
public: bool,
request_new_link: bool,
) -> Result<EditAlbumResult, CyberdropError> {
let payload = EditAlbumRequest {
id,
name: name.into(),
description: description.into(),
download,
public,
request_link: request_new_link,
};
let response: EditAlbumResponse = self.post_json("api/albums/edit", &payload, true).await?;
if !response.success.unwrap_or(false) {
let msg = response
.description
.or(response.message)
.unwrap_or_else(|| "edit album failed".to_string());
return Err(CyberdropError::Api(msg));
}
if response.name.is_none() && response.identifier.is_none() {
return Err(CyberdropError::MissingField(
"edit album response missing name/identifier",
));
}
Ok(EditAlbumResult {
name: response.name,
identifier: response.identifier,
})
}
}