commit 674db022036fc3b01b86cd84b98b31fe3e87faa9
parent dd7b453e6deb849b1262030d2803abc00abb702c
Author: Sylvia Ivory <git@sivory.net>
Date: Thu, 19 Jun 2025 17:58:56 -0700
Add downloading photos api
Diffstat:
4 files changed, 102 insertions(+), 2 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1297,10 +1297,12 @@ dependencies = [
name = "fjordgard-unsplash"
version = "0.1.0"
dependencies = [
+ "bytes",
"reqwest",
"serde",
"serde_json",
"serde_with",
+ "strum",
"thiserror 2.0.12",
"tokio",
]
diff --git a/crates/unsplash/Cargo.toml b/crates/unsplash/Cargo.toml
@@ -4,10 +4,12 @@ version = "0.1.0"
edition = "2024"
[dependencies]
+bytes = "1.10.1"
reqwest = { version = "0.12.20", features = ["json"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde_with = "3.13.0"
+strum = { version = "0.27.1", features = ["derive"] }
thiserror = "2.0.12"
[dev-dependencies]
diff --git a/crates/unsplash/src/lib.rs b/crates/unsplash/src/lib.rs
@@ -1,5 +1,6 @@
use std::fmt::Debug;
+use bytes::Bytes;
use reqwest::{
Client, StatusCode,
header::{self, HeaderMap, HeaderValue},
@@ -94,6 +95,16 @@ impl UnsplashClient {
photos,
})
}
+
+ pub async fn download_photo(&self, photo: &Photo, opts: Option<PhotoFetchOptions>) -> Result<Bytes> {
+ let mut req = self.client.get(&photo.urls.raw);
+
+ if let Some(ref query) = opts {
+ req = req.query(query);
+ }
+
+ Ok(req.send().await?.error_for_status()?.bytes().await?)
+ }
}
#[cfg(test)]
diff --git a/crates/unsplash/src/model.rs b/crates/unsplash/src/model.rs
@@ -1,6 +1,7 @@
-use std::collections::HashMap;
+use std::{collections::HashMap, fmt::Display};
-use serde::{Deserialize, Serialize};
+use serde::{Deserialize, Serialize, Serializer};
+use strum::Display;
#[derive(Deserialize, Debug)]
#[serde(untagged)]
@@ -132,3 +133,87 @@ pub struct UserSocials {
pub twitter_username: Option<String>,
pub paypal_email: Option<String>,
}
+
+#[derive(Display)]
+#[strum(serialize_all = "lowercase")]
+pub enum Crop {
+ Top,
+ Bottom,
+ Left,
+ Right,
+ Faces,
+ FocalPoint,
+ Edges,
+ Entropy
+}
+
+#[derive(Serialize)]
+#[serde(rename = "lowercase")]
+pub enum Format {
+ Avif,
+ Gif,
+ Jp2,
+ Jpg,
+ Json,
+ Jxr,
+ PJpg,
+ Mp4,
+ Png,
+ Png8,
+ Png32,
+ Webm,
+ Webp,
+ BlurHash
+}
+
+#[derive(Serialize)]
+#[serde(rename = "lowercase")]
+pub enum Auto {
+ Compress,
+ Enhance,
+ True,
+ Format,
+ Redeye
+}
+
+#[derive(Serialize)]
+#[serde(rename = "lowercase")]
+pub enum Fit {
+ Clamp,
+ Clip,
+ Crop,
+ FaceArea,
+ Fill,
+ FillMax,
+ Max,
+ Min,
+ Scale
+}
+
+#[serde_with::skip_serializing_none]
+#[derive(Serialize, Default)]
+pub struct PhotoFetchOptions {
+ pub w: Option<f64>,
+ pub h: Option<f64>,
+ #[serde(serialize_with = "csv")]
+ pub crop: Option<Vec<Crop>>,
+ pub fm: Option<Format>,
+ pub auto: Option<Auto>,
+ pub q: Option<usize>,
+ pub fit: Option<Fit>,
+ pub dpr: Option<usize>,
+}
+
+fn csv<S: Serializer, T: Display>(list: &Option<Vec<T>>, serializer: S) -> Result<S::Ok, S::Error> {
+ if let Some(list) = list {
+ let s: String = list
+ .iter()
+ .map(|v| v.to_string())
+ .collect::<Vec<String>>()
+ .join(",");
+
+ serializer.serialize_str(&s)
+ } else {
+ serializer.serialize_none()
+ }
+}