import { type Video, type InsertVideo } from "@shared/schema"; interface BunnyVideo { guid: string; title: string; description?: string; length: number; status: number; dateUploaded: string; views: number; thumbnailFileName?: string; category?: string; metaTags?: Array<{ property: string; value: string; }>; moments?: Array<{ type: string; // "preroll", "midroll", "postroll" position?: number; // seconds for midroll vastTag?: string; duration?: number; network?: string; title?: string; }>; } interface BunnyVideoDetails extends BunnyVideo { description?: string; customMetadata?: Record; } interface BunnyLibraryResponse { items: BunnyVideo[]; currentPage: number; itemsPerPage: number; totalItems: number; } export class BunnyService { private apiKey: string; private libraryId: string; private hostname: string; constructor() { this.apiKey = process.env.BUNNY_API_KEY!; this.libraryId = process.env.BUNNY_LIBRARY_ID!; this.hostname = process.env.BUNNY_HOSTNAME!; if (!this.apiKey || !this.libraryId || !this.hostname) { throw new Error("Missing Bunny.net configuration"); } } private async makeRequest(endpoint: string): Promise { const url = `https://video.bunnycdn.com/library/${this.libraryId}/${endpoint}`; const response = await fetch(url, { headers: { 'AccessKey': this.apiKey, 'Accept': 'application/json', 'Content-Type': 'application/json' }, // Add timeout and connection optimizations signal: AbortSignal.timeout(10000), // 10 second timeout }); if (!response.ok) { throw new Error(`Bunny API error: ${response.status} ${response.statusText}`); } return response.json(); } private bunnyVideoToVideo(bunnyVideo: BunnyVideo | BunnyVideoDetails): Video { // Generate optimized thumbnail URL from Bunny CDN with WebP format for better performance const thumbnailUrl = bunnyVideo.thumbnailFileName ? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?width=400&height=225&format=webp` : `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp`; // Generate signed HLS URL for private video access const hlsUrl = this.generateSignedUrl(bunnyVideo.guid); const iframeUrl = `https://iframe.mediadelivery.net/embed/${this.libraryId}/${bunnyVideo.guid}?preroll=false&postroll=false&ads=false&controls=false`; // Extract description from BunnyVideoDetails if available let description = 'description' in bunnyVideo ? bunnyVideo.description || "" : ""; // Always check metaTags for description since Bunny.net stores it there if (bunnyVideo.metaTags && bunnyVideo.metaTags.length > 0) { const descriptionTag = bunnyVideo.metaTags.find((tag: any) => tag.property?.toLowerCase() === 'description' ); if (descriptionTag && descriptionTag.value) { description = descriptionTag.value; } } // No category from Bunny.net - keeping category empty const category = ""; // No tags from Bunny.net - keeping tags empty const tags: string[] = []; return { id: bunnyVideo.guid, title: bunnyVideo.title || 'Untitled Video', description: description, thumbnailUrl, customThumbnailUrl: null, videoUrl: hlsUrl, // Signed HLS URL videoUrlMp4: hlsUrl, // Use signed HLS URL for preview as well videoUrlIframe: iframeUrl, // iframe fallback duration: Math.floor(bunnyVideo.length || 0), views: bunnyVideo.views || 0, category: category, tags: tags, isPublic: true, uploadStatus: "completed", originalFileName: null, fileSize: null, bitrate: null, resolution: null, format: null, encoding: null, faceCenterPosition: null, facesDetected: null, faceConfidence: null, createdAt: new Date(bunnyVideo.dateUploaded), updatedAt: new Date(bunnyVideo.dateUploaded) }; } async getVideos(page: number = 1, itemsPerPage: number = 20, search?: string): Promise<{ videos: Video[], total: number }> { try { let endpoint = `videos?page=${page}&itemsPerPage=${itemsPerPage}&orderBy=date`; if (search) { endpoint += `&search=${encodeURIComponent(search)}`; } const response: BunnyLibraryResponse = await this.makeRequest(endpoint); // Filter only successfully processed videos (status 4 = finished) const processedVideos = response.items.filter(video => video.status === 4); const videos = processedVideos.map(video => this.bunnyVideoToVideo(video)); return { videos, total: response.totalItems }; } catch (error) { console.error('Error fetching videos from Bunny:', error); throw error; } } async getVideo(guid: string): Promise