import { type Video, type InsertVideo } from "@shared/schema"; interface BunnyVideo { guid: string; title: string; length: number; status: number; dateUploaded: string; views: number; thumbnailFileName?: string; category?: string; } 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' } }); if (!response.ok) { throw new Error(`Bunny API error: ${response.status} ${response.statusText}`); } return response.json(); } private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video { // Generate thumbnail URL from CDN (thumbnails are usually public) const thumbnailUrl = `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg`; // 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}`; return { id: bunnyVideo.guid, title: bunnyVideo.title || 'Untitled Video', description: null, // Bunny API doesn't return description in list view thumbnailUrl, videoUrl: hlsUrl, // Signed HLS URL videoUrlMp4: undefined, // Remove MP4 since it likely won't work for private videos videoUrlIframe: iframeUrl, // iframe fallback duration: Math.floor(bunnyVideo.length || 0), views: bunnyVideo.views || 0, category: bunnyVideo.category || null, createdAt: 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