refactor: eslint and prettier conflict will be resolved and formatted

This commit is contained in:
Enes Köksal
2026-04-23 17:05:43 +03:00
parent 385b61f29b
commit 3cfea40fbd
219 changed files with 16638 additions and 13663 deletions
+48 -47
View File
@@ -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;
},
};
+5 -7
View File
@@ -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 -10
View File
@@ -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) {
+5 -1
View File
@@ -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();
}
+9 -9
View File
@@ -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,