[APP-2066] Migrate from Tenor to KLIPY (#10240)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Spence Pope
2026-04-17 08:43:13 -04:00
committed by GitHub
parent a97b15b204
commit a77b6e3525
12 changed files with 411 additions and 50 deletions
+31 -3
View File
@@ -190,16 +190,44 @@ export async function resolveGif(
agent: BskyAgent,
gif: Gif,
): Promise<ResolvedExternalLink> {
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',
uri,
title: gif.content_description,
description: createGIFDescription(gif.content_description),
title: altText,
description: createGIFDescription(altText),
thumb: await imageToThumb(gif.media_formats.preview.url),
}
}
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,
+5
View File
@@ -178,6 +178,11 @@ export const GIF_SEARCH = (params: string) =>
export const GIF_FEATURED = (params: string) =>
`${GIF_SERVICE}/tenor/v2/featured?${params}`
export const GIF_KLIPY_SEARCH = (params: string) =>
`${GIF_SERVICE}/klipy/v2/search?${params}`
export const GIF_KLIPY_FEATURED = (params: string) =>
`${GIF_SERVICE}/klipy/v2/featured?${params}`
export const MAX_LABELERS = 20
export const VIDEO_SERVICE = 'https://video.bsky.app'
+104
View File
@@ -23,6 +23,7 @@ export const embedPlayerSources = [
'vimeo',
'giphy',
'tenor',
'klipy',
'flickr',
'bandcamp',
] as const
@@ -44,6 +45,7 @@ export type EmbedPlayerType =
| 'vimeo_video'
| 'giphy_gif'
| 'tenor_gif'
| 'klipy_gif'
| 'flickr_album'
| 'bandcamp_album'
| 'bandcamp_track'
@@ -55,6 +57,7 @@ export const externalEmbedLabels: Record<EmbedPlayerSource, string> = {
twitch: 'Twitch',
giphy: 'GIPHY',
tenor: 'Tenor',
klipy: 'KLIPY',
spotify: 'Spotify',
appleMusic: 'Apple Music',
soundcloud: 'SoundCloud',
@@ -391,6 +394,20 @@ export function parseEmbedPlayerFromUrl(
}
}
const klipyGif = parseKlipyGif(urlp)
if (klipyGif.success) {
const {playerUri, dimensions} = klipyGif
return {
type: 'klipy_gif',
source: 'klipy',
isGif: true,
hideDetails: true,
playerUri,
dimensions,
}
}
// this is a standard flickr path! we can use the embedder for albums and groups, so validate the path
if (urlp.hostname === 'www.flickr.com' || urlp.hostname === 'flickr.com') {
let i = urlp.pathname.length - 1
@@ -628,3 +645,90 @@ export function isTenorGifUri(url: URL | string) {
return false
}
}
export function parseKlipyGif(urlp: URL):
| {success: false}
| {
success: true
playerUri: string
dimensions: {height: number; width: number}
} {
if (urlp.hostname !== 'static.klipy.com') {
return {success: false}
}
if (!urlp.pathname.startsWith('/ii/')) {
return {success: false}
}
const h = urlp.searchParams.get('hh')
const w = urlp.searchParams.get('ww')
if (!h || !w) {
return {success: false}
}
const dimensions = {
height: Number(h),
width: Number(w),
}
// Validate dimensions are valid positive numbers
if (
isNaN(dimensions.height) ||
isNaN(dimensions.width) ||
dimensions.height <= 0 ||
dimensions.width <= 0
) {
return {success: false}
}
const playerUrl = new URL(urlp.href)
playerUrl.hostname = 'k.gifs.bsky.app'
// On web, swap the gif filename for a video format so the <video>
// element can play it. Klipy uses different filename slugs per
// format (unlike Tenor's ID-based scheme), so the slugs are
// embedded as query params at composition time by resolveGif().
if (IS_WEB) {
const webmSlug = playerUrl.searchParams.get('webm')
const mp4Slug = playerUrl.searchParams.get('mp4')
const slug = IS_WEB_SAFARI ? mp4Slug : webmSlug
const ext = IS_WEB_SAFARI ? 'mp4' : 'webm'
// Without a slug we can't produce a playable video URL on web,
// so fall back to the link card instead of returning a broken player.
if (!slug) {
return {success: false}
}
const parts = playerUrl.pathname.split('/')
parts[parts.length - 1] = `${slug}.${ext}`
playerUrl.pathname = parts.join('/')
}
// Strip all metadata params — only the path matters for the CDN
playerUrl.searchParams.delete('hh')
playerUrl.searchParams.delete('ww')
playerUrl.searchParams.delete('mp4')
playerUrl.searchParams.delete('webm')
return {
success: true,
playerUri: playerUrl.href,
dimensions,
}
}
export function isKlipyGifUri(url: URL | string) {
try {
return parseKlipyGif(typeof url === 'string' ? new URL(url) : url).success
} catch {
// Invalid URL
return false
}
}
export function isGifEmbed(url: URL | string) {
return isTenorGifUri(url) || isKlipyGifUri(url)
}