videofolxtv/server/storage.ts
sebastjanartic 84c41fe289 Enable users to edit video titles, descriptions, and thumbnails
Introduce functionality to edit video metadata including title, description, category, tags, and public status, along with a new modal for editing and a backend endpoint for updating video information.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 11420304-80a9-4ef2-adff-cbdaa418ffa8
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/11420304-80a9-4ef2-adff-cbdaa418ffa8/vOcB2tQ
2025-08-07 08:26:08 +00:00

284 lines
10 KiB
TypeScript

import { type Video, type InsertVideo, type UpdateVideo } from "@shared/schema";
import { randomUUID } from "crypto";
import { BunnyService } from "./bunny";
export interface IStorage {
getVideos(limit?: number, offset?: number, search?: string): Promise<Video[]>;
getVideo(id: string): Promise<Video | undefined>;
createVideo(video: InsertVideo): Promise<Video>;
updateVideo(id: string, video: UpdateVideo): Promise<Video | undefined>;
updateVideoViews(id: string): Promise<void>;
getVideoCount(search?: string): Promise<number>;
}
export class MemStorage implements IStorage {
private videos: Map<string, Video>;
constructor() {
this.videos = new Map();
// Initialize with some sample videos for demonstration
// In production, these would be fetched from bunny.net API
this.initializeSampleVideos();
}
private initializeSampleVideos() {
const sampleVideos: InsertVideo[] = [
{
title: "Advanced Web Development Techniques and Best Practices",
description: "Learn modern web development techniques including React, TypeScript, and performance optimization strategies.",
thumbnailUrl: "https://images.unsplash.com/photo-1560472355-536de3962603?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&h=450",
videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
duration: 754, // 12:34
views: 2100,
category: "Tutorial"
},
{
title: "Team Collaboration Strategies for Remote Work",
description: "Effective strategies for managing remote teams and improving collaboration in distributed environments.",
thumbnailUrl: "https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&h=450",
videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
duration: 495, // 8:15
views: 856,
category: "Business"
},
{
title: "Modern UI/UX Design Principles and Workflows",
description: "Comprehensive guide to modern design principles, user experience optimization, and design system creation.",
thumbnailUrl: "https://images.unsplash.com/photo-1581291518857-4e27b48ff24e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&h=450",
videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
duration: 942, // 15:42
views: 3700,
category: "Design"
},
{
title: "Data Analytics and Business Intelligence Tutorial",
description: "Learn how to analyze data effectively using modern tools and create meaningful business insights.",
thumbnailUrl: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&h=450",
videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
duration: 1328, // 22:08
views: 1400,
category: "Analytics"
},
{
title: "Mobile App Development: From Concept to Launch",
description: "Complete guide to mobile app development covering design, development, testing, and deployment strategies.",
thumbnailUrl: "https://images.unsplash.com/photo-1512941937669-90a1b58e7e9c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&h=450",
videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4",
duration: 1113, // 18:33
views: 4200,
category: "Mobile"
},
{
title: "Cloud Infrastructure and DevOps Fundamentals",
description: "Understanding cloud computing, infrastructure as code, and DevOps practices for modern applications.",
thumbnailUrl: "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&h=450",
videoUrl: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4",
duration: 1517, // 25:17
views: 987,
category: "DevOps"
}
];
sampleVideos.forEach(video => {
const id = randomUUID();
const fullVideo: Video = {
...video,
id,
description: video.description || "",
category: video.category || "",
customThumbnailUrl: null,
videoUrlMp4: null,
videoUrlIframe: null,
tags: [],
isPublic: true,
createdAt: new Date(),
updatedAt: new Date()
};
this.videos.set(id, fullVideo);
});
}
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
let videos = Array.from(this.videos.values());
// Filter by search
if (search) {
const searchLower = search.toLowerCase();
videos = videos.filter(video =>
video.title.toLowerCase().includes(searchLower) ||
video.description?.toLowerCase().includes(searchLower)
);
}
// Sort by created date (newest first)
videos.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return videos.slice(offset, offset + limit);
}
async getVideo(id: string): Promise<Video | undefined> {
return this.videos.get(id);
}
async createVideo(video: InsertVideo): Promise<Video> {
const id = randomUUID();
const fullVideo: Video = {
...video,
id,
description: video.description || "",
category: video.category || "",
customThumbnailUrl: null,
videoUrlMp4: null,
videoUrlIframe: null,
tags: video.tags || [],
isPublic: video.isPublic ?? true,
createdAt: new Date(),
updatedAt: new Date()
};
this.videos.set(id, fullVideo);
return fullVideo;
}
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
const video = this.videos.get(id);
if (!video) return undefined;
const updatedVideo: Video = {
...video,
...updates,
updatedAt: new Date()
};
this.videos.set(id, updatedVideo);
return updatedVideo;
}
async updateVideoViews(id: string): Promise<void> {
const video = this.videos.get(id);
if (video) {
video.views += 1;
this.videos.set(id, video);
}
}
async getVideoCount(search?: string): Promise<number> {
const videos = await this.getVideos(1000, 0, search);
return videos.length;
}
}
// Use Bunny.net storage if API keys are available, otherwise fallback to memory storage
class BunnyStorage implements IStorage {
private bunnyService: BunnyService;
private viewsCache: Map<string, number> = new Map();
constructor() {
this.bunnyService = new BunnyService();
}
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
try {
// For search filtering, we need to get more videos than the requested limit
// because we'll filter client-side and then slice to the requested amount
const fetchLimit = search ? 100 : limit;
const page = Math.floor(offset / fetchLimit) + 1;
// Get videos from Bunny API
const { videos } = await this.bunnyService.getVideos(page, fetchLimit);
// Apply client-side filtering
let filteredVideos = videos;
// Filter by search
if (search) {
const searchLower = search.toLowerCase();
filteredVideos = filteredVideos.filter(video =>
video.title.toLowerCase().includes(searchLower) ||
(video.description && video.description.toLowerCase().includes(searchLower))
);
}
// Apply cached view counts
filteredVideos.forEach(video => {
if (this.viewsCache.has(video.id)) {
video.views += this.viewsCache.get(video.id)!;
}
});
// Apply pagination after filtering
return filteredVideos.slice(offset, offset + limit);
} catch (error) {
console.error('Error fetching videos from Bunny:', error);
// Fallback to empty array on error
return [];
}
}
async getVideo(id: string): Promise<Video | undefined> {
try {
const video = await this.bunnyService.getVideo(id);
if (!video) return undefined;
// Apply cached view counts
if (this.viewsCache.has(video.id)) {
video.views += this.viewsCache.get(video.id)!;
}
return video;
} catch (error) {
console.error(`Error fetching video ${id} from Bunny:`, error);
return undefined;
}
}
async createVideo(video: InsertVideo): Promise<Video> {
// Note: Creating videos would require uploading to Bunny.net
// For now, we'll throw an error as this operation is not supported
throw new Error("Creating videos is not supported with Bunny.net integration");
}
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
// Note: Updating video metadata in Bunny.net would require API calls
// For now, we'll throw an error as this operation is not supported
throw new Error("Updating videos is not supported with Bunny.net integration");
}
async updateVideoViews(id: string): Promise<void> {
// Since we can't update views in Bunny.net directly, we'll cache them locally
const currentViews = this.viewsCache.get(id) || 0;
this.viewsCache.set(id, currentViews + 1);
}
async getVideoCount(search?: string): Promise<number> {
try {
// For accurate count with client-side filtering, we need to get all videos and filter them
const allVideos = await this.getVideos(1000, 0, search);
return allVideos.length;
} catch (error) {
console.error('Error getting video count from Bunny:', error);
return 0;
}
}
}
// Try to use Bunny.net storage, fallback to memory storage if not configured
let storage: IStorage;
// Check if Bunny.net environment variables are available
const hasBunnyConfig = process.env.BUNNY_API_KEY && process.env.BUNNY_LIBRARY_ID && process.env.BUNNY_HOSTNAME;
if (hasBunnyConfig) {
try {
storage = new BunnyStorage();
console.log('✅ Using Bunny.net storage with library ID:', process.env.BUNNY_LIBRARY_ID);
} catch (error) {
console.error('❌ Failed to initialize Bunny.net storage:', error);
console.log('📁 Falling back to memory storage');
storage = new MemStorage();
}
} else {
console.log('📁 Bunny.net environment variables not found, using memory storage');
storage = new MemStorage();
}
export { storage };