mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
feat(gifs): serve every medium, tiny and nano variant klipy offers (#1829)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import {Logger} from '@fluxer/logger/src/Logger';
|
||||
import type {ResolvedGifEntrySchema} from '@fluxer/schema/src/domains/gif/FavoriteGifSchemas';
|
||||
import {inferFormatContentType, PREVIEW_FORMAT_PRIORITY} from '@fluxer/schema/src/domains/gif/GifMediaFormatKeys';
|
||||
import type {GifMediaFormat, GifResponse} from '@fluxer/schema/src/domains/gif/GifSchemas';
|
||||
import type {EmbedMediaResponse} from '@fluxer/schema/src/domains/message/EmbedSchemas';
|
||||
import {tryExtractGifProviderSlug} from '../gif/GifProviderUtils';
|
||||
@@ -10,17 +11,6 @@ import type {IGifProvider} from '../gif/IGifProvider';
|
||||
import type {IMediaService, MediaProxyMetadataResponse} from '../infrastructure/IMediaService';
|
||||
import type {IUnfurlerService} from '../infrastructure/IUnfurlerService';
|
||||
|
||||
const PREVIEW_FORMAT_PRIORITY = ['webm', 'mp4', 'tinywebm', 'tinymp4', 'webp', 'gif', 'tinygif', 'nanogif'] as const;
|
||||
const FORMAT_CONTENT_TYPES: Record<string, string> = {
|
||||
webm: 'video/webm',
|
||||
tinywebm: 'video/webm',
|
||||
mp4: 'video/mp4',
|
||||
tinymp4: 'video/mp4',
|
||||
webp: 'image/webp',
|
||||
gif: 'image/gif',
|
||||
tinygif: 'image/gif',
|
||||
nanogif: 'image/gif',
|
||||
};
|
||||
const logger = new Logger('FavoriteGifResolver');
|
||||
|
||||
function pickFavoriteGifPreviewFormat(
|
||||
@@ -221,10 +211,6 @@ function isUsableGifMediaFormat(format: GifMediaFormat | undefined): format is G
|
||||
return Boolean(format?.src && format.proxy_src && format.width > 0 && format.height > 0);
|
||||
}
|
||||
|
||||
function inferFormatContentType(formatKey: string): string {
|
||||
return FORMAT_CONTENT_TYPES[formatKey] ?? '';
|
||||
}
|
||||
|
||||
function isRenderableMediaType(contentType: string | null | undefined): boolean {
|
||||
if (!contentType) return false;
|
||||
return contentType.startsWith('image/') || contentType.startsWith('video/');
|
||||
|
||||
+352
-106
@@ -7,6 +7,7 @@ use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::sync::LazyLock;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -20,10 +21,13 @@ const KLIPY_RESPONSE_LIMIT_BYTES: usize = 512 * 1024;
|
||||
const FLUXER_USER_AGENT: &str = "Fluxerbot/1.0 (+https://fluxer.app)";
|
||||
const KLIPY_PROVIDER_NAME: &str = "klipy";
|
||||
const KLIPY_FEATURED_CATEGORY_REFRESH_COUNTRY: &str = "US";
|
||||
const UNRESOLVABLE_CACHE_KEY: &str = "unresolvable";
|
||||
|
||||
const SIZE_PREFERENCE: [&str; 4] = ["hd", "md", "sm", "xs"];
|
||||
const FILE_FORMAT_PREFERENCE: [&str; 4] = ["webm", "mp4", "webp", "gif"];
|
||||
const MEDIA_FORMAT_PREFERENCE: [&str; 11] = [
|
||||
const SIZE_KEY_PREFIXES: [(&str, &str); 4] =
|
||||
[("hd", ""), ("md", "medium"), ("sm", "tiny"), ("xs", "nano")];
|
||||
const FILE_FORMATS: [&str; 4] = ["webm", "mp4", "webp", "gif"];
|
||||
const UNSIZED_MEDIA_FORMAT_KEYS: [&str; 1] = ["loopedmp4"];
|
||||
const MEDIA_FORMAT_PREFERENCE: [&str; 17] = [
|
||||
"webm",
|
||||
"mp4",
|
||||
"webp",
|
||||
@@ -35,9 +39,14 @@ const MEDIA_FORMAT_PREFERENCE: [&str; 11] = [
|
||||
"nanowebm",
|
||||
"nanomp4",
|
||||
"nanogif",
|
||||
"loopedmp4",
|
||||
"mediummp4",
|
||||
"mediumwebm",
|
||||
"mediumwebp",
|
||||
"nanowebp",
|
||||
"tinywebp",
|
||||
];
|
||||
const MEDIA_FILTER: &str =
|
||||
"webm,mp4,webp,gif,mediumgif,tinywebm,tinymp4,tinygif,nanowebm,nanomp4,nanogif";
|
||||
static MEDIA_FILTER: LazyLock<String> = LazyLock::new(|| MEDIA_FORMAT_PREFERENCE.join(","));
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KlipyClient {
|
||||
@@ -168,7 +177,7 @@ impl KlipyClient {
|
||||
("country", country),
|
||||
("locale", &locale),
|
||||
("limit", &limit),
|
||||
("media_filter", MEDIA_FILTER),
|
||||
("media_filter", MEDIA_FILTER.as_str()),
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -188,7 +197,7 @@ impl KlipyClient {
|
||||
("country", country),
|
||||
("locale", &locale),
|
||||
("limit", "1"),
|
||||
("media_filter", MEDIA_FILTER),
|
||||
("media_filter", MEDIA_FILTER.as_str()),
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -208,7 +217,7 @@ impl KlipyClient {
|
||||
("country", country),
|
||||
("locale", &locale),
|
||||
("limit", "50"),
|
||||
("media_filter", MEDIA_FILTER),
|
||||
("media_filter", MEDIA_FILTER.as_str()),
|
||||
],
|
||||
)
|
||||
.await
|
||||
@@ -253,7 +262,13 @@ impl KlipyClient {
|
||||
("q", q),
|
||||
],
|
||||
)?;
|
||||
let response = self.http_client.get(url).send().await?;
|
||||
let response = self
|
||||
.http_client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(reqwest::Error::without_url)
|
||||
.context("KLIPY registershare request failed")?;
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!(
|
||||
"KLIPY registershare failed with status {}",
|
||||
@@ -372,7 +387,10 @@ impl KlipyClient {
|
||||
url: Url,
|
||||
path: &KlipyPath,
|
||||
) -> anyhow::Result<Option<GifItem>> {
|
||||
match self.fetch_json_response::<DirectGifResponse>(url).await? {
|
||||
match self
|
||||
.fetch_json_response::<DirectGifResponse>(url, klipy_resource(path.path_type))
|
||||
.await?
|
||||
{
|
||||
KlipyJsonFetch::NotFound => Ok(None),
|
||||
KlipyJsonFetch::Found(response) => Ok(response
|
||||
.data
|
||||
@@ -388,7 +406,7 @@ impl KlipyClient {
|
||||
let url = self.create_url(endpoint, params)?;
|
||||
let mut last_error = None;
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
match self.fetch_json_once(url.clone()).await {
|
||||
match self.fetch_json_once(url.clone(), endpoint).await {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(error) if attempt + 1 < MAX_RETRIES => {
|
||||
last_error = Some(error);
|
||||
@@ -400,26 +418,41 @@ impl KlipyClient {
|
||||
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("exceeded KLIPY retry limit")))
|
||||
}
|
||||
|
||||
async fn fetch_json_once<T>(&self, url: Url) -> anyhow::Result<T>
|
||||
async fn fetch_json_once<T>(&self, url: Url, label: &str) -> anyhow::Result<T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
match self.fetch_json_response(url).await? {
|
||||
match self.fetch_json_response(url, label).await? {
|
||||
KlipyJsonFetch::Found(value) => Ok(value),
|
||||
KlipyJsonFetch::NotFound => anyhow::bail!("KLIPY request returned not found"),
|
||||
KlipyJsonFetch::NotFound => {
|
||||
anyhow::bail!("KLIPY {label} request returned not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_json_response<T>(&self, url: Url) -> anyhow::Result<KlipyJsonFetch<T>>
|
||||
async fn fetch_json_response<T>(
|
||||
&self,
|
||||
url: Url,
|
||||
label: &str,
|
||||
) -> anyhow::Result<KlipyJsonFetch<T>>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
let response = self.http_client.get(url.clone()).send().await?;
|
||||
let response = self
|
||||
.http_client
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(reqwest::Error::without_url)
|
||||
.with_context(|| format!("KLIPY {label} request failed"))?;
|
||||
if response.status() == reqwest::StatusCode::NOT_FOUND {
|
||||
return Ok(KlipyJsonFetch::NotFound);
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("KLIPY request failed with status {}", response.status());
|
||||
anyhow::bail!(
|
||||
"KLIPY {label} request failed with status {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
if response
|
||||
.content_length()
|
||||
@@ -427,12 +460,16 @@ impl KlipyClient {
|
||||
{
|
||||
anyhow::bail!("KLIPY response declared more than {KLIPY_RESPONSE_LIMIT_BYTES} bytes");
|
||||
}
|
||||
let bytes = response.bytes().await?;
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(reqwest::Error::without_url)
|
||||
.with_context(|| format!("KLIPY {label} response body failed"))?;
|
||||
if bytes.len() > KLIPY_RESPONSE_LIMIT_BYTES {
|
||||
anyhow::bail!("KLIPY response exceeded {KLIPY_RESPONSE_LIMIT_BYTES} bytes");
|
||||
anyhow::bail!("KLIPY {label} response exceeded {KLIPY_RESPONSE_LIMIT_BYTES} bytes");
|
||||
}
|
||||
serde_json::from_slice(&bytes)
|
||||
.with_context(|| format!("failed to parse KLIPY response from {url}"))
|
||||
.with_context(|| format!("failed to parse KLIPY {label} response"))
|
||||
.map(KlipyJsonFetch::Found)
|
||||
}
|
||||
|
||||
@@ -519,52 +556,39 @@ impl KlipyClient {
|
||||
) -> (BTreeMap<String, GifMediaFormat>, Option<GifMediaFormat>) {
|
||||
let mut media = BTreeMap::new();
|
||||
let mut preferred = None;
|
||||
for size in SIZE_PREFERENCE {
|
||||
let Some(KlipyFileGroup::Sized(bucket)) =
|
||||
input.file.as_ref().and_then(|files| files.get(size))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
for format in FILE_FORMAT_PREFERENCE {
|
||||
let Some(entry) = bucket.get(format) else {
|
||||
if let Some(files) = input.file.as_ref() {
|
||||
report_unmapped_file_keys(files);
|
||||
for (size, _) in SIZE_KEY_PREFIXES {
|
||||
let Some(KlipyFileGroup::Sized(bucket)) = files.get(size) else {
|
||||
continue;
|
||||
};
|
||||
let Some(media_format) = self.to_media_format(entry, None) else {
|
||||
continue;
|
||||
};
|
||||
let public_key = public_format_key(size, format);
|
||||
media.insert(public_key, media_format.clone());
|
||||
if preferred.is_none() {
|
||||
preferred = Some(media_format);
|
||||
for format in FILE_FORMATS {
|
||||
let Some(entry) = bucket.get(format) else {
|
||||
continue;
|
||||
};
|
||||
let Some(media_format) = self.to_media_format(entry, None) else {
|
||||
continue;
|
||||
};
|
||||
let Some(public_key) = public_format_key(size, format) else {
|
||||
continue;
|
||||
};
|
||||
media
|
||||
.entry(public_key)
|
||||
.or_insert_with(|| media_format.clone());
|
||||
if preferred.is_none() {
|
||||
preferred = Some(media_format);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
for format in MEDIA_FORMAT_PREFERENCE {
|
||||
let Some(entry) = formats.get(format) else {
|
||||
for format in FILE_FORMATS {
|
||||
let Some(KlipyFileGroup::Flat(entry)) = files.get(format) else {
|
||||
continue;
|
||||
};
|
||||
let Some(media_format) = self.to_media_format(entry, None) else {
|
||||
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
|
||||
@@ -574,18 +598,18 @@ impl KlipyClient {
|
||||
preferred = Some(media_format);
|
||||
}
|
||||
}
|
||||
|
||||
for (format, entry) in formats {
|
||||
if MEDIA_FORMAT_PREFERENCE.contains(&format.as_str())
|
||||
|| !is_supported_media_format_key(format)
|
||||
{
|
||||
}
|
||||
if let Some(formats) = input.media_formats.as_ref() {
|
||||
report_unmapped_media_format_keys(formats);
|
||||
for format in MEDIA_FORMAT_PREFERENCE {
|
||||
let Some(entry) = formats.get(format) else {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let Some(media_format) = self.to_media_format(entry, None) else {
|
||||
continue;
|
||||
};
|
||||
media
|
||||
.entry(format.clone())
|
||||
.entry(format.to_owned())
|
||||
.or_insert_with(|| media_format.clone());
|
||||
if preferred.is_none() {
|
||||
preferred = Some(media_format);
|
||||
@@ -665,6 +689,13 @@ pub fn extract_slug_from_url(url: &str) -> Option<String> {
|
||||
parse_klipy_path(url).map(|path| path.slug)
|
||||
}
|
||||
|
||||
pub fn resolve_cache_key(url: &str) -> String {
|
||||
match parse_klipy_path(url) {
|
||||
Some(path) => format!("{}:{}", klipy_resource(path.path_type), path.slug),
|
||||
None => UNRESOLVABLE_CACHE_KEY.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn klipy_id_as_string(value: &Value) -> Option<String> {
|
||||
match value {
|
||||
Value::String(value) => {
|
||||
@@ -712,27 +743,49 @@ fn klipy_resource(path_type: KlipyPathType) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn public_format_key(size: &str, format: &str) -> String {
|
||||
match (size, format) {
|
||||
("hd", "webm") => "webm",
|
||||
("hd", "mp4") => "mp4",
|
||||
("hd", "webp") => "webp",
|
||||
("hd", "gif") => "gif",
|
||||
("md", "webm") => "mediumwebm",
|
||||
("md", "mp4") => "mediummp4",
|
||||
("md", "webp") => "mediumwebp",
|
||||
("md", "gif") => "mediumgif",
|
||||
("sm", "webm") => "tinywebm",
|
||||
("sm", "mp4") => "tinymp4",
|
||||
("sm", "webp") => "tinywebp",
|
||||
("sm", "gif") => "tinygif",
|
||||
("xs", "webm") => "nanowebm",
|
||||
("xs", "mp4") => "nanomp4",
|
||||
("xs", "webp") => "nanowebp",
|
||||
("xs", "gif") => "nanogif",
|
||||
_ => format,
|
||||
fn size_key_prefix(size: &str) -> Option<&'static str> {
|
||||
SIZE_KEY_PREFIXES
|
||||
.iter()
|
||||
.find(|(known, _)| *known == size)
|
||||
.map(|(_, prefix)| *prefix)
|
||||
}
|
||||
|
||||
fn public_format_key(size: &str, format: &str) -> Option<String> {
|
||||
if !FILE_FORMATS.contains(&format) {
|
||||
return None;
|
||||
}
|
||||
Some(format!("{}{format}", size_key_prefix(size)?))
|
||||
}
|
||||
|
||||
fn report_unmapped_file_keys(files: &BTreeMap<String, KlipyFileGroup>) {
|
||||
for (key, group) in files {
|
||||
match group {
|
||||
KlipyFileGroup::Sized(bucket) => {
|
||||
if size_key_prefix(key).is_none() {
|
||||
tracing::warn!(size = %key, "skipping gif file bucket with an unmapped size");
|
||||
continue;
|
||||
}
|
||||
for format in bucket.keys() {
|
||||
if !FILE_FORMATS.contains(&format.as_str()) {
|
||||
tracing::warn!(size = %key, format = %format, "skipping gif file entry with an unmapped format");
|
||||
}
|
||||
}
|
||||
}
|
||||
KlipyFileGroup::Flat(_) => {
|
||||
if !FILE_FORMATS.contains(&key.as_str()) {
|
||||
tracing::warn!(format = %key, "skipping flat gif file entry with an unmapped format");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn report_unmapped_media_format_keys(formats: &BTreeMap<String, KlipyMediaEntry>) {
|
||||
for key in formats.keys() {
|
||||
if !is_supported_media_format_key(key) {
|
||||
tracing::debug!(format = %key, "skipping unsupported gif media format");
|
||||
}
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn valid_dimensions(width: i32, height: i32) -> Option<(i32, i32)> {
|
||||
@@ -740,21 +793,12 @@ fn valid_dimensions(width: i32, height: i32) -> Option<(i32, i32)> {
|
||||
}
|
||||
|
||||
fn is_supported_media_format_key(format: &str) -> bool {
|
||||
matches!(
|
||||
format,
|
||||
"webm"
|
||||
| "tinywebm"
|
||||
| "nanowebm"
|
||||
| "mp4"
|
||||
| "loopedmp4"
|
||||
| "tinymp4"
|
||||
| "nanomp4"
|
||||
| "webp"
|
||||
| "gif"
|
||||
| "mediumgif"
|
||||
| "tinygif"
|
||||
| "nanogif"
|
||||
)
|
||||
UNSIZED_MEDIA_FORMAT_KEYS.contains(&format)
|
||||
|| SIZE_KEY_PREFIXES.iter().any(|(_, prefix)| {
|
||||
format
|
||||
.strip_prefix(prefix)
|
||||
.is_some_and(|codec| FILE_FORMATS.contains(&codec))
|
||||
})
|
||||
}
|
||||
|
||||
fn category_response(name: String, gif: Option<GifItem>) -> GifCategoryTag {
|
||||
@@ -992,10 +1036,212 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_provider_format_keys() {
|
||||
assert_eq!(public_format_key("hd", "webm"), "webm");
|
||||
assert_eq!(public_format_key("sm", "gif"), "tinygif");
|
||||
assert_eq!(public_format_key("xs", "webp"), "nanowebp");
|
||||
fn resolve_cache_key_ignores_everything_but_the_klipy_path() {
|
||||
assert_eq!(
|
||||
resolve_cache_key("https://klipy.com/gifs/funny-123"),
|
||||
"gifs:funny-123"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_cache_key("https://www.klipy.com/gif/funny-123?utm_source=x"),
|
||||
"gifs:funny-123"
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_cache_key("https://klipy.com/clips/kittens"),
|
||||
"clips:kittens"
|
||||
);
|
||||
assert_ne!(
|
||||
resolve_cache_key("https://klipy.com/gifs/kittens"),
|
||||
resolve_cache_key("https://klipy.com/clips/kittens")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_cache_key("https://notklipy.com/gifs/funny"),
|
||||
"unresolvable"
|
||||
);
|
||||
}
|
||||
|
||||
const FROZEN_PUBLIC_FORMAT_KEYS: [(&str, &str, &str); 16] = [
|
||||
("hd", "webm", "webm"),
|
||||
("hd", "mp4", "mp4"),
|
||||
("hd", "webp", "webp"),
|
||||
("hd", "gif", "gif"),
|
||||
("md", "webm", "mediumwebm"),
|
||||
("md", "mp4", "mediummp4"),
|
||||
("md", "webp", "mediumwebp"),
|
||||
("md", "gif", "mediumgif"),
|
||||
("sm", "webm", "tinywebm"),
|
||||
("sm", "mp4", "tinymp4"),
|
||||
("sm", "webp", "tinywebp"),
|
||||
("sm", "gif", "tinygif"),
|
||||
("xs", "webm", "nanowebm"),
|
||||
("xs", "mp4", "nanomp4"),
|
||||
("xs", "webp", "nanowebp"),
|
||||
("xs", "gif", "nanogif"),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn every_size_and_format_pair_maps_to_its_frozen_key() {
|
||||
assert_eq!(
|
||||
FROZEN_PUBLIC_FORMAT_KEYS.len(),
|
||||
SIZE_KEY_PREFIXES.len() * FILE_FORMATS.len()
|
||||
);
|
||||
for (size, format, expected) in FROZEN_PUBLIC_FORMAT_KEYS {
|
||||
assert_eq!(public_format_key(size, format).as_deref(), Some(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmapped_sizes_and_formats_produce_no_key() {
|
||||
assert_eq!(public_format_key("xxl", "webm"), None);
|
||||
assert_eq!(public_format_key("orig", "gif"), None);
|
||||
assert_eq!(public_format_key("hd", "avif"), None);
|
||||
assert_eq!(public_format_key("", "webm"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_accepted_vocabulary_is_frozen() {
|
||||
let mut expected: Vec<&str> = FROZEN_PUBLIC_FORMAT_KEYS
|
||||
.iter()
|
||||
.map(|(_, _, key)| *key)
|
||||
.collect();
|
||||
expected.extend(UNSIZED_MEDIA_FORMAT_KEYS);
|
||||
expected.sort_unstable();
|
||||
let mut ordered = MEDIA_FORMAT_PREFERENCE.to_vec();
|
||||
ordered.sort_unstable();
|
||||
assert_eq!(
|
||||
ordered, expected,
|
||||
"preference order must cover exactly the accepted keys"
|
||||
);
|
||||
for key in expected {
|
||||
assert!(is_supported_media_format_key(key), "dropped {key}");
|
||||
}
|
||||
for key in [
|
||||
"preview", "avif", "medium", "hdwebm", "webmm", "tinyavif", "",
|
||||
] {
|
||||
assert!(!is_supported_media_format_key(key), "admitted {key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_filter_requests_every_accepted_key() {
|
||||
assert_eq!(
|
||||
MEDIA_FILTER.as_str(),
|
||||
"webm,mp4,webp,gif,mediumgif,tinywebm,tinymp4,tinygif,nanowebm,nanomp4,nanogif,loopedmp4,mediummp4,mediumwebm,mediumwebp,nanowebp,tinywebp"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sized_payloads_emit_exactly_the_frozen_key_set() {
|
||||
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": "frozen-keys",
|
||||
"title": "Frozen Keys",
|
||||
"itemurl": "https://klipy.com/gifs/frozen-keys",
|
||||
"file": {
|
||||
"hd": {
|
||||
"webm": {"url": "https://static.klipy.com/hd.webm", "width": 640, "height": 420},
|
||||
"mp4": {"url": "https://static.klipy.com/hd.mp4", "width": 640, "height": 420},
|
||||
"webp": {"url": "https://static.klipy.com/hd.webp", "width": 640, "height": 420},
|
||||
"gif": {"url": "https://static.klipy.com/hd.gif", "width": 640, "height": 420},
|
||||
"avif": {"url": "https://static.klipy.com/hd.avif", "width": 640, "height": 420}
|
||||
},
|
||||
"md": {
|
||||
"webm": {"url": "https://static.klipy.com/md.webm", "width": 498, "height": 327},
|
||||
"mp4": {"url": "https://static.klipy.com/md.mp4", "width": 498, "height": 327},
|
||||
"webp": {"url": "https://static.klipy.com/md.webp", "width": 498, "height": 327},
|
||||
"gif": {"url": "https://static.klipy.com/md.gif", "width": 498, "height": 327}
|
||||
},
|
||||
"sm": {
|
||||
"webm": {"url": "https://static.klipy.com/sm.webm", "width": 220, "height": 144},
|
||||
"mp4": {"url": "https://static.klipy.com/sm.mp4", "width": 220, "height": 144},
|
||||
"webp": {"url": "https://static.klipy.com/sm.webp", "width": 220, "height": 144},
|
||||
"gif": {"url": "https://static.klipy.com/sm.gif", "width": 220, "height": 144}
|
||||
},
|
||||
"xs": {
|
||||
"webm": {"url": "https://static.klipy.com/xs.webm", "width": 137, "height": 90},
|
||||
"mp4": {"url": "https://static.klipy.com/xs.mp4", "width": 137, "height": 90},
|
||||
"webp": {"url": "https://static.klipy.com/xs.webp", "width": 137, "height": 90},
|
||||
"gif": {"url": "https://static.klipy.com/xs.gif", "width": 137, "height": 90}
|
||||
},
|
||||
"xxl": {
|
||||
"webm": {"url": "https://static.klipy.com/xxl.webm", "width": 1280, "height": 840}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.expect("fixture");
|
||||
|
||||
let gif = client.transform_gif(input).expect("transformed gif");
|
||||
|
||||
let keys: Vec<&str> = gif.media.keys().map(String::as_str).collect();
|
||||
assert_eq!(
|
||||
keys,
|
||||
[
|
||||
"gif",
|
||||
"mediumgif",
|
||||
"mediummp4",
|
||||
"mediumwebm",
|
||||
"mediumwebp",
|
||||
"mp4",
|
||||
"nanogif",
|
||||
"nanomp4",
|
||||
"nanowebm",
|
||||
"nanowebp",
|
||||
"tinygif",
|
||||
"tinymp4",
|
||||
"tinywebm",
|
||||
"tinywebp",
|
||||
"webm",
|
||||
"webp"
|
||||
]
|
||||
);
|
||||
assert_eq!(gif.media["webm"].src, "https://static.klipy.com/hd.webm");
|
||||
assert_eq!(gif.src, gif.media["webm"].src);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_medium_variants_from_the_v2_media_formats_payload() {
|
||||
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": "medium-variants",
|
||||
"title": "Medium Variants",
|
||||
"itemurl": "https://klipy.com/gifs/medium-variants",
|
||||
"media_formats": {
|
||||
"webp": {"url": "https://static.klipy.com/m.webp", "dims": [498, 327]},
|
||||
"mediumwebm": {"url": "https://static.klipy.com/m-medium.webm", "dims": [640, 420]},
|
||||
"mediummp4": {"url": "https://static.klipy.com/m-medium.mp4", "dims": [640, 420]},
|
||||
"mediumwebp": {"url": "https://static.klipy.com/m-medium.webp", "dims": [640, 420]},
|
||||
"tinywebp": {"url": "https://static.klipy.com/m-tiny.webp", "dims": [220, 144]},
|
||||
"nanowebp": {"url": "https://static.klipy.com/m-nano.webp", "dims": [137, 90]}
|
||||
}
|
||||
}))
|
||||
.expect("fixture");
|
||||
|
||||
let gif = client.transform_gif(input).expect("transformed gif");
|
||||
|
||||
for format in [
|
||||
"mediumwebm",
|
||||
"mediummp4",
|
||||
"mediumwebp",
|
||||
"tinywebp",
|
||||
"nanowebp",
|
||||
] {
|
||||
assert!(gif.media.contains_key(format), "missing {format}");
|
||||
}
|
||||
assert_eq!(
|
||||
gif.media.get("mediumwebm").map(|format| (
|
||||
format.src.as_str(),
|
||||
format.width,
|
||||
format.height
|
||||
)),
|
||||
Some(("https://static.klipy.com/m-medium.webm", 640, 420))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -80,12 +80,9 @@ fn request_key(req: &GifRequest) -> Option<String> {
|
||||
} => Some(format!("trending:{locale}:{country}")),
|
||||
GifRequest::Suggest { q, locale, .. } => Some(format!("suggest:{locale}:{q}")),
|
||||
GifRequest::RegisterShare { .. } => None,
|
||||
GifRequest::ResolveByUrl {
|
||||
url,
|
||||
locale,
|
||||
country,
|
||||
..
|
||||
} => Some(format!("resolve:{locale}:{country}:{url}")),
|
||||
GifRequest::ResolveByUrl { url, .. } => {
|
||||
Some(format!("resolve:{}", crate::klipy::resolve_cache_key(url)))
|
||||
}
|
||||
GifRequest::BuildShareUrl { slug } => Some(format!("share-url:{slug}")),
|
||||
GifRequest::ExtractSlugFromUrl { url } => Some(format!("extract-slug:{url}")),
|
||||
}
|
||||
@@ -167,6 +164,42 @@ mod tests {
|
||||
assert_eq!(request_l1_key(&base), Some("trending:en_US:US".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_key_collapses_locale_country_and_url_spelling() {
|
||||
let make = |url: &str, locale: &str, country: &str| GifRequest::ResolveByUrl {
|
||||
api_key: "key".to_owned(),
|
||||
url: url.to_owned(),
|
||||
locale: locale.to_owned(),
|
||||
country: country.to_owned(),
|
||||
};
|
||||
let base = make("https://klipy.com/gifs/kittens", "en_US", "US");
|
||||
|
||||
assert_eq!(
|
||||
GifsRouter::coalesce_key(&base),
|
||||
Some("resolve:gifs:kittens".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
GifsRouter::coalesce_key(&base),
|
||||
GifsRouter::coalesce_key(&make("https://klipy.com/gifs/kittens", "sv_SE", "SE"))
|
||||
);
|
||||
assert_eq!(
|
||||
GifsRouter::coalesce_key(&base),
|
||||
GifsRouter::coalesce_key(&make(
|
||||
"https://www.klipy.com/gif/kittens?x=1",
|
||||
"en_US",
|
||||
"US"
|
||||
))
|
||||
);
|
||||
assert_ne!(
|
||||
GifsRouter::coalesce_key(&base),
|
||||
GifsRouter::coalesce_key(&make("https://klipy.com/clips/kittens", "en_US", "US"))
|
||||
);
|
||||
assert_eq!(
|
||||
request_l1_key(&base),
|
||||
Some("resolve:gifs:kittens".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_share_is_not_cached_or_coalesced() {
|
||||
let request = GifRequest::RegisterShare {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::klipy::{KlipyClient, build_share_url, extract_slug_from_url};
|
||||
use crate::klipy::{KlipyClient, build_share_url, extract_slug_from_url, resolve_cache_key};
|
||||
use crate::media_proxy::MediaProxyUrlBuilder;
|
||||
use crate::types::{GifCategoryTag, GifItem, GifRequest, GifServiceResponse};
|
||||
use fluxer_svc::config::ServiceConfig;
|
||||
@@ -310,7 +310,7 @@ impl GifsShard {
|
||||
locale: String,
|
||||
country: String,
|
||||
) -> anyhow::Result<GifServiceResponse> {
|
||||
let key = format!("resolve:{locale}:{country}:{url}");
|
||||
let key = format!("resolve:{}", resolve_cache_key(&url));
|
||||
let this = self.clone();
|
||||
let gif = self
|
||||
.get_cached(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export const FORMAT_CONTENT_TYPES: Record<string, string> = {
|
||||
gif: 'image/gif',
|
||||
loopedmp4: 'video/mp4',
|
||||
mediumgif: 'image/gif',
|
||||
mediummp4: 'video/mp4',
|
||||
mediumwebm: 'video/webm',
|
||||
mediumwebp: 'image/webp',
|
||||
mp4: 'video/mp4',
|
||||
nanogif: 'image/gif',
|
||||
nanomp4: 'video/mp4',
|
||||
nanowebm: 'video/webm',
|
||||
nanowebp: 'image/webp',
|
||||
tinygif: 'image/gif',
|
||||
tinymp4: 'video/mp4',
|
||||
tinywebm: 'video/webm',
|
||||
tinywebp: 'image/webp',
|
||||
webm: 'video/webm',
|
||||
webp: 'image/webp',
|
||||
};
|
||||
|
||||
export const PREVIEW_FORMAT_PRIORITY = [
|
||||
'tinywebm',
|
||||
'tinymp4',
|
||||
'mediumwebm',
|
||||
'mediummp4',
|
||||
'webm',
|
||||
'mp4',
|
||||
'loopedmp4',
|
||||
'nanowebm',
|
||||
'nanomp4',
|
||||
'tinywebp',
|
||||
'tinygif',
|
||||
'mediumwebp',
|
||||
'webp',
|
||||
'mediumgif',
|
||||
'gif',
|
||||
'nanowebp',
|
||||
'nanogif',
|
||||
] as const;
|
||||
|
||||
export function inferFormatContentType(formatKey: string): string {
|
||||
return FORMAT_CONTENT_TYPES[formatKey] ?? '';
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export const GifResponse = z.object({
|
||||
media: z
|
||||
.record(z.string(), GifMediaFormat)
|
||||
.describe(
|
||||
'Map of format-name → media descriptor. Keys are provider-issued format names (e.g. "webm", "mp4", "webp", "gif", "tinygif", "nanogif"). Clients without webm support should pick "webp" / "gif" / "tinygif" / "nanogif" in that order.',
|
||||
'Map of format-name → media descriptor. Keys are a size prefix (none for full size, "medium", "tiny", "nano") joined to a codec name ("webm", "mp4", "webp", "gif"), plus "loopedmp4". Video keys are "webm" / "mp4" / "loopedmp4" / "mediumwebm" / "mediummp4" / "tinywebm" / "tinymp4" / "nanowebm" / "nanomp4"; image keys are "webp" / "gif" / "mediumwebp" / "mediumgif" / "tinywebp" / "tinygif" / "nanowebp" / "nanogif". Every key is optional, so clients must walk a priority list rather than index a single key. Clients that cannot decode the video keys should prefer "tinywebp" / "tinygif" / "mediumwebp" / "mediumgif" / "webp" / "gif" / "nanowebp" / "nanogif" in that order.',
|
||||
),
|
||||
placeholder: z
|
||||
.string()
|
||||
|
||||
Reference in New Issue
Block a user