videofolxtv/server/storage.ts

1188 lines
40 KiB
TypeScript

import {
type Video, type InsertVideo, type UpdateVideo,
type User, type InsertUser,
type VideoUpload, type InsertVideoUpload,
type Category, type InsertCategory,
type Tag, type InsertTag,
videos, users, videoUploads, categories, tags, videoTags
} from "@shared/schema";
import { randomUUID } from "crypto";
import { BunnyService } from "./bunny";
import { videoSyncService } from "./videoSync";
import { db } from "./db";
import { eq, desc, asc, like, or, sql, and } from "drizzle-orm";
import bcrypt from "bcryptjs";
export interface IStorage {
// Video operations
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>;
deleteVideo(id: string): Promise<boolean>;
// User operations
getUser(id: string): Promise<User | undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
getAllUsers(limit?: number, offset?: number, search?: string): Promise<User[]>;
getUserCount(search?: string): Promise<number>;
createUser(user: InsertUser): Promise<User>;
updateUser(id: string, user: Partial<InsertUser>): Promise<User | undefined>;
upsertUser(user: any): Promise<User>;
validateUserPassword(email: string, password: string): Promise<User | null>;
// Upload operations
createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload>;
getVideoUpload(id: string): Promise<VideoUpload | undefined>;
updateVideoUpload(id: string, upload: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined>;
getUserVideoUploads(userId: string): Promise<VideoUpload[]>;
// Category operations
getCategories(): Promise<Category[]>;
createCategory(category: InsertCategory): Promise<Category>;
updateCategory(id: string, category: Partial<InsertCategory>): Promise<Category | undefined>;
deleteCategory(id: string): Promise<boolean>;
// Tag operations
getTags(): Promise<Tag[]>;
getPopularTags(limit?: number): Promise<Tag[]>;
createTag(tag: InsertTag): Promise<Tag>;
incrementTagUse(name: string): Promise<void>;
}
// Database storage implementation using PostgreSQL
export class DatabaseStorage implements IStorage {
// Video operations
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
let query = db.select().from(videos);
if (search) {
const searchTerm = `%${search}%`;
query = query.where(
and(
eq(videos.isPublic, true),
or(
like(videos.title, searchTerm),
like(videos.description, searchTerm)
)
)
) as any;
} else {
query = query.where(eq(videos.isPublic, true)) as any;
}
const result = await query
.orderBy(desc(videos.createdAt))
.limit(limit)
.offset(offset);
return result;
}
async getVideo(id: string): Promise<Video | undefined> {
const result = await db.select().from(videos).where(eq(videos.id, id));
return result[0];
}
async createVideo(video: InsertVideo): Promise<Video> {
const result = await db.insert(videos).values({
...video,
updatedAt: new Date()
}).returning();
return result[0];
}
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
const result = await db.update(videos)
.set({ ...updates, updatedAt: new Date() })
.where(eq(videos.id, id))
.returning();
return result[0];
}
async updateVideoViews(id: string): Promise<void> {
await db.update(videos)
.set({ views: sql`${videos.views} + 1` })
.where(eq(videos.id, id));
}
async getVideoCount(search?: string): Promise<number> {
let query = db.select({ count: sql<number>`count(*)` }).from(videos);
if (search) {
const searchTerm = `%${search}%`;
query = query.where(
and(
eq(videos.isPublic, true),
or(
like(videos.title, searchTerm),
like(videos.description, searchTerm)
)
)
) as any;
} else {
query = query.where(eq(videos.isPublic, true)) as any;
}
const result = await query;
return result[0].count;
}
async deleteVideo(id: string): Promise<boolean> {
const result = await db.delete(videos).where(eq(videos.id, id));
return (result.rowCount ?? 0) > 0;
}
// User operations
async getUser(id: string): Promise<User | undefined> {
const result = await db.select().from(users).where(eq(users.id, id));
return result[0];
}
async getUserByEmail(email: string): Promise<User | undefined> {
const result = await db.select().from(users).where(eq(users.email, email));
return result[0];
}
async getUserByUsername(username: string): Promise<User | undefined> {
const result = await db.select().from(users).where(eq(users.username, username));
return result[0];
}
async createUser(user: InsertUser): Promise<User> {
// Hash password before storing
const hashedPassword = await bcrypt.hash(user.passwordHash, 12);
const { passwordHash: _, ...userWithoutPassword } = user;
const result = await db.insert(users).values({
...userWithoutPassword,
passwordHash: hashedPassword,
updatedAt: new Date()
} as any).returning();
return result[0];
}
async updateUser(id: string, updates: Partial<InsertUser>): Promise<User | undefined> {
const updateData: any = { ...updates, updatedAt: new Date() };
// Hash password if it's being updated
if (updates.passwordHash) {
updateData.passwordHash = await bcrypt.hash(updates.passwordHash, 12);
}
const result = await db.update(users)
.set(updateData)
.where(eq(users.id, id))
.returning();
return result[0];
}
async upsertUser(userData: any): Promise<User> {
const [user] = await db
.insert(users)
.values({
id: userData.id,
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
avatar: userData.profileImageUrl,
username: userData.email || `user_${userData.id}`,
passwordHash: '', // No password for OAuth users
isAdmin: false,
isSuperAdmin: false,
})
.onConflictDoUpdate({
target: users.id,
set: {
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
avatar: userData.profileImageUrl,
updatedAt: new Date(),
},
})
.returning();
return user;
}
async getAllUsers(limit = 50, offset = 0, search?: string): Promise<User[]> {
let query = db.select().from(users);
if (search) {
const searchTerm = `%${search}%`;
query = query.where(
or(
like(users.username, searchTerm),
like(users.email, searchTerm),
like(users.firstName, searchTerm),
like(users.lastName, searchTerm)
)
) as any;
}
const result = await query
.orderBy(desc(users.createdAt))
.limit(limit)
.offset(offset);
return result;
}
async getUserCount(search?: string): Promise<number> {
let query = db.select({ count: sql<number>`count(*)` }).from(users);
if (search) {
const searchTerm = `%${search}%`;
query = query.where(
or(
like(users.username, searchTerm),
like(users.email, searchTerm),
like(users.firstName, searchTerm),
like(users.lastName, searchTerm)
)
) as any;
}
const result = await query;
return result[0].count;
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
const user = await this.getUserByEmail(email);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.passwordHash);
return isValid ? user : null;
}
// Upload operations
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
const result = await db.insert(videoUploads).values({
...upload,
updatedAt: new Date()
}).returning();
return result[0];
}
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
const result = await db.select().from(videoUploads).where(eq(videoUploads.id, id));
return result[0];
}
async updateVideoUpload(id: string, updates: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
const result = await db.update(videoUploads)
.set({ ...updates, updatedAt: new Date() })
.where(eq(videoUploads.id, id))
.returning();
return result[0];
}
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
return await db.select().from(videoUploads)
.where(eq(videoUploads.userId, userId))
.orderBy(desc(videoUploads.createdAt));
}
// Category operations
async getCategories(): Promise<Category[]> {
return await db.select().from(categories).orderBy(asc(categories.name));
}
async createCategory(category: InsertCategory): Promise<Category> {
const result = await db.insert(categories).values(category).returning();
return result[0];
}
async updateCategory(id: string, updates: Partial<InsertCategory>): Promise<Category | undefined> {
const result = await db.update(categories)
.set(updates)
.where(eq(categories.id, id))
.returning();
return result[0];
}
async deleteCategory(id: string): Promise<boolean> {
const result = await db.delete(categories).where(eq(categories.id, id));
return (result.rowCount ?? 0) > 0;
}
// Tag operations
async getTags(): Promise<Tag[]> {
return await db.select().from(tags).orderBy(desc(tags.useCount));
}
async getPopularTags(limit = 10): Promise<Tag[]> {
return await db.select().from(tags)
.orderBy(desc(tags.useCount))
.limit(limit);
}
async createTag(tag: InsertTag): Promise<Tag> {
const result = await db.insert(tags).values(tag).returning();
return result[0];
}
async incrementTagUse(name: string): Promise<void> {
await db.update(tags)
.set({ useCount: sql`${tags.useCount} + 1` })
.where(eq(tags.name, name));
}
}
export class MemStorage implements IStorage {
private videos: Map<string, Video>;
private users: Map<string, User>;
private uploads: Map<string, VideoUpload>;
private categoriesMap: Map<string, Category>;
private tagsMap: Map<string, Tag>;
constructor() {
this.videos = new Map();
this.users = new Map();
this.uploads = new Map();
this.categoriesMap = new Map();
this.tagsMap = new Map();
this.initializeSampleData();
}
private async initializeSampleData() {
// Initialize sample categories
const sampleCategories: Category[] = [
{ id: randomUUID(), name: "Tutorial", description: "Educational content", color: "#3B82F6", createdAt: new Date() },
{ id: randomUUID(), name: "Business", description: "Business and professional content", color: "#10B981", createdAt: new Date() },
{ id: randomUUID(), name: "Design", description: "UI/UX and design content", color: "#F59E0B", createdAt: new Date() },
{ id: randomUUID(), name: "Analytics", description: "Data and analytics content", color: "#EF4444", createdAt: new Date() },
{ id: randomUUID(), name: "Mobile", description: "Mobile development", color: "#8B5CF6", createdAt: new Date() },
{ id: randomUUID(), name: "DevOps", description: "DevOps and infrastructure", color: "#06B6D4", createdAt: new Date() }
];
sampleCategories.forEach(category => {
this.categoriesMap.set(category.id, category);
});
// Initialize sample tags
const sampleTags: Tag[] = [
{ id: randomUUID(), name: "react", useCount: 25, createdAt: new Date() },
{ id: randomUUID(), name: "typescript", useCount: 20, createdAt: new Date() },
{ id: randomUUID(), name: "javascript", useCount: 30, createdAt: new Date() },
{ id: randomUUID(), name: "frontend", useCount: 18, createdAt: new Date() },
{ id: randomUUID(), name: "backend", useCount: 15, createdAt: new Date() },
{ id: randomUUID(), name: "tutorial", useCount: 40, createdAt: new Date() },
];
sampleTags.forEach(tag => {
this.tagsMap.set(tag.id, tag);
});
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 || "",
artist: null, // Add artist field
filename: null, // Original filename
episodeNumber: null, // Episode number for shows
episodeTitle: null, // Episode title for shows
customThumbnailUrl: null,
faceCenterPosition: null,
facesDetected: 0,
faceConfidence: 0,
videoUrlMp4: null,
videoUrlIframe: null,
tags: [],
isPublic: true,
views: video.views || 0,
contentType: video.contentType || 'video',
genre: video.genre || 'other',
createdAt: new Date(),
updatedAt: new Date(),
uploadStatus: video.uploadStatus || "completed",
originalFileName: video.originalFileName || null,
fileSize: video.fileSize || null,
bitrate: video.bitrate || null,
resolution: video.resolution || null,
format: video.format || null,
encoding: video.encoding || null
};
this.videos.set(id, fullVideo);
});
}
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
let videos = Array.from(this.videos.values());
// Optimized search - only search meaningful queries (2+ chars)
if (search && search.length >= 2) {
const searchLower = search.toLowerCase();
videos = videos.filter(video => {
// Check title first (most common match)
if (video.title.toLowerCase().includes(searchLower)) return true;
// Check description if exists
return video.description?.toLowerCase().includes(searchLower) || false;
});
}
// Sort by created date (newest first) - more efficient sort
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 || "",
artist: video.artist || null, // Add artist field
filename: video.filename || null,
episodeNumber: video.episodeNumber || null,
episodeTitle: video.episodeTitle || null,
customThumbnailUrl: null,
faceCenterPosition: null,
facesDetected: 0,
faceConfidence: 0,
videoUrlMp4: null,
videoUrlIframe: null,
tags: video.tags || [],
isPublic: video.isPublic ?? true,
views: video.views || 0,
contentType: video.contentType || 'video',
genre: video.genre || 'other',
uploadStatus: video.uploadStatus || "completed",
originalFileName: video.originalFileName || null,
fileSize: video.fileSize || null,
bitrate: video.bitrate || null,
resolution: video.resolution || null,
format: video.format || null,
encoding: video.encoding || null,
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> {
// More efficient count without loading all data
let videos = Array.from(this.videos.values());
if (search && search.length >= 2) {
const searchLower = search.toLowerCase();
videos = videos.filter(video =>
video.title.toLowerCase().includes(searchLower) ||
video.description?.toLowerCase().includes(searchLower)
);
}
return videos.length;
}
async deleteVideo(id: string): Promise<boolean> {
return this.videos.delete(id);
}
// User operations
async getUser(id: string): Promise<User | undefined> {
return this.users.get(id);
}
async getUserByEmail(email: string): Promise<User | undefined> {
return Array.from(this.users.values()).find(user => user.email === email);
}
async getUserByUsername(username: string): Promise<User | undefined> {
return Array.from(this.users.values()).find(user => user.username === username);
}
async createUser(user: InsertUser): Promise<User> {
const id = randomUUID();
const hashedPassword = await bcrypt.hash(user.passwordHash, 12);
const fullUser: User = {
...user,
id,
passwordHash: hashedPassword,
firstName: user.firstName || null,
lastName: user.lastName || null,
avatar: user.avatar || null,
isActive: user.isActive ?? true,
isAdmin: user.isAdmin ?? false,
isSuperAdmin: user.isSuperAdmin ?? false,
createdAt: new Date(),
updatedAt: new Date()
};
this.users.set(id, fullUser);
return fullUser;
}
async updateUser(id: string, updates: Partial<InsertUser>): Promise<User | undefined> {
const user = this.users.get(id);
if (!user) return undefined;
const updateData: any = { ...updates, updatedAt: new Date() };
if (updates.passwordHash) {
updateData.passwordHash = await bcrypt.hash(updates.passwordHash, 12);
}
const updatedUser: User = {
...user,
...updateData
};
this.users.set(id, updatedUser);
return updatedUser;
}
async upsertUser(userData: any): Promise<User> {
const existingUser = this.users.get(userData.id);
const user: User = {
id: userData.id,
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
avatar: userData.profileImageUrl,
username: userData.email || `user_${userData.id}`,
passwordHash: '',
isActive: existingUser?.isActive ?? true,
isAdmin: existingUser?.isAdmin || false,
isSuperAdmin: existingUser?.isSuperAdmin || false,
createdAt: existingUser?.createdAt || new Date(),
updatedAt: new Date(),
};
this.users.set(userData.id, user);
return user;
}
async getAllUsers(limit = 50, offset = 0, search?: string): Promise<User[]> {
let users = Array.from(this.users.values());
if (search && search.length >= 2) {
const searchLower = search.toLowerCase();
users = users.filter(user =>
user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.firstName?.toLowerCase().includes(searchLower) ||
user.lastName?.toLowerCase().includes(searchLower)
);
}
// Sort by created date (newest first)
users.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return users.slice(offset, offset + limit);
}
async getUserCount(search?: string): Promise<number> {
let users = Array.from(this.users.values());
if (search && search.length >= 2) {
const searchLower = search.toLowerCase();
users = users.filter(user =>
user.username.toLowerCase().includes(searchLower) ||
user.email.toLowerCase().includes(searchLower) ||
user.firstName?.toLowerCase().includes(searchLower) ||
user.lastName?.toLowerCase().includes(searchLower)
);
}
return users.length;
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
const user = await this.getUserByEmail(email);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.passwordHash);
return isValid ? user : null;
}
// Upload operations
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
const id = randomUUID();
const fullUpload: VideoUpload = {
...upload,
id,
uploadStatus: upload.uploadStatus || "uploading",
uploadProgress: upload.uploadProgress || 0,
videoId: upload.videoId || null,
errorMessage: upload.errorMessage || null,
uploadUrl: upload.uploadUrl || null,
createdAt: new Date(),
updatedAt: new Date()
};
this.uploads.set(id, fullUpload);
return fullUpload;
}
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
return this.uploads.get(id);
}
async updateVideoUpload(id: string, updates: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
const upload = this.uploads.get(id);
if (!upload) return undefined;
const updatedUpload: VideoUpload = {
...upload,
...updates,
updatedAt: new Date()
};
this.uploads.set(id, updatedUpload);
return updatedUpload;
}
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
return Array.from(this.uploads.values())
.filter(upload => upload.userId === userId)
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
// Category operations
async getCategories(): Promise<Category[]> {
return Array.from(this.categoriesMap.values())
.sort((a, b) => a.name.localeCompare(b.name));
}
async createCategory(category: InsertCategory): Promise<Category> {
const id = randomUUID();
const fullCategory: Category = {
...category,
id,
description: category.description || null,
color: category.color || "#000000",
createdAt: new Date()
};
this.categoriesMap.set(id, fullCategory);
return fullCategory;
}
async updateCategory(id: string, updates: Partial<InsertCategory>): Promise<Category | undefined> {
const category = this.categoriesMap.get(id);
if (!category) return undefined;
const updatedCategory: Category = {
...category,
...updates
};
this.categoriesMap.set(id, updatedCategory);
return updatedCategory;
}
async deleteCategory(id: string): Promise<boolean> {
return this.categoriesMap.delete(id);
}
// Tag operations
async getTags(): Promise<Tag[]> {
return Array.from(this.tagsMap.values())
.sort((a, b) => b.useCount - a.useCount);
}
async getPopularTags(limit = 10): Promise<Tag[]> {
return this.getTags().then(tags => tags.slice(0, limit));
}
async createTag(tag: InsertTag): Promise<Tag> {
const id = randomUUID();
const fullTag: Tag = {
...tag,
id,
useCount: tag.useCount ?? 0,
createdAt: new Date()
};
this.tagsMap.set(id, fullTag);
return fullTag;
}
async incrementTagUse(name: string): Promise<void> {
const tag = Array.from(this.tagsMap.values()).find(t => t.name === name);
if (tag) {
tag.useCount += 1;
this.tagsMap.set(tag.id, tag);
}
}
}
// Use Bunny.net storage if API keys are available, otherwise fallback to memory storage
class BunnyStorage implements IStorage {
private bunnyService: BunnyService;
private faceDataCache: Map<string, { faceCenterPosition?: string; facesDetected?: number; faceConfidence?: number }> = new Map();
constructor() {
this.bunnyService = new BunnyService();
}
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
console.log(`Fetching videos from cache: limit=${limit}, offset=${offset}, search=${search}`);
const result = videoSyncService.getVideos(limit, offset, search);
console.log(`Returning ${result.videos.length} videos from cache (age: ${result.cacheAge}ms)`);
// Apply face detection data from cache
return result.videos.map(video => {
const faceData = this.faceDataCache.get(video.id);
return faceData ? { ...video, ...faceData } : video;
});
}
async getVideo(id: string): Promise<Video | undefined> {
// Try cache first for faster loading
const cachedVideo = videoSyncService.getVideos(100, 0).videos.find(v => v.id === id);
if (cachedVideo) {
// Apply face detection data from cache
const faceData = this.faceDataCache.get(cachedVideo.id);
return faceData ? { ...cachedVideo, ...faceData } : cachedVideo;
}
// Fallback to direct API call
try {
const video = await this.bunnyService.getVideo(id);
if (!video) return undefined;
// Apply face detection data from cache
const faceData = this.faceDataCache.get(video.id);
return faceData ? { ...video, ...faceData } : video;
} catch (error) {
console.error(`Error fetching video ${id} from Bunny:`, error);
return undefined;
}
}
async createVideo(video: InsertVideo): Promise<Video> {
throw new Error("Creating videos is not supported with Bunny.net integration");
}
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
// For admin edits, allow all metadata updates
// Face detection and views require special handling but metadata can be updated
try {
// Try to update in database first
let dbUpdateSuccessful = false;
try {
console.log(`Updating video ${id} with data:`, JSON.stringify(updates, null, 2));
await db.update(videos).set({...updates, updatedAt: new Date()}).where(eq(videos.id, id));
console.log(`Successfully updated video ${id} in database`);
dbUpdateSuccessful = true;
} catch (dbError) {
console.warn(`Database update failed for video ${id}, using cache:`, dbError);
}
// If database update was successful, force video sync refresh
if (dbUpdateSuccessful) {
try {
// Force videoSyncService to refresh this specific video
console.log(`Updated video ${id} in database, forcing cache refresh`);
videoSyncService.forceVideoRefresh(id);
} catch (cacheError) {
console.warn('Failed to update local cache after video update:', cacheError);
}
}
// Update face detection data in cache if present
const faceFields = ['faceCenterPosition', 'facesDetected', 'faceConfidence'];
const faceData = Object.fromEntries(
Object.entries(updates).filter(([key]) => faceFields.includes(key))
);
if (Object.keys(faceData).length > 0) {
const existing = this.faceDataCache.get(id) || {};
this.faceDataCache.set(id, { ...existing, ...faceData });
}
// Return updated video
return await this.getVideo(id);
} catch (error) {
console.error(`Error updating video ${id}:`, error);
throw error;
}
}
async deleteVideo(id: string): Promise<boolean> {
throw new Error("Deleting videos is not supported with Bunny.net integration");
}
// User operations (not supported with Bunny.net)
async getUser(id: string): Promise<User | undefined> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async getUserByEmail(email: string): Promise<User | undefined> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async getUserByUsername(username: string): Promise<User | undefined> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async createUser(user: InsertUser): Promise<User> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async updateUser(id: string, updates: Partial<InsertUser>): Promise<User | undefined> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async upsertUser(userData: any): Promise<User> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async getAllUsers(limit?: number, offset?: number, search?: string): Promise<User[]> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async getUserCount(search?: string): Promise<number> {
throw new Error("User operations are not supported with Bunny.net integration");
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
throw new Error("User operations are not supported with Bunny.net integration");
}
// Upload operations (not supported with Bunny.net)
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
throw new Error("Upload operations are not supported with Bunny.net integration");
}
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
throw new Error("Upload operations are not supported with Bunny.net integration");
}
async updateVideoUpload(id: string, updates: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
throw new Error("Upload operations are not supported with Bunny.net integration");
}
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
throw new Error("Upload operations are not supported with Bunny.net integration");
}
// Category operations (not supported with Bunny.net)
async getCategories(): Promise<Category[]> {
throw new Error("Category operations are not supported with Bunny.net integration");
}
async createCategory(category: InsertCategory): Promise<Category> {
throw new Error("Category operations are not supported with Bunny.net integration");
}
async updateCategory(id: string, updates: Partial<InsertCategory>): Promise<Category | undefined> {
throw new Error("Category operations are not supported with Bunny.net integration");
}
async deleteCategory(id: string): Promise<boolean> {
throw new Error("Category operations are not supported with Bunny.net integration");
}
// Tag operations (not supported with Bunny.net)
async getTags(): Promise<Tag[]> {
throw new Error("Tag operations are not supported with Bunny.net integration");
}
async getPopularTags(limit = 10): Promise<Tag[]> {
throw new Error("Tag operations are not supported with Bunny.net integration");
}
async createTag(tag: InsertTag): Promise<Tag> {
throw new Error("Tag operations are not supported with Bunny.net integration");
}
async incrementTagUse(name: string): Promise<void> {
throw new Error("Tag operations are not supported with Bunny.net integration");
}
async updateVideoViews(id: string): Promise<void> {
// Views are tracked automatically by Bunny.net CDN when videos are played
// No additional tracking needed - views are fetched fresh every 5 minutes from Bunny.net API
console.log(`Video ${id} played - views tracked by Bunny.net CDN`);
}
async getVideoCount(search?: string): Promise<number> {
try {
// Get the total from Bunny API directly for better performance
const { total } = await this.bunnyService.getVideos(1, 1);
// If no search, return total count
if (!search) {
return total;
}
// For search, we need to get videos and count filtered results
// This is expensive but needed for accurate search counts
const { videos } = await this.bunnyService.getVideos(1, 1000);
const searchLower = search.toLowerCase();
const filteredCount = videos.filter(video =>
video.title.toLowerCase().includes(searchLower) ||
(video.description && video.description.toLowerCase().includes(searchLower))
).length;
return filteredCount;
} catch (error) {
console.error('Error getting video count from Bunny:', error);
return 0;
}
}
}
// Hybrid storage implementation that uses Bunny.net for videos and Database for users
export class HybridStorage implements IStorage {
private bunnyStorage: BunnyStorage;
private databaseStorage: DatabaseStorage;
constructor() {
this.bunnyStorage = new BunnyStorage();
this.databaseStorage = new DatabaseStorage();
}
// Video operations - use Bunny.net
async getVideos(limit?: number, offset?: number, search?: string): Promise<Video[]> {
return this.databaseStorage.getVideos(limit, offset, search);
}
async getVideo(id: string): Promise<Video | undefined> {
return this.databaseStorage.getVideo(id);
}
async createVideo(video: InsertVideo): Promise<Video> {
return this.bunnyStorage.createVideo(video);
}
async updateVideo(id: string, video: UpdateVideo): Promise<Video | undefined> {
// Save changes ONLY to PostgreSQL database - do not update Bunny cache
return this.databaseStorage.updateVideo(id, video);
}
async updateVideoViews(id: string): Promise<void> {
return this.databaseStorage.updateVideoViews(id);
}
async getVideoCount(search?: string): Promise<number> {
return this.databaseStorage.getVideoCount(search);
}
async deleteVideo(id: string): Promise<boolean> {
return this.bunnyStorage.deleteVideo(id);
}
// User operations - use Database
async getUser(id: string): Promise<User | undefined> {
return this.databaseStorage.getUser(id);
}
async getUserByEmail(email: string): Promise<User | undefined> {
return this.databaseStorage.getUserByEmail(email);
}
async getUserByUsername(username: string): Promise<User | undefined> {
return this.databaseStorage.getUserByUsername(username);
}
async createUser(user: InsertUser): Promise<User> {
return this.databaseStorage.createUser(user);
}
async updateUser(id: string, user: Partial<InsertUser>): Promise<User | undefined> {
return this.databaseStorage.updateUser(id, user);
}
async upsertUser(user: any): Promise<User> {
return this.databaseStorage.upsertUser(user);
}
async getAllUsers(limit?: number, offset?: number, search?: string): Promise<User[]> {
return this.databaseStorage.getAllUsers(limit, offset, search);
}
async getUserCount(search?: string): Promise<number> {
return this.databaseStorage.getUserCount(search);
}
async validateUserPassword(email: string, password: string): Promise<User | null> {
return this.databaseStorage.validateUserPassword(email, password);
}
// Upload operations - use Database
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
return this.databaseStorage.createVideoUpload(upload);
}
async getVideoUpload(id: string): Promise<VideoUpload | undefined> {
return this.databaseStorage.getVideoUpload(id);
}
async updateVideoUpload(id: string, upload: Partial<InsertVideoUpload>): Promise<VideoUpload | undefined> {
return this.databaseStorage.updateVideoUpload(id, upload);
}
async getUserVideoUploads(userId: string): Promise<VideoUpload[]> {
return this.databaseStorage.getUserVideoUploads(userId);
}
// Category operations - use Database
async getCategories(): Promise<Category[]> {
return this.databaseStorage.getCategories();
}
async createCategory(category: InsertCategory): Promise<Category> {
return this.databaseStorage.createCategory(category);
}
async updateCategory(id: string, category: Partial<InsertCategory>): Promise<Category | undefined> {
return this.databaseStorage.updateCategory(id, category);
}
async deleteCategory(id: string): Promise<boolean> {
return this.databaseStorage.deleteCategory(id);
}
// Tag operations - use Database
async getTags(): Promise<Tag[]> {
return this.databaseStorage.getTags();
}
async getPopularTags(limit?: number): Promise<Tag[]> {
return this.databaseStorage.getPopularTags(limit);
}
async createTag(tag: InsertTag): Promise<Tag> {
return this.databaseStorage.createTag(tag);
}
async incrementTagUse(name: string): Promise<void> {
return this.databaseStorage.incrementTagUse(name);
}
}
// Storage selection logic - use hybrid approach when both Bunny.net and Database are available
let storage: IStorage;
const hasDatabase = process.env.DATABASE_URL;
const hasBunnyConfig = process.env.BUNNY_API_KEY && process.env.BUNNY_LIBRARY_ID && process.env.BUNNY_HOSTNAME;
// Use hybrid storage when both Bunny.net and Database are available
if (hasBunnyConfig && hasDatabase) {
try {
storage = new HybridStorage();
console.log('✅ Using Hybrid storage (Bunny.net for videos + Database for users)');
} catch (error) {
console.error('❌ Failed to initialize Hybrid storage:', error);
console.log('📁 Falling back to memory storage');
storage = new MemStorage();
}
}
// Prioritize Bunny.net storage for video content only
else 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 if (hasDatabase) {
try {
storage = new DatabaseStorage();
console.log('✅ Using PostgreSQL database storage');
} catch (error) {
console.error('❌ Failed to initialize database storage:', error);
console.log('📁 Falling back to memory storage');
storage = new MemStorage();
}
} else {
console.log('📁 Using memory storage (no database or Bunny.net config found)');
storage = new MemStorage();
}
export { storage };