fix(gifs): expose Klipy animated image formats (#1271)

This commit is contained in:
Hampus
2026-07-03 19:28:07 +02:00
committed by GitHub
parent 0e7d49c950
commit f7a4d95ceb
2 changed files with 270 additions and 45 deletions
+256 -45
View File
@@ -22,7 +22,22 @@ const KLIPY_PROVIDER_NAME: &str = "klipy";
const KLIPY_FEATURED_CATEGORY_REFRESH_COUNTRY: &str = "US";
const SIZE_PREFERENCE: [&str; 4] = ["hd", "md", "sm", "xs"];
const FORMAT_PREFERENCE: [&str; 4] = ["webm", "mp4", "webp", "gif"];
const FILE_FORMAT_PREFERENCE: [&str; 4] = ["webm", "mp4", "webp", "gif"];
const MEDIA_FORMAT_PREFERENCE: [&str; 11] = [
"webm",
"mp4",
"webp",
"gif",
"mediumgif",
"tinywebm",
"tinymp4",
"tinygif",
"nanowebm",
"nanomp4",
"nanogif",
];
const MEDIA_FILTER: &str =
"webm,mp4,webp,gif,mediumgif,tinywebm,tinymp4,tinygif,nanowebm,nanomp4,nanogif";
#[derive(Clone)]
pub struct KlipyClient {
@@ -50,31 +65,25 @@ struct KlipyGif {
#[serde(default)]
itemurl: Option<String>,
#[serde(default)]
file: Option<BTreeMap<String, BTreeMap<String, KlipyFileEntry>>>,
file: Option<BTreeMap<String, BTreeMap<String, KlipyMediaEntry>>>,
#[serde(default)]
media_formats: Option<KlipyMediaFormats>,
media_formats: Option<BTreeMap<String, KlipyMediaEntry>>,
}
#[derive(Debug, Deserialize)]
struct KlipyMediaFormats {
#[serde(default)]
webm: Option<KlipyFallbackMediaFormat>,
}
#[derive(Debug, Deserialize)]
struct KlipyFallbackMediaFormat {
url: String,
dims: [i32; 2],
}
#[derive(Debug, Deserialize)]
struct KlipyFileEntry {
#[serde(default)]
url: Option<String>,
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
#[serde(untagged)]
enum KlipyMediaEntry {
Url(String),
Object {
#[serde(default)]
url: Option<String>,
#[serde(default)]
width: Option<i32>,
#[serde(default)]
height: Option<i32>,
#[serde(default)]
dims: Option<[i32; 2]>,
},
}
#[derive(Debug, Deserialize)]
@@ -136,6 +145,7 @@ impl KlipyClient {
("country", country),
("locale", &locale),
("limit", &limit),
("media_filter", MEDIA_FILTER),
],
)
.await
@@ -155,6 +165,7 @@ impl KlipyClient {
("country", country),
("locale", &locale),
("limit", "1"),
("media_filter", MEDIA_FILTER),
],
)
.await
@@ -174,6 +185,7 @@ impl KlipyClient {
("country", country),
("locale", &locale),
("limit", "50"),
("media_filter", MEDIA_FILTER),
],
)
.await
@@ -486,7 +498,7 @@ impl KlipyClient {
let Some(bucket) = input.file.as_ref().and_then(|files| files.get(size)) else {
continue;
};
for format in FORMAT_PREFERENCE {
for format in FILE_FORMAT_PREFERENCE {
let Some(entry) = bucket.get(format) else {
continue;
};
@@ -500,34 +512,47 @@ impl KlipyClient {
}
}
}
if media.is_empty()
&& let Some(webm) = input
.media_formats
.as_ref()
.and_then(|formats| formats.webm.as_ref())
&& webm.dims[0] > 0
&& webm.dims[1] > 0
&& let Some(proxy_src) = self.media_proxy.external_proxy_url(&webm.url)
{
let fallback = GifMediaFormat {
src: webm.url.clone(),
proxy_src,
width: webm.dims[0],
height: webm.dims[1],
};
media.insert("webm".to_owned(), fallback.clone());
preferred = Some(fallback);
if let Some(formats) = input.media_formats.as_ref() {
for format in MEDIA_FORMAT_PREFERENCE {
let Some(entry) = formats.get(format) else {
continue;
};
let Some(media_format) = self.to_media_format(entry) else {
continue;
};
media
.entry(format.to_owned())
.or_insert_with(|| media_format.clone());
if preferred.is_none() {
preferred = Some(media_format);
}
}
for (format, entry) in formats {
if MEDIA_FORMAT_PREFERENCE.contains(&format.as_str())
|| !is_supported_media_format_key(format)
{
continue;
}
let Some(media_format) = self.to_media_format(entry) else {
continue;
};
media
.entry(format.clone())
.or_insert_with(|| media_format.clone());
if preferred.is_none() {
preferred = Some(media_format);
}
}
}
(media, preferred)
}
fn to_media_format(&self, entry: &KlipyFileEntry) -> Option<GifMediaFormat> {
let src = entry.url.as_ref()?;
let width = entry.width.filter(|width| *width > 0)?;
let height = entry.height.filter(|height| *height > 0)?;
fn to_media_format(&self, entry: &KlipyMediaEntry) -> Option<GifMediaFormat> {
let (src, width, height) = entry.media_parts()?;
let proxy_src = self.media_proxy.external_proxy_url(src)?;
Some(GifMediaFormat {
src: src.clone(),
src: src.to_owned(),
proxy_src,
width,
height,
@@ -535,6 +560,32 @@ impl KlipyClient {
}
}
impl KlipyMediaEntry {
fn media_parts(&self) -> Option<(&str, i32, i32)> {
match self {
KlipyMediaEntry::Url(url) => {
let _ = url;
None
}
KlipyMediaEntry::Object {
url,
width,
height,
dims,
} => {
let src = url.as_deref().filter(|url| !url.trim().is_empty())?;
let size_from_dims =
(*dims).and_then(|[width, height]| valid_dimensions(width, height));
let size_from_fields = (*width)
.zip(*height)
.and_then(|(width, height)| valid_dimensions(width, height));
let (width, height) = size_from_dims.or(size_from_fields)?;
Some((src, width, height))
}
}
}
}
pub fn normalize_locale(locale: &str) -> String {
// KLIPY v2 rejects numeric-region tags such as es-419 on categories and autocomplete.
if let Some((language, region)) = locale.split_once(['-', '_'])
@@ -628,6 +679,28 @@ fn public_format_key(size: &str, format: &str) -> String {
.to_owned()
}
fn valid_dimensions(width: i32, height: i32) -> Option<(i32, i32)> {
(width > 0 && height > 0).then_some((width, height))
}
fn is_supported_media_format_key(format: &str) -> bool {
matches!(
format,
"webm"
| "tinywebm"
| "nanowebm"
| "mp4"
| "loopedmp4"
| "tinymp4"
| "nanomp4"
| "webp"
| "gif"
| "mediumgif"
| "tinygif"
| "nanogif"
)
}
fn category_response(name: String, gif: Option<GifItem>) -> GifCategoryTag {
GifCategoryTag {
src: gif.as_ref().map(|gif| gif.src.clone()).unwrap_or_default(),
@@ -701,6 +774,144 @@ mod tests {
assert_eq!(public_format_key("xs", "webp"), "nanowebp");
}
#[test]
fn transforms_v2_media_formats_into_animated_variants() {
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": "1373787462859498",
"title": "Move Faster",
"itemurl": "https://klipy.com/gifs/move-faster",
"media_formats": {
"webp": {
"url": "https://static.klipy.com/media/move.webp",
"dims": [498, 327]
},
"gif": {
"url": "https://static.klipy.com/media/move.gif",
"dims": [498, 326]
},
"mediumgif": {
"url": "https://static.klipy.com/media/move-medium.gif",
"dims": [640, 420]
},
"tinygif": {
"url": "https://static.klipy.com/media/move-tiny.gif",
"dims": [220, 144]
},
"nanogif": {
"url": "https://static.klipy.com/media/move-nano.gif",
"dims": [137, 90]
},
"webm": {
"url": "https://static.klipy.com/media/move.webm",
"dims": [640, 420]
},
"tinywebm": {
"url": "https://static.klipy.com/media/move-tiny.webm",
"dims": [320, 210]
},
"mp4": {
"url": "https://static.klipy.com/media/move.mp4",
"dims": [640, 420]
},
"preview": {
"url": "https://static.klipy.com/media/move.jpg",
"dims": [220, 144]
}
}
}))
.expect("fixture");
let gif = client.transform_gif(input).expect("transformed gif");
assert_eq!(gif.id, "move-faster");
assert_eq!(gif.url, "https://klipy.com/gifs/move-faster");
assert_eq!(gif.src, "https://static.klipy.com/media/move.webm");
assert_eq!(gif.width, 640);
assert_eq!(gif.height, 420);
assert_eq!(
gif.media.get("webp").map(|format| format.src.as_str()),
Some("https://static.klipy.com/media/move.webp")
);
assert_eq!(
gif.media.get("gif").map(|format| format.src.as_str()),
Some("https://static.klipy.com/media/move.gif")
);
assert_eq!(
gif.media
.get("tinygif")
.map(|format| (format.width, format.height)),
Some((220, 144))
);
assert!(gif.media.contains_key("mediumgif"));
assert!(gif.media.contains_key("nanogif"));
assert!(gif.media.contains_key("tinywebm"));
assert!(!gif.media.contains_key("preview"));
assert!(
gif.media
.get("webp")
.expect("webp format")
.proxy_src
.starts_with("https://media.example.test/external/")
);
}
#[test]
fn media_formats_without_video_choose_webp_as_top_level() {
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": "cat-cone",
"title": "Cat Cone",
"media_formats": {
"webp": {
"url": "https://static.klipy.com/media/cat.webp",
"dims": [320, 180]
},
"gif": {
"url": "https://static.klipy.com/media/cat.gif",
"dims": [320, 180]
}
}
}))
.expect("fixture");
let gif = client.transform_gif(input).expect("transformed gif");
assert_eq!(gif.src, "https://static.klipy.com/media/cat.webp");
assert_eq!(gif.proxy_src, gif.media["webp"].proxy_src);
assert_eq!(gif.media["gif"].width, 320);
}
#[tokio::test]
#[ignore]
async fn live_search_exposes_animated_image_variants() {
let api_key = std::env::var("FLUXER_KLIPY_API_KEY")
.or_else(|_| std::env::var("KLIPY_API_KEY"))
.expect("FLUXER_KLIPY_API_KEY or KLIPY_API_KEY set");
let client =
KlipyClient::new(MediaProxyUrlBuilder::from_env().expect("media proxy env configured"))
.expect("KLIPY client");
let gifs = client
.search(&api_key, "move faster", "en_US", "US", 1)
.await
.expect("KLIPY search");
let gif = gifs.first().expect("at least one GIF");
assert!(gif.media.contains_key("webm"));
assert!(gif.media.contains_key("webp"));
assert!(gif.media.contains_key("gif"));
assert!(gif.media.contains_key("tinygif") || gif.media.contains_key("nanogif"));
}
#[tokio::test]
#[ignore]
async fn live_accepts_es_419_locale_for_picker_endpoints() {
+14
View File
@@ -15,6 +15,20 @@ pub struct MediaProxyUrlBuilder {
}
impl MediaProxyUrlBuilder {
#[cfg(test)]
pub(crate) fn for_test(endpoint: &str, secret_key: &str) -> Self {
let endpoint = endpoint.trim_end_matches('/').to_owned();
let endpoint_host = Url::parse(&endpoint)
.ok()
.and_then(|parsed| parsed.host_str().map(ToOwned::to_owned));
Self {
endpoint,
endpoint_host,
secret_key: secret_key.to_owned(),
}
}
pub fn from_env() -> anyhow::Result<Self> {
let endpoint = std::env::var("FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT")
.or_else(|_| std::env::var("FLUXER_MEDIA_ENDPOINT"))