mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
refactor: eslint and prettier conflict will be resolved and formatted
This commit is contained in:
@@ -6,64 +6,65 @@ import { resolveServerBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
// e.g. https://iframe.mediadelivery.net/play/libraryId/videoId
|
||||
// e.g. https://video.bunnycdn.com/play/libraryId/videoId
|
||||
const BUNNY_PATTERNS = [
|
||||
/(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)\/(?:play|embed)\/[0-9]+\/([a-zA-Z0-9_-]+)/,
|
||||
/(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)\/(?:play|embed)\/[0-9]+\/([a-zA-Z0-9_-]+)/,
|
||||
];
|
||||
|
||||
export const bunnyProvider: VideoProvider = {
|
||||
id: 'bunny',
|
||||
name: 'Bunny Stream',
|
||||
icon: 'Video',
|
||||
id: 'bunny',
|
||||
name: 'Bunny Stream',
|
||||
icon: 'Video',
|
||||
|
||||
canHandle(url: string): boolean {
|
||||
return BUNNY_PATTERNS.some(pattern => pattern.test(url));
|
||||
},
|
||||
canHandle(url: string): boolean {
|
||||
return BUNNY_PATTERNS.some((pattern) => pattern.test(url));
|
||||
},
|
||||
|
||||
extractVideoId(url: string): string | null {
|
||||
for (const pattern of BUNNY_PATTERNS) {
|
||||
const match = url.match(pattern);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
extractVideoId(url: string): string | null {
|
||||
for (const pattern of BUNNY_PATTERNS) {
|
||||
const match = url.match(pattern);
|
||||
if (match && match[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getEmbedUrl(videoId: string, options: EmbedOptions = {}): string {
|
||||
// Requires library ID, but our current DB only stores `videoId` for standard providers
|
||||
// For Bunny, we typically store the full embed URL as `originalUrl`
|
||||
// So if this function is called, we try to extract it from the environment or default
|
||||
const libraryId = process.env.NEXT_PUBLIC_BUNNY_STREAM_LIBRARY_ID || process.env.BUNNY_STREAM_LIBRARY_ID || '0';
|
||||
getEmbedUrl(videoId: string, options: EmbedOptions = {}): string {
|
||||
// Requires library ID, but our current DB only stores `videoId` for standard providers
|
||||
// For Bunny, we typically store the full embed URL as `originalUrl`
|
||||
// So if this function is called, we try to extract it from the environment or default
|
||||
const libraryId =
|
||||
process.env.NEXT_PUBLIC_BUNNY_STREAM_LIBRARY_ID || process.env.BUNNY_STREAM_LIBRARY_ID || '0';
|
||||
|
||||
const params = new URLSearchParams();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (options.autoplay) params.set('autoplay', 'true');
|
||||
if (options.loop) params.set('loop', 'true');
|
||||
if (options.muted) params.set('muted', 'true');
|
||||
if (options.autoplay) params.set('autoplay', 'true');
|
||||
if (options.loop) params.set('loop', 'true');
|
||||
if (options.muted) params.set('muted', 'true');
|
||||
|
||||
// We can use video.bunnycdn.com or iframe.mediadelivery.net
|
||||
return `https://iframe.mediadelivery.net/embed/${libraryId}/${videoId}?${params.toString()}`;
|
||||
},
|
||||
// We can use video.bunnycdn.com or iframe.mediadelivery.net
|
||||
return `https://iframe.mediadelivery.net/embed/${libraryId}/${videoId}?${params.toString()}`;
|
||||
},
|
||||
|
||||
getThumbnailUrl(videoId: string): string {
|
||||
const bunnyCdnHostname = resolveServerBunnyCdnHostname();
|
||||
if (!bunnyCdnHostname) return '';
|
||||
return `https://${bunnyCdnHostname}/${videoId}/thumbnail.jpg`;
|
||||
},
|
||||
getThumbnailUrl(videoId: string): string {
|
||||
const bunnyCdnHostname = resolveServerBunnyCdnHostname();
|
||||
if (!bunnyCdnHostname) return '';
|
||||
return `https://${bunnyCdnHostname}/${videoId}/thumbnail.jpg`;
|
||||
},
|
||||
|
||||
async getMetadata(videoId: string): Promise<VideoMetadata> {
|
||||
const cacheKey = `bunny:${videoId}`;
|
||||
const cached = getCachedMetadata(cacheKey);
|
||||
if (cached) return cached;
|
||||
async getMetadata(videoId: string): Promise<VideoMetadata> {
|
||||
const cacheKey = `bunny:${videoId}`;
|
||||
const cached = getCachedMetadata(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
// We can't fetch title/duration via public API without an API key,
|
||||
// so we return basic metadata. When videos are uploaded via our server,
|
||||
// the title will be passed during creation.
|
||||
const fallback: VideoMetadata = {
|
||||
title: 'Bunny Video',
|
||||
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
|
||||
};
|
||||
// We can't fetch title/duration via public API without an API key,
|
||||
// so we return basic metadata. When videos are uploaded via our server,
|
||||
// the title will be passed during creation.
|
||||
const fallback: VideoMetadata = {
|
||||
title: 'Bunny Video',
|
||||
thumbnailUrl: this.getThumbnailUrl(videoId, 'large'),
|
||||
};
|
||||
|
||||
setCachedMetadata(cacheKey, fallback);
|
||||
return fallback;
|
||||
},
|
||||
setCachedMetadata(cacheKey, fallback);
|
||||
return fallback;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { VideoProvider, VideoMetadata, EmbedOptions } from './types';
|
||||
|
||||
// Direct video URL patterns (for future self-hosted videos)
|
||||
const DIRECT_VIDEO_PATTERNS = [
|
||||
/\.(mp4|webm|ogg|mov)(\?.*)?$/i,
|
||||
];
|
||||
const DIRECT_VIDEO_PATTERNS = [/\.(mp4|webm|ogg|mov)(\?.*)?$/i];
|
||||
|
||||
// Security: Validate URL protocol to prevent XSS
|
||||
function isValidVideoUrl(url: string): boolean {
|
||||
@@ -13,7 +11,7 @@ function isValidVideoUrl(url: string): boolean {
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return false;
|
||||
}
|
||||
return DIRECT_VIDEO_PATTERNS.some(pattern => pattern.test(url));
|
||||
return DIRECT_VIDEO_PATTERNS.some((pattern) => pattern.test(url));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -42,9 +40,9 @@ export const directProvider: VideoProvider = {
|
||||
// For direct videos, we'll use HTML5 video player
|
||||
// The videoId IS the URL for direct uploads
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
||||
if (options.startTime) params.set('t', String(Math.floor(options.startTime)));
|
||||
|
||||
|
||||
const queryString = params.toString();
|
||||
return `${videoId}${queryString ? `#t=${options.startTime}` : ''}`;
|
||||
},
|
||||
@@ -61,7 +59,7 @@ export const directProvider: VideoProvider = {
|
||||
// This is a placeholder implementation
|
||||
const filename = videoId.split('/').pop() || 'Video';
|
||||
const nameWithoutExt = filename.replace(/\.[^/.]+$/, '');
|
||||
|
||||
|
||||
return {
|
||||
title: nameWithoutExt,
|
||||
thumbnailUrl: this.getThumbnailUrl(videoId),
|
||||
|
||||
@@ -10,16 +10,10 @@ import { logError } from '@/lib/logger';
|
||||
export * from './types';
|
||||
|
||||
// Registry of all available providers
|
||||
const providers: VideoProvider[] = [
|
||||
youtubeProvider,
|
||||
directProvider,
|
||||
bunnyProvider,
|
||||
];
|
||||
const providers: VideoProvider[] = [youtubeProvider, directProvider, bunnyProvider];
|
||||
|
||||
// Provider lookup map for quick access
|
||||
const providerMap = new Map<string, VideoProvider>(
|
||||
providers.map(p => [p.id, p])
|
||||
);
|
||||
const providerMap = new Map<string, VideoProvider>(providers.map((p) => [p.id, p]));
|
||||
|
||||
/**
|
||||
* Detect which provider can handle a given URL
|
||||
@@ -91,7 +85,10 @@ export async function fetchVideoMetadata(source: VideoSource): Promise<VideoMeta
|
||||
/**
|
||||
* Get embed URL for a video source
|
||||
*/
|
||||
export function getEmbedUrl(source: VideoSource, options?: Parameters<VideoProvider['getEmbedUrl']>[1]): string | null {
|
||||
export function getEmbedUrl(
|
||||
source: VideoSource,
|
||||
options?: Parameters<VideoProvider['getEmbedUrl']>[1]
|
||||
): string | null {
|
||||
const provider = getProvider(source.providerId);
|
||||
|
||||
if (!provider) {
|
||||
@@ -104,7 +101,10 @@ export function getEmbedUrl(source: VideoSource, options?: Parameters<VideoProvi
|
||||
/**
|
||||
* Get thumbnail URL for a video source
|
||||
*/
|
||||
export function getThumbnailUrl(source: VideoSource, size?: Parameters<VideoProvider['getThumbnailUrl']>[1]): string | null {
|
||||
export function getThumbnailUrl(
|
||||
source: VideoSource,
|
||||
size?: Parameters<VideoProvider['getThumbnailUrl']>[1]
|
||||
): string | null {
|
||||
const provider = getProvider(source.providerId);
|
||||
|
||||
if (!provider) {
|
||||
|
||||
@@ -32,7 +32,11 @@ export function getCachedMetadata(key: string): VideoMetadata | null {
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
export function setCachedMetadata(key: string, value: VideoMetadata, ttlMs: number = DEFAULT_TTL_MS): void {
|
||||
export function setCachedMetadata(
|
||||
key: string,
|
||||
value: VideoMetadata,
|
||||
ttlMs: number = DEFAULT_TTL_MS
|
||||
): void {
|
||||
cache.set(key, { value, expiresAt: Date.now() + ttlMs });
|
||||
pruneIfNeeded();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const youtubeProvider: VideoProvider = {
|
||||
icon: 'Youtube',
|
||||
|
||||
canHandle(url: string): boolean {
|
||||
return YOUTUBE_PATTERNS.some(pattern => pattern.test(url));
|
||||
return YOUTUBE_PATTERNS.some((pattern) => pattern.test(url));
|
||||
},
|
||||
|
||||
extractVideoId(url: string): string | null {
|
||||
@@ -28,21 +28,21 @@ export const youtubeProvider: VideoProvider = {
|
||||
|
||||
getEmbedUrl(videoId: string, options: EmbedOptions = {}): string {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
||||
// Enable JS API for programmatic control
|
||||
params.set('enablejsapi', '1');
|
||||
params.set('origin', typeof window !== 'undefined' ? window.location.origin : '');
|
||||
|
||||
|
||||
if (options.autoplay) params.set('autoplay', '1');
|
||||
if (options.startTime) params.set('start', String(Math.floor(options.startTime)));
|
||||
if (options.controls === false) params.set('controls', '0');
|
||||
if (options.loop) params.set('loop', '1');
|
||||
if (options.muted) params.set('mute', '1');
|
||||
|
||||
|
||||
// Better UX options
|
||||
params.set('rel', '0'); // Don't show related videos from other channels
|
||||
params.set('modestbranding', '1'); // Minimal YouTube branding
|
||||
|
||||
|
||||
return `https://www.youtube.com/embed/${videoId}?${params.toString()}`;
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ export const youtubeProvider: VideoProvider = {
|
||||
large: 'hqdefault', // 480x360
|
||||
maxres: 'maxresdefault', // 1280x720
|
||||
};
|
||||
|
||||
|
||||
return `https://img.youtube.com/vi/${videoId}/${sizeMap[size]}.jpg`;
|
||||
},
|
||||
|
||||
@@ -68,13 +68,13 @@ export const youtubeProvider: VideoProvider = {
|
||||
`https://www.youtube.com/oembed?url=https://www.youtube.com/watch?v=${videoId}&format=json`,
|
||||
{ signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch video metadata');
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
const metadata: VideoMetadata = {
|
||||
title: data.title,
|
||||
thumbnailUrl: data.thumbnail_url,
|
||||
|
||||
Reference in New Issue
Block a user