composition time formatting
This commit is contained in:
@@ -453,6 +453,7 @@ describe('parseEmbedPlayerFromUrl', () => {
|
|||||||
'https://bandcamp.com',
|
'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',
|
||||||
|
'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?hh=200',
|
||||||
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif',
|
'https://static.klipy.com/ii/abc123/73/ac/someFile.gif',
|
||||||
'https://static.klipy.com/other/path.gif?hh=200&ww=300',
|
'https://static.klipy.com/other/path.gif?hh=200&ww=300',
|
||||||
@@ -853,6 +854,19 @@ describe('parseEmbedPlayerFromUrl', () => {
|
|||||||
undefined,
|
undefined,
|
||||||
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',
|
type: 'klipy_gif',
|
||||||
source: 'klipy',
|
source: 'klipy',
|
||||||
|
|||||||
+28
-1
@@ -190,7 +190,26 @@ export async function resolveGif(
|
|||||||
agent: BskyAgent,
|
agent: BskyAgent,
|
||||||
gif: Gif,
|
gif: Gif,
|
||||||
): Promise<ResolvedExternalLink> {
|
): 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
|
const altText = gif.content_description || gif.title
|
||||||
return {
|
return {
|
||||||
type: 'external',
|
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(
|
async function resolveExternal(
|
||||||
agent: BskyAgent,
|
agent: BskyAgent,
|
||||||
uri: string,
|
uri: string,
|
||||||
|
|||||||
@@ -683,14 +683,31 @@ export function parseKlipyGif(urlp: URL):
|
|||||||
return {success: false}
|
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)
|
const playerUrl = new URL(urlp.href)
|
||||||
playerUrl.hostname = 'k.gifs.bsky.app'
|
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'
|
||||||
|
|
||||||
|
if (slug) {
|
||||||
|
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('hh')
|
||||||
playerUrl.searchParams.delete('ww')
|
playerUrl.searchParams.delete('ww')
|
||||||
|
playerUrl.searchParams.delete('mp4')
|
||||||
|
playerUrl.searchParams.delete('webm')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -143,7 +143,8 @@ export type Gif = {
|
|||||||
/**
|
/**
|
||||||
* A dictionary with a content format as the key and a Media Object as the value.
|
* A dictionary with a content format as the key and a Media Object as the value.
|
||||||
*/
|
*/
|
||||||
media_formats: Record<ContentFormats, MediaObject>
|
media_formats: Record<BaseContentFormats, MediaObject> &
|
||||||
|
Partial<Record<VideoContentFormats, MediaObject>>
|
||||||
/**
|
/**
|
||||||
* An array of tags for the post
|
* An array of tags for the post
|
||||||
*/
|
*/
|
||||||
@@ -198,16 +199,20 @@ type MediaObject = {
|
|||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentFormats =
|
type BaseContentFormats =
|
||||||
| 'preview'
|
| 'preview'
|
||||||
| 'gif'
|
| 'gif'
|
||||||
// | 'mediumgif'
|
// | 'mediumgif'
|
||||||
| 'tinygif'
|
| 'tinygif'
|
||||||
// | 'nanogif'
|
// | 'nanogif'
|
||||||
// | 'mp4'
|
|
||||||
|
type VideoContentFormats =
|
||||||
|
| 'mp4'
|
||||||
// | 'loopedmp4'
|
// | 'loopedmp4'
|
||||||
// | 'tinymp4'
|
// | 'tinymp4'
|
||||||
// | 'nanomp4'
|
// | 'nanomp4'
|
||||||
// | 'webm'
|
| 'webm'
|
||||||
// | 'tinywebm'
|
// | 'tinywebm'
|
||||||
// | 'nanowebm'
|
// | 'nanowebm'
|
||||||
|
|
||||||
|
type ContentFormats = BaseContentFormats | VideoContentFormats
|
||||||
|
|||||||
@@ -397,6 +397,8 @@ function parseGifFromUrl(
|
|||||||
url.searchParams.delete('ww')
|
url.searchParams.delete('ww')
|
||||||
url.searchParams.delete('hh')
|
url.searchParams.delete('hh')
|
||||||
url.searchParams.delete('alt')
|
url.searchParams.delete('alt')
|
||||||
|
url.searchParams.delete('mp4')
|
||||||
|
url.searchParams.delete('webm')
|
||||||
|
|
||||||
return {url: url.toString(), width, height, alt}
|
return {url: url.toString(), width, height, alt}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user