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