From b77e18915f3d8c0368d204dd5d8b17b391644f81 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Mon, 4 Aug 2025 18:32:51 +0000 Subject: [PATCH] Integrate videos directly from Bunny.net CDN for faster streaming Implements Bunny.net CDN integration for video streaming and utilizes its API to fetch and display videos. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 50814a1e-92e4-4968-856f-7bc7eedf5e8f Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/50814a1e-92e4-4968-856f-7bc7eedf5e8f/DXDnT5V --- client/src/components/video-modal.tsx | 2 +- server/bunny.ts | 122 ++++++++++++++++++++++++++ server/routes.ts | 1 + server/storage.ts | 98 ++++++++++++++++++++- 4 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 server/bunny.ts diff --git a/client/src/components/video-modal.tsx b/client/src/components/video-modal.tsx index 322285a..30af466 100644 --- a/client/src/components/video-modal.tsx +++ b/client/src/components/video-modal.tsx @@ -109,8 +109,8 @@ export default function VideoModal({ video, isOpen, onClose }: VideoModalProps) preload="metadata" onPlay={handleVideoPlay} data-testid="video-player" + src={video.videoUrl} > - Your browser does not support the video tag. diff --git a/server/bunny.ts b/server/bunny.ts new file mode 100644 index 0000000..d57a3e2 --- /dev/null +++ b/server/bunny.ts @@ -0,0 +1,122 @@ +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 Bunny CDN + const thumbnailUrl = bunnyVideo.thumbnailFileName + ? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}` + : `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg`; + + // Generate video URL for streaming + const videoUrl = `https://${this.hostname}/${bunnyVideo.guid}/playlist.m3u8`; + + return { + id: bunnyVideo.guid, + title: bunnyVideo.title || 'Untitled Video', + description: null, // Bunny API doesn't return description in list view + thumbnailUrl, + videoUrl, + 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