feat: add URL classifier for embed type detection
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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];
|
||||
|
||||
// /embed/ID
|
||||
const embedMatch = path.match(/^\/embed\/([A-Za-z0-9_-]+)/);
|
||||
if (embedMatch) return embedMatch[1];
|
||||
|
||||
// /v/ID (legacy)
|
||||
const vMatch = path.match(/^\/v\/([A-Za-z0-9_-]+)/);
|
||||
if (vMatch) return vMatch[1];
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user