fix: resolve KLIPY media through the items endpoint (#1592)

This commit is contained in:
Hampus
2026-08-14 19:38:37 +02:00
committed by GitHub
parent 01432bc682
commit 8a9b12e6a1
2 changed files with 332 additions and 61 deletions
+237 -13
View File
@@ -65,11 +65,28 @@ struct KlipyGif {
#[serde(default)] #[serde(default)]
itemurl: Option<String>, itemurl: Option<String>,
#[serde(default)] #[serde(default)]
file: Option<BTreeMap<String, BTreeMap<String, KlipyMediaEntry>>>, file: Option<BTreeMap<String, KlipyFileGroup>>,
#[serde(default)]
file_meta: Option<BTreeMap<String, KlipyFileMeta>>,
#[serde(default)] #[serde(default)]
media_formats: Option<BTreeMap<String, KlipyMediaEntry>>, media_formats: Option<BTreeMap<String, KlipyMediaEntry>>,
} }
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum KlipyFileGroup {
Sized(BTreeMap<String, KlipyMediaEntry>),
Flat(KlipyMediaEntry),
}
#[derive(Debug, Deserialize)]
struct KlipyFileMeta {
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
enum KlipyMediaEntry { enum KlipyMediaEntry {
@@ -94,7 +111,13 @@ struct KlipyCategoryTag {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct DirectGifResponse { struct DirectGifResponse {
#[serde(default)] #[serde(default)]
data: Option<KlipyGif>, data: Option<DirectGifItems>,
}
#[derive(Debug, Deserialize)]
struct DirectGifItems {
#[serde(default)]
data: Vec<KlipyGif>,
} }
enum KlipyJsonFetch<T> { enum KlipyJsonFetch<T> {
@@ -353,6 +376,7 @@ impl KlipyClient {
KlipyJsonFetch::NotFound => Ok(None), KlipyJsonFetch::NotFound => Ok(None),
KlipyJsonFetch::Found(response) => Ok(response KlipyJsonFetch::Found(response) => Ok(response
.data .data
.and_then(|items| items.data.into_iter().next())
.and_then(|gif| self.transform_gif_with_path(gif, Some(path)))), .and_then(|gif| self.transform_gif_with_path(gif, Some(path)))),
} }
} }
@@ -426,7 +450,7 @@ impl KlipyClient {
} }
fn create_direct_url(&self, api_key: &str, path: &KlipyPath) -> anyhow::Result<Url> { fn create_direct_url(&self, api_key: &str, path: &KlipyPath) -> anyhow::Result<Url> {
let mut url = Url::parse(&format!("{KLIPY_DIRECT_BASE_URL}/"))?; let mut url = Url::parse(KLIPY_DIRECT_BASE_URL)?;
{ {
let mut segments = url let mut segments = url
.path_segments_mut() .path_segments_mut()
@@ -434,8 +458,9 @@ impl KlipyClient {
segments segments
.push(api_key) .push(api_key)
.push(klipy_resource(path.path_type)) .push(klipy_resource(path.path_type))
.push(&path.slug); .push("items");
} }
url.query_pairs_mut().append_pair("slugs", &path.slug);
Ok(url) Ok(url)
} }
@@ -495,14 +520,16 @@ impl KlipyClient {
let mut media = BTreeMap::new(); let mut media = BTreeMap::new();
let mut preferred = None; let mut preferred = None;
for size in SIZE_PREFERENCE { for size in SIZE_PREFERENCE {
let Some(bucket) = input.file.as_ref().and_then(|files| files.get(size)) else { let Some(KlipyFileGroup::Sized(bucket)) =
input.file.as_ref().and_then(|files| files.get(size))
else {
continue; continue;
}; };
for format in FILE_FORMAT_PREFERENCE { for format in FILE_FORMAT_PREFERENCE {
let Some(entry) = bucket.get(format) else { let Some(entry) = bucket.get(format) else {
continue; continue;
}; };
let Some(media_format) = self.to_media_format(entry) else { let Some(media_format) = self.to_media_format(entry, None) else {
continue; continue;
}; };
let public_key = public_format_key(size, format); let public_key = public_format_key(size, format);
@@ -512,12 +539,32 @@ impl KlipyClient {
} }
} }
} }
for format in FILE_FORMAT_PREFERENCE {
let Some(KlipyFileGroup::Flat(entry)) =
input.file.as_ref().and_then(|files| files.get(format))
else {
continue;
};
let meta = input
.file_meta
.as_ref()
.and_then(|file_meta| file_meta.get(format));
let Some(media_format) = self.to_media_format(entry, meta) else {
continue;
};
media
.entry(format.to_owned())
.or_insert_with(|| media_format.clone());
if preferred.is_none() {
preferred = Some(media_format);
}
}
if let Some(formats) = input.media_formats.as_ref() { if let Some(formats) = input.media_formats.as_ref() {
for format in MEDIA_FORMAT_PREFERENCE { for format in MEDIA_FORMAT_PREFERENCE {
let Some(entry) = formats.get(format) else { let Some(entry) = formats.get(format) else {
continue; continue;
}; };
let Some(media_format) = self.to_media_format(entry) else { let Some(media_format) = self.to_media_format(entry, None) else {
continue; continue;
}; };
media media
@@ -534,7 +581,7 @@ impl KlipyClient {
{ {
continue; continue;
} }
let Some(media_format) = self.to_media_format(entry) else { let Some(media_format) = self.to_media_format(entry, None) else {
continue; continue;
}; };
media media
@@ -548,8 +595,12 @@ impl KlipyClient {
(media, preferred) (media, preferred)
} }
fn to_media_format(&self, entry: &KlipyMediaEntry) -> Option<GifMediaFormat> { fn to_media_format(
let (src, width, height) = entry.media_parts()?; &self,
entry: &KlipyMediaEntry,
meta: Option<&KlipyFileMeta>,
) -> Option<GifMediaFormat> {
let (src, width, height) = entry.media_parts(meta)?;
let proxy_src = self.media_proxy.external_proxy_url(src)?; let proxy_src = self.media_proxy.external_proxy_url(src)?;
Some(GifMediaFormat { Some(GifMediaFormat {
src: src.to_owned(), src: src.to_owned(),
@@ -561,11 +612,16 @@ impl KlipyClient {
} }
impl KlipyMediaEntry { impl KlipyMediaEntry {
fn media_parts(&self) -> Option<(&str, i32, i32)> { fn media_parts(&self, meta: Option<&KlipyFileMeta>) -> Option<(&str, i32, i32)> {
match self { match self {
KlipyMediaEntry::Url(url) => { KlipyMediaEntry::Url(url) => {
let _ = url; let src = url.trim();
None if src.is_empty() {
return None;
}
let meta = meta?;
let (width, height) = valid_dimensions(meta.width?, meta.height?)?;
Some((src, width, height))
} }
KlipyMediaEntry::Object { KlipyMediaEntry::Object {
url, url,
@@ -744,6 +800,174 @@ mod tests {
); );
} }
#[test]
fn direct_url_targets_the_items_endpoint() {
let client = KlipyClient::new(MediaProxyUrlBuilder::for_test(
"https://media.example.test",
"secret",
))
.expect("client");
let url = client
.create_direct_url(
"secret/key",
&KlipyPath {
path_type: KlipyPathType::Gif,
slug: "walter blame government-1".to_owned(),
},
)
.expect("direct URL");
assert_eq!(
url.as_str(),
"https://api.klipy.com/api/v1/secret%2Fkey/gifs/items?slugs=walter+blame+government-1"
);
let clip_url = client
.create_direct_url(
"key",
&KlipyPath {
path_type: KlipyPathType::Clip,
slug: "kittens".to_owned(),
},
)
.expect("direct URL");
assert_eq!(
clip_url.as_str(),
"https://api.klipy.com/api/v1/key/clips/items?slugs=kittens"
);
}
#[test]
fn transforms_direct_gif_item_sizes() {
let client = KlipyClient::new(MediaProxyUrlBuilder::for_test(
"https://media.example.test",
"secret",
))
.expect("client");
let input = serde_json::from_value::<KlipyGif>(serde_json::json!({
"id": 5441109273429299_i64,
"slug": "walter-blame-government-1",
"title": "Walter blame government",
"type": "gif",
"file": {
"hd": {
"gif": {"url": "https://static.klipy.com/hd.gif", "width": 498, "height": 420, "size": 1},
"webp": {"url": "https://static.klipy.com/hd.webp", "width": 498, "height": 420, "size": 1},
"webm": {"url": "https://static.klipy.com/hd.webm", "width": 498, "height": 420, "size": 1}
},
"sm": {
"webp": {"url": "https://static.klipy.com/sm.webp", "width": 165, "height": 139, "size": 1}
}
}
}))
.expect("fixture");
let gif = client
.transform_gif_with_path(
input,
Some(&KlipyPath {
path_type: KlipyPathType::Gif,
slug: "walter-blame-government-1".to_owned(),
}),
)
.expect("transformed gif");
assert_eq!(gif.id, "walter-blame-government-1");
assert_eq!(gif.url, "https://klipy.com/gifs/walter-blame-government-1");
assert_eq!(gif.src, "https://static.klipy.com/hd.webm");
assert_eq!((gif.width, gif.height), (498, 420));
assert_eq!(
gif.media.get("webp").map(|format| format.src.as_str()),
Some("https://static.klipy.com/hd.webp")
);
assert_eq!(
gif.media
.get("tinywebp")
.map(|format| (format.width, format.height)),
Some((165, 139))
);
}
#[test]
fn transforms_direct_clip_item_using_file_meta_dimensions() {
let client = KlipyClient::new(MediaProxyUrlBuilder::for_test(
"https://media.example.test",
"secret",
))
.expect("client");
let input = serde_json::from_value::<KlipyGif>(serde_json::json!({
"id": 6641306069493365_i64,
"url": "https://klipy.com/clips/kittens",
"slug": "kittens",
"title": "Kittens",
"type": "clip",
"file": {
"mp4": "https://static.klipy.com/clip.mp4",
"gif": "https://static.klipy.com/clip.gif",
"webp": "https://static.klipy.com/clip.webp"
},
"file_meta": {
"mp4": {"width": 854, "height": 480, "size": 924555},
"gif": {"width": 320, "height": 180, "size": 4117532},
"webp": {"width": 320, "height": 180, "size": 625686}
}
}))
.expect("fixture");
let clip = client
.transform_gif_with_path(
input,
Some(&KlipyPath {
path_type: KlipyPathType::Clip,
slug: "kittens".to_owned(),
}),
)
.expect("transformed clip");
assert_eq!(clip.id, "kittens");
assert_eq!(clip.url, "https://klipy.com/clips/kittens");
assert_eq!(clip.src, "https://static.klipy.com/clip.mp4");
assert_eq!((clip.width, clip.height), (854, 480));
assert_eq!(
clip.media
.get("webp")
.map(|format| (format.src.as_str(), format.width, format.height)),
Some(("https://static.klipy.com/clip.webp", 320, 180))
);
assert!(
clip.media
.get("mp4")
.expect("mp4 format")
.proxy_src
.starts_with("https://media.example.test/external/")
);
}
#[test]
fn direct_response_unwraps_the_items_array() {
let response = serde_json::from_value::<DirectGifResponse>(serde_json::json!({
"result": true,
"data": {
"data": [{
"id": 1_i64,
"slug": "kittens",
"title": "Kittens",
"file": {"mp4": "https://static.klipy.com/clip.mp4"}
}],
"meta": {}
}
}))
.expect("fixture");
let items = response.data.expect("data");
assert_eq!(items.data.len(), 1);
assert_eq!(items.data[0].slug.as_deref(), Some("kittens"));
let empty = serde_json::from_value::<DirectGifResponse>(serde_json::json!({
"result": true,
"data": {"data": [], "meta": {}}
}))
.expect("fixture");
assert!(empty.data.expect("data").data.is_empty());
}
#[test] #[test]
fn build_share_url_uses_gifs_path() { fn build_share_url_uses_gifs_path() {
assert_eq!(build_share_url("hello"), "https://klipy.com/gifs/hello"); assert_eq!(build_share_url("hello"), "https://klipy.com/gifs/hello");
+95 -48
View File
@@ -172,21 +172,27 @@ async fn resolve_media_via_api(
} }
let payload: serde_json::Value = serde_json::from_slice(&response.bytes)?; let payload: serde_json::Value = serde_json::from_slice(&response.bytes)?;
Ok(payload.get("data").and_then(extract_klipy_api_media)) Ok(payload
.pointer("/data/data/0")
.and_then(extract_klipy_api_media))
} }
fn klipy_direct_url(api_key: &str, resource: &str, slug: &str) -> anyhow::Result<Url> { fn klipy_direct_url(api_key: &str, resource: &str, slug: &str) -> anyhow::Result<Url> {
Ok(Url::parse(&format!( let mut url = Url::parse(KLIPY_API_V1_BASE_URL)?;
"{KLIPY_API_V1_BASE_URL}/{api_key}/{resource}/{slug}" url.path_segments_mut()
))?) .map_err(|_| anyhow::anyhow!("KLIPY API base URL cannot be a base"))?
.push(api_key)
.push(resource)
.push("items");
url.query_pairs_mut().append_pair("slugs", slug);
Ok(url)
} }
fn extract_klipy_api_media(item: &serde_json::Value) -> Option<KlipyMediaFormats> { fn extract_klipy_api_media(item: &serde_json::Value) -> Option<KlipyMediaFormats> {
let file = item.get("file"); let file = item.get("file")?;
let thumbnail = file.and_then(|file| pick_klipy_file_format(file, KLIPY_THUMBNAIL_FORMATS)); let file_meta = item.get("file_meta");
let video = file let thumbnail = pick_klipy_file_format(file, file_meta, KLIPY_THUMBNAIL_FORMATS);
.and_then(|file| pick_klipy_file_format(file, KLIPY_VIDEO_FORMATS)) let video = pick_klipy_file_format(file, file_meta, KLIPY_VIDEO_FORMATS);
.or_else(|| extract_klipy_fallback_webm(item.pointer("/media_formats/webm")));
if thumbnail.is_none() && video.is_none() { if thumbnail.is_none() && video.is_none() {
return None; return None;
@@ -194,7 +200,11 @@ fn extract_klipy_api_media(item: &serde_json::Value) -> Option<KlipyMediaFormats
Some(KlipyMediaFormats { thumbnail, video }) Some(KlipyMediaFormats { thumbnail, video })
} }
fn pick_klipy_file_format(file: &serde_json::Value, formats: &[&str]) -> Option<KlipyMediaFormat> { fn pick_klipy_file_format(
file: &serde_json::Value,
file_meta: Option<&serde_json::Value>,
formats: &[&str],
) -> Option<KlipyMediaFormat> {
for size in KLIPY_SIZE_PREFERENCE { for size in KLIPY_SIZE_PREFERENCE {
for media_format in formats { for media_format in formats {
if let Some(media) = if let Some(media) =
@@ -205,35 +215,30 @@ fn pick_klipy_file_format(file: &serde_json::Value, formats: &[&str]) -> Option<
} }
} }
for media_format in formats { for media_format in formats {
if let Some(media) = extract_media_format(file.get(*media_format)) { let Some(mut media) = extract_media_format(file.get(*media_format)) else {
return Some(media); continue;
};
if media.width.is_none() {
media.width = klipy_meta_dimension(file_meta, media_format, "width");
} }
if media.height.is_none() {
media.height = klipy_meta_dimension(file_meta, media_format, "height");
}
return Some(media);
} }
None None
} }
fn extract_klipy_fallback_webm(value: Option<&serde_json::Value>) -> Option<KlipyMediaFormat> { fn klipy_meta_dimension(
let value = value?; file_meta: Option<&serde_json::Value>,
let url = value media_format: &str,
.get("url") key: &str,
.and_then(|v| v.as_str()) ) -> Option<u32> {
.filter(|url| !url.is_empty())?; file_meta?
let dims = value.get("dims")?.as_array()?; .pointer(&format!("/{media_format}/{key}"))
let width = dims .and_then(serde_json::Value::as_u64)
.first()
.and_then(|value| value.as_i64())
.filter(|value| *value > 0) .filter(|value| *value > 0)
.and_then(|value| u32::try_from(value).ok()); .and_then(|value| u32::try_from(value).ok())
let height = dims
.get(1)
.and_then(|value| value.as_i64())
.filter(|value| *value > 0)
.and_then(|value| u32::try_from(value).ok());
Some(KlipyMediaFormat {
url: Some(url.to_owned()),
width,
height,
})
} }
fn resolve_relative_url(base_url: &Url, media_url: &str) -> Option<String> { fn resolve_relative_url(base_url: &Url, media_url: &str) -> Option<String> {
@@ -404,7 +409,7 @@ mod tests {
"webp": {"url": "https://img.klipy.com/sm.webp", "width": 165, "height": 294} "webp": {"url": "https://img.klipy.com/sm.webp", "width": 165, "height": 294}
} }
}); });
let thumbnail = pick_klipy_file_format(&file, KLIPY_THUMBNAIL_FORMATS).unwrap(); let thumbnail = pick_klipy_file_format(&file, None, KLIPY_THUMBNAIL_FORMATS).unwrap();
assert_eq!( assert_eq!(
thumbnail.url.as_deref(), thumbnail.url.as_deref(),
Some("https://img.klipy.com/hd.webp") Some("https://img.klipy.com/hd.webp")
@@ -412,7 +417,7 @@ mod tests {
assert_eq!(thumbnail.width, Some(254)); assert_eq!(thumbnail.width, Some(254));
assert_eq!(thumbnail.height, Some(450)); assert_eq!(thumbnail.height, Some(450));
assert_eq!( assert_eq!(
pick_klipy_file_format(&file, KLIPY_VIDEO_FORMATS) pick_klipy_file_format(&file, None, KLIPY_VIDEO_FORMATS)
.unwrap() .unwrap()
.url .url
.as_deref(), .as_deref(),
@@ -427,14 +432,14 @@ mod tests {
"gif": "https://img.klipy.com/c.gif", "gif": "https://img.klipy.com/c.gif",
"webp": "https://img.klipy.com/c.webp" "webp": "https://img.klipy.com/c.webp"
}); });
let thumbnail = pick_klipy_file_format(&file, KLIPY_THUMBNAIL_FORMATS).unwrap(); let thumbnail = pick_klipy_file_format(&file, None, KLIPY_THUMBNAIL_FORMATS).unwrap();
assert_eq!( assert_eq!(
thumbnail.url.as_deref(), thumbnail.url.as_deref(),
Some("https://img.klipy.com/c.webp") Some("https://img.klipy.com/c.webp")
); );
assert_eq!(thumbnail.width, None); assert_eq!(thumbnail.width, None);
assert_eq!( assert_eq!(
pick_klipy_file_format(&file, KLIPY_VIDEO_FORMATS) pick_klipy_file_format(&file, None, KLIPY_VIDEO_FORMATS)
.unwrap() .unwrap()
.url .url
.as_deref(), .as_deref(),
@@ -443,24 +448,66 @@ mod tests {
} }
#[test] #[test]
fn extract_klipy_api_media_uses_fallback_webm_shape() { fn klipy_direct_url_targets_the_items_endpoint() {
let url = klipy_direct_url("secret/key", "gifs", "walter blame government-1").unwrap();
assert_eq!(
url.as_str(),
"https://api.klipy.com/api/v1/secret%2Fkey/gifs/items?slugs=walter+blame+government-1"
);
}
#[test]
fn extract_klipy_api_media_reads_the_gif_item_shape() {
let item = serde_json::json!({ let item = serde_json::json!({
"media_formats": { "slug": "walter-blame-government-1",
"webm": { "file": {
"url": "https://img.klipy.com/fallback.webm", "hd": {
"dims": [320, 180] "gif": {"url": "https://img.klipy.com/hd.gif", "width": 498, "height": 420},
"webp": {"url": "https://img.klipy.com/hd.webp", "width": 498, "height": 420},
"webm": {"url": "https://img.klipy.com/hd.webm", "width": 498, "height": 420}
} }
} }
}); });
let media = extract_klipy_api_media(&item).unwrap(); let media = extract_klipy_api_media(&item).unwrap();
let video = media.video.unwrap(); let thumbnail = media.thumbnail.unwrap();
assert_eq!( assert_eq!(
video.url.as_deref(), thumbnail.url.as_deref(),
Some("https://img.klipy.com/fallback.webm") Some("https://img.klipy.com/hd.webp")
); );
assert_eq!(video.width, Some(320)); assert_eq!(thumbnail.width, Some(498));
assert_eq!(video.height, Some(180)); assert_eq!(thumbnail.height, Some(420));
assert!(media.thumbnail.is_none()); let video = media.video.unwrap();
assert_eq!(video.url.as_deref(), Some("https://img.klipy.com/hd.webm"));
assert_eq!(extract_klipy_api_media(&serde_json::json!({})), None);
}
#[test]
fn extract_klipy_api_media_reads_clip_dimensions_from_file_meta() {
let item = serde_json::json!({
"slug": "kittens",
"file": {
"mp4": "https://img.klipy.com/clip.mp4",
"gif": "https://img.klipy.com/clip.gif",
"webp": "https://img.klipy.com/clip.webp"
},
"file_meta": {
"mp4": {"width": 854, "height": 480, "size": 924555},
"gif": {"width": 320, "height": 180, "size": 4117532},
"webp": {"width": 320, "height": 180, "size": 625686}
}
});
let media = extract_klipy_api_media(&item).unwrap();
let thumbnail = media.thumbnail.unwrap();
assert_eq!(
thumbnail.url.as_deref(),
Some("https://img.klipy.com/clip.webp")
);
assert_eq!(thumbnail.width, Some(320));
assert_eq!(thumbnail.height, Some(180));
let video = media.video.unwrap();
assert_eq!(video.url.as_deref(), Some("https://img.klipy.com/clip.mp4"));
assert_eq!(video.width, Some(854));
assert_eq!(video.height, Some(480));
} }
#[test] #[test]