Files
backspace/packages/server/src/utils/embedClassifier.ts
T
Jannis Braun 01ee7ab67a feat: wire embeds into all message query and creation paths
Integrate embed infrastructure into the complete message flow:
- messages.ts: batch-fetch embeds in GET, resolve on POST, re-resolve on PATCH
- dm.ts: same pattern for DM messages with isDm=true
- search.ts: include embeds in all 4 search/around endpoints
- events.ts: embed resolution in WS message create/edit for both space and DM
- Fix embedClassifier.ts type errors (regex match undefined → null)
- Add embeds: [] to all inline MessageWithUser/DmMessageWithUser constructions
- Add embeds: [] to chatStore optimistic message
2026-03-20 23:41:53 +01:00

109 lines
2.9 KiB
TypeScript

import type { EmbedType, EmbedProvider } from '@backspace/shared';
export interface EmbedClassification {
embedType: EmbedType;
provider: EmbedProvider | null;
embedUrl: string | null;
needsMetadataFetch: boolean;
}
function extractYouTubeId(url: URL): string | null {
const host = url.hostname.replace(/^www\./, '');
if (host === 'youtu.be') {
const id = url.pathname.slice(1).split('/')[0];
return id || null;
}
if (host === 'youtube.com' || host === 'm.youtube.com') {
const path = url.pathname;
// /watch?v=ID
if (path === '/watch') {
return url.searchParams.get('v');
}
// /shorts/ID
const shortsMatch = path.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
if (shortsMatch) return shortsMatch[1] ?? null;
// /embed/ID
const embedMatch = path.match(/^\/embed\/([A-Za-z0-9_-]+)/);
if (embedMatch) return embedMatch[1] ?? null;
// /v/ID (legacy)
const vMatch = path.match(/^\/v\/([A-Za-z0-9_-]+)/);
if (vMatch) return vMatch[1] ?? null;
}
return null;
}
export function classifyUrl(rawUrl: string): EmbedClassification {
// Direct media extension checks
if (/\.(jpg|jpeg|png|gif|webp|avif)(\?.*)?$/i.test(rawUrl)) {
return { embedType: 'image', provider: null, embedUrl: null, needsMetadataFetch: false };
}
if (/\.(mp3|ogg|wav|flac|opus)(\?.*)?$/i.test(rawUrl)) {
return { embedType: 'audio', provider: null, embedUrl: null, needsMetadataFetch: false };
}
if (/\.(mp4|webm|mov)(\?.*)?$/i.test(rawUrl)) {
return { embedType: 'video', provider: null, embedUrl: null, needsMetadataFetch: false };
}
// Provider matching requires a valid URL
let url: URL;
try {
url = new URL(rawUrl);
} catch {
return { embedType: 'generic', provider: null, embedUrl: null, needsMetadataFetch: true };
}
const host = url.hostname.replace(/^www\./, '');
// YouTube
if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'youtu.be') {
const videoId = extractYouTubeId(url);
if (videoId) {
return {
embedType: 'video',
provider: 'youtube',
embedUrl: `https://www.youtube-nocookie.com/embed/${videoId}`,
needsMetadataFetch: true,
};
}
}
// Vimeo
if (host === 'vimeo.com') {
const match = url.pathname.match(/^\/(\d+)/);
if (match) {
return {
embedType: 'video',
provider: 'vimeo',
embedUrl: `https://player.vimeo.com/video/${match[1]}`,
needsMetadataFetch: true,
};
}
}
// Spotify
if (host === 'open.spotify.com') {
const match = url.pathname.match(/^\/(track|album|playlist)\/([A-Za-z0-9]+)/);
if (match) {
const [, type, id] = match;
return {
embedType: 'rich',
provider: 'spotify',
embedUrl: `https://open.spotify.com/embed/${type}/${id}`,
needsMetadataFetch: true,
};
}
}
// Everything else
return { embedType: 'generic', provider: null, embedUrl: null, needsMetadataFetch: true };
}