diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts index 753262a4f6..38966c35cb 100644 --- a/__tests__/lib/string.test.ts +++ b/__tests__/lib/string.test.ts @@ -453,6 +453,7 @@ describe('parseEmbedPlayerFromUrl', () => { 'https://bandcamp.com', 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300', + 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200&ww=300&mp4=videoSlugMp4&webm=videoSlugWebm', 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif?hh=200', 'https://static.klipy.com/ii/abc123/73/ac/someFile.gif', 'https://static.klipy.com/other/path.gif?hh=200&ww=300', @@ -853,6 +854,19 @@ describe('parseEmbedPlayerFromUrl', () => { undefined, undefined, + { + type: 'klipy_gif', + source: 'klipy', + isGif: true, + hideDetails: true, + playerUri: 'https://k.gifs.bsky.app/ii/abc123/73/ac/someFile.gif', + dimensions: { + width: 300, + height: 200, + }, + }, + // With video slug params — on native (test env), keeps gif filename, + // strips mp4/webm params. On web, would swap to video filename. { type: 'klipy_gif', source: 'klipy', diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index 5ce0d70971..e6d21845fd 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -190,7 +190,26 @@ export async function resolveGif( agent: BskyAgent, gif: Gif, ): Promise { - const uri = `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}` + const gifUrl = gif.media_formats.gif.url + const params = new URLSearchParams() + params.set('hh', String(gif.media_formats.gif.dims[1])) + params.set('ww', String(gif.media_formats.gif.dims[0])) + + // For Klipy GIFs, embed video format slugs so parseKlipyGif can + // swap to the right format per platform at render time. Klipy uses + // different filename slugs per format (unlike Tenor where format is + // encoded in the URL ID), so this info must travel with the URL. + try { + const url = new URL(gifUrl) + if (url.hostname === 'static.klipy.com') { + const mp4Slug = getFileSlug(gif.media_formats.mp4?.url) + const webmSlug = getFileSlug(gif.media_formats.webm?.url) + if (mp4Slug) params.set('mp4', mp4Slug) + if (webmSlug) params.set('webm', webmSlug) + } + } catch {} + + const uri = `${gifUrl}?${params.toString()}` const altText = gif.content_description || gif.title return { type: 'external', @@ -201,6 +220,14 @@ export async function resolveGif( } } +function getFileSlug(url: string | undefined): string | undefined { + if (!url) return undefined + const filename = url.split('/').pop() + if (!filename) return undefined + const dotIndex = filename.lastIndexOf('.') + return dotIndex > 0 ? filename.slice(0, dotIndex) : undefined +} + async function resolveExternal( agent: BskyAgent, uri: string, diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts index d8a5999797..8459bf707f 100644 --- a/src/lib/strings/embed-player.ts +++ b/src/lib/strings/embed-player.ts @@ -683,14 +683,31 @@ export function parseKlipyGif(urlp: URL): return {success: false} } - // Use the base URL without dimension params as the player URI, - // routed through the bsky KLIPY proxy (k.gifs.bsky.app). Mirrors - // Tenor's t.gifs.bsky.app rewrite, but on a separate hostname so - // the two upstreams can be routed independently. const playerUrl = new URL(urlp.href) playerUrl.hostname = 'k.gifs.bsky.app' + + // On web, swap the gif filename for a video format so the