fix(embeds): resolve TikTok short links via oEmbed + og:url fallback
TikTok 'copy-link' share URLs (vm.tiktok.com, tiktok.com/t/…) carry no video id and the homeserver's link preview is bot-walled (generic 'TikTok - Make Your Day', no og:url/og:image), so they fell through to the static fallback card with no play button. New TikTokEmbedCard resolves the id client-side via TikTok's CORS-enabled oEmbed API on click (keeps the facade privacy model), then plays the player/v1 embed (portrait, autoplay + full controls + our fullscreen button). Canonical /video/<id> links skip the lookup. Also added a general og:url fallback so other short/redirect links resolve to their canonical form when the raw URL doesn't. Web CSP connect-src gains www.tiktok.com for the oEmbed fetch (desktop already allows https:). Tests 19/19. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -71,7 +71,7 @@ export function getVimeoVideoId(url: string): string | null {
|
||||
|
||||
// --- TikTok ---------------------------------------------------------------
|
||||
|
||||
/** Only resolvable for canonical /video/<id> links (short vm.tiktok.com links redirect). */
|
||||
/** Canonical /video/<id> links only; short links resolve via oEmbed (below). */
|
||||
export function getTikTokVideoId(url: string): string | null {
|
||||
try {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
@@ -84,6 +84,42 @@ export function getTikTokVideoId(url: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** Any embeddable TikTok video link — canonical, short (/t/, /v/), or vm/vt.tiktok.com. */
|
||||
export function isTikTokLink(url: string): boolean {
|
||||
try {
|
||||
const { hostname, pathname } = new URL(url);
|
||||
const h = hostname.replace(/^www\./, '');
|
||||
if (h === 'vm.tiktok.com' || h === 'vt.tiktok.com') return true;
|
||||
if (h === 'tiktok.com') return /^\/(t\/|v\/|embed\/|@[^/]+\/video\/)/.test(pathname);
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function tiktokOembedUrl(url: string): string {
|
||||
return `https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
/** Extract the numeric video id from a TikTok oEmbed JSON response. */
|
||||
export function tiktokIdFromOembed(data: {
|
||||
embed_product_id?: unknown;
|
||||
html?: unknown;
|
||||
}): string | null {
|
||||
const pid = String(data.embed_product_id ?? '').match(/\d+/)?.[0];
|
||||
if (pid) return pid;
|
||||
if (typeof data.html === 'string') {
|
||||
return data.html.match(/data-video-id="(\d+)"/)?.[1] ?? data.html.match(/\/video\/(\d+)/)?.[1] ?? null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function tiktokPlayerEmbedUrl(id: string): string {
|
||||
return `https://www.tiktok.com/player/v1/${encodeURIComponent(
|
||||
id,
|
||||
)}?autoplay=1&music_info=1&description=1&controls=1&progress_bar=1&play_button=1&volume_control=1&fullscreen_button=1&rel=0`;
|
||||
}
|
||||
|
||||
// --- Dailymotion ----------------------------------------------------------
|
||||
|
||||
export function getDailymotionId(url: string): string | null {
|
||||
@@ -289,13 +325,9 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
|
||||
if (vimeoId)
|
||||
return { provider: 'vimeo', kind: 'landscape', embedUrl: buildVideoEmbedUrl('vimeo', vimeoId) };
|
||||
|
||||
const tiktokId = getTikTokVideoId(url);
|
||||
if (tiktokId)
|
||||
return {
|
||||
provider: 'tiktok',
|
||||
kind: 'portrait',
|
||||
embedUrl: `https://www.tiktok.com/player/v1/${enc(tiktokId)}?music_info=1&description=1&rel=0`,
|
||||
};
|
||||
// NOTE: TikTok is handled by its own card (TikTokEmbedCard) — short "copy-link"
|
||||
// URLs (vm.tiktok.com, tiktok.com/t/…) need a client-side oEmbed lookup to
|
||||
// resolve the video id, which a sync parser can't do. See isTikTokLink below.
|
||||
|
||||
const dmId = getDailymotionId(url);
|
||||
if (dmId)
|
||||
|
||||
Reference in New Issue
Block a user