1use std::fmt::Display;
2
3use chrono::NaiveDateTime;
4use reqwasm::http::Request;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8const THREE_MINS: u32 = 3 * 60;
10
11#[derive(Debug, Clone, Error)]
12pub enum ApiError {
13 #[error("Network error: {0}")]
14 Network(String),
15 #[error("HTTP {status} error: {message}")]
16 Http { status: u16, message: String },
17 #[error("Serialization error: {0}")]
18 Serialization(String),
19 #[error("Deserialization error: {0}")]
20 Deserialization(String),
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub enum Update<T> {
25 Set(T),
26 Remove,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30pub struct AlbumID(pub i32);
31
32impl Display for AlbumID {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(f, "{}", self.0)
35 }
36}
37
38impl From<i32> for AlbumID {
39 fn from(id: i32) -> Self {
40 Self(id)
41 }
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
45pub struct PhotoID(pub i32);
46
47impl Display for PhotoID {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "{}", self.0)
50 }
51}
52
53impl From<i32> for PhotoID {
54 fn from(id: i32) -> Self {
55 Self(id)
56 }
57}
58
59#[derive(Debug)]
60pub struct UploadPhotoRequest {
61 title: Option<String>,
62 artist: Option<String>,
63 copyright: Option<String>,
64 date_taken: Option<NaiveDateTime>,
65 albums: Vec<AlbumID>,
66}
67
68#[derive(Debug, Serialize, Deserialize)]
69pub struct UpdatePhotoRequest {
70 pub title: Option<Update<String>>,
71 pub artist: Option<Update<String>>,
72 pub copyright: Option<Update<String>>,
73 pub date_taken: Option<Update<NaiveDateTime>>,
74}
75
76#[derive(Debug, Serialize, Deserialize)]
77pub struct CreateAlbumRequest {
78 pub name: String,
79 pub notes: Option<String>,
80}
81
82#[derive(Debug, Serialize, Deserialize)]
83pub struct UpdateAlbumRequest {
84 pub name: Option<String>,
85 pub notes: Option<Update<String>>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Album {
91 pub id: AlbumID,
92 pub name: String,
93 pub notes: Option<String>,
94 pub photos: Vec<PhotoID>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct Photo {
100 pub id: PhotoID,
101 pub url: String,
102 pub title: Option<String>,
103 pub notes: Option<String>,
104 pub artist: Option<String>,
105 pub copyright: Option<String>,
106 pub date_taken: Option<NaiveDateTime>,
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110pub struct UpdateSettingsRequest {
111 pub current_album_id: Option<Update<AlbumID>>,
112 pub interval_seconds: Option<i32>,
113}
114
115#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
116pub struct Interval(pub u32);
117
118impl Interval {
119 pub fn from_seconds(seconds: u32) -> Self {
120 Self(seconds)
121 }
122
123 pub fn seconds(&self) -> u32 {
124 self.0
125 }
126}
127
128impl Default for Interval {
129 fn default() -> Self {
130 Self(THREE_MINS)
131 }
132}
133
134impl From<i32> for Interval {
135 fn from(seconds: i32) -> Self {
136 Self(seconds as u32)
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Next {
142 pub photo: Photo,
143 pub interval: Interval,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CurrentAlbum {
148 pub album: AlbumID,
150 pub index: usize,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct RotationSettings {
156 pub current_album: Option<CurrentAlbum>,
157 pub interval: Interval,
159}
160
161impl Default for RotationSettings {
162 fn default() -> Self {
163 Self {
164 current_album: None,
165 interval: Interval::default(),
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
171pub struct Client {
172 base_url: String,
173}
174
175impl Client {
176 fn build_url(&self, path: impl AsRef<str>) -> String {
178 format!("{}{}", self.base_url.trim_end_matches('/'), path.as_ref())
179 }
180
181 async fn get<T>(&self, path: impl AsRef<str>) -> Result<T, ApiError>
183 where
184 T: for<'de> Deserialize<'de>,
185 {
186 let url = self.build_url(path);
187 let response = Request::get(&url)
188 .send()
189 .await
190 .map_err(|e| ApiError::Network(e.to_string()))?;
191
192 if !response.ok() {
193 return Err(ApiError::Http {
194 status: response.status(),
195 message: response.status_text(),
196 });
197 }
198
199 response
200 .json::<T>()
201 .await
202 .map_err(|e| ApiError::Deserialization(e.to_string()))
203 }
204
205 async fn post<T, B>(&self, path: impl AsRef<str>, body: &B) -> Result<T, ApiError>
207 where
208 T: for<'de> Deserialize<'de>,
209 B: Serialize,
210 {
211 let url = self.build_url(path);
212 let body_json =
213 serde_json::to_string(body).map_err(|e| ApiError::Serialization(e.to_string()))?;
214
215 let response = Request::post(&url)
216 .header("Content-Type", "application/json")
217 .body(body_json)
218 .send()
219 .await
220 .map_err(|e| ApiError::Network(e.to_string()))?;
221
222 if !response.ok() {
223 return Err(ApiError::Http {
224 status: response.status(),
225 message: response.status_text(),
226 });
227 }
228
229 response
230 .json::<T>()
231 .await
232 .map_err(|e| ApiError::Deserialization(e.to_string()))
233 }
234
235 async fn put<T, B>(&self, path: impl AsRef<str>, body: &B) -> Result<T, ApiError>
237 where
238 T: for<'de> Deserialize<'de>,
239 B: Serialize,
240 {
241 let url = self.build_url(path);
242 let body_json =
243 serde_json::to_string(body).map_err(|e| ApiError::Serialization(e.to_string()))?;
244
245 let response = Request::put(&url)
246 .header("Content-Type", "application/json")
247 .body(body_json)
248 .send()
249 .await
250 .map_err(|e| ApiError::Network(e.to_string()))?;
251
252 if !response.ok() {
253 return Err(ApiError::Http {
254 status: response.status(),
255 message: response.status_text(),
256 });
257 }
258
259 response
260 .json::<T>()
261 .await
262 .map_err(|e| ApiError::Deserialization(e.to_string()))
263 }
264
265 async fn delete<T>(&self, path: impl AsRef<str>) -> Result<T, ApiError>
267 where
268 T: for<'de> Deserialize<'de>,
269 {
270 let url = self.build_url(path);
271 let response = Request::delete(&url)
272 .send()
273 .await
274 .map_err(|e| ApiError::Network(e.to_string()))?;
275
276 if !response.ok() {
277 return Err(ApiError::Http {
278 status: response.status(),
279 message: response.status_text(),
280 });
281 }
282
283 response
284 .json::<T>()
285 .await
286 .map_err(|e| ApiError::Deserialization(e.to_string()))
287 }
288}
289
290impl Client {
292 pub fn new(base_url: impl Into<String>) -> Self {
293 Self {
294 base_url: base_url.into(),
295 }
296 }
297
298 pub async fn get_next(&self) -> Result<Next, ApiError> {
303 self.get("/api/next").await
304 }
305
306 pub async fn get_photos(&self) -> Result<Vec<Photo>, ApiError> {
311 self.get("/api/photos").await
312 }
313
314 pub async fn get_photo(&self, id: PhotoID) -> Result<Photo, ApiError> {
315 self.get(format!("/api/photos/{id}")).await
316 }
317
318 pub async fn update_photo(
319 &self,
320 id: PhotoID,
321 updates: &UpdatePhotoRequest,
322 ) -> Result<(), ApiError> {
323 self.put(format!("/api/photos/{id}"), updates).await
324 }
325
326 pub async fn delete_photo(&self, id: PhotoID) -> Result<(), ApiError> {
327 self.delete(format!("/api/photos/{id}")).await
328 }
329
330 pub async fn get_albums(&self) -> Result<Vec<Album>, ApiError> {
335 self.get("/api/albums").await
336 }
337
338 pub async fn get_album(&self, id: AlbumID) -> Result<Album, ApiError> {
339 self.get(format!("/api/albums/{id}")).await
340 }
341
342 pub async fn create_album(&self, req: &CreateAlbumRequest) -> Result<Album, ApiError> {
343 self.post("/api/albums", req).await
344 }
345
346 pub async fn update_album(
347 &self,
348 id: AlbumID,
349 updates: &UpdateAlbumRequest,
350 ) -> Result<(), ApiError> {
351 self.put(format!("/api/albums/{id}"), updates).await
352 }
353
354 pub async fn delete_album(&self, id: AlbumID) -> Result<(), ApiError> {
355 self.delete(format!("/api/albums/{id}")).await
356 }
357
358 pub async fn add_photo_to_album(
359 &self,
360 album_id: AlbumID,
361 photo_id: PhotoID,
362 ) -> Result<(), ApiError> {
363 self.post(format!("/api/albums/{album_id}/photos/{photo_id}"), &()).await
364 }
365
366 pub async fn remove_photo_from_album(
367 &self,
368 album_id: AlbumID,
369 photo_id: PhotoID,
370 ) -> Result<(), ApiError> {
371 self.delete(format!("/api/albums/{album_id}/photos/{photo_id}")).await
372 }
373
374 pub async fn get_settings(&self) -> Result<RotationSettings, ApiError> {
379 self.get("/api/settings").await
380 }
381
382 pub async fn update_settings(&self, updates: &UpdateSettingsRequest) -> Result<(), ApiError> {
383 self.put("/api/settings", updates).await
384 }
385}