Implement user registration, login, and logout endpoints with session management. Integrate Multer for video file uploads and extend the schema for user, video upload, category, and tag management. Replit-Commit-Author: Agent Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/WCZ9oGO
806 lines
27 KiB
TypeScript
806 lines
27 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 { 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>;
|
|
createUser(user: InsertUser): Promise<User>;
|
|
updateUser(id: string, user: Partial<InsertUser>): Promise<User | undefined>;
|
|
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(
|
|
or(
|
|
like(videos.title, searchTerm),
|
|
like(videos.description, searchTerm)
|
|
)
|
|
);
|
|
}
|
|
|
|
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(
|
|
or(
|
|
like(videos.title, searchTerm),
|
|
like(videos.description, searchTerm)
|
|
)
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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.password, 12);
|
|
|
|
const result = await db.insert(users).values({
|
|
...user,
|
|
password: hashedPassword,
|
|
updatedAt: new Date()
|
|
}).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.password) {
|
|
updateData.password = await bcrypt.hash(updates.password, 12);
|
|
}
|
|
|
|
const result = await db.update(users)
|
|
.set(updateData)
|
|
.where(eq(users.id, id))
|
|
.returning();
|
|
return result[0];
|
|
}
|
|
|
|
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.password);
|
|
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;
|
|
}
|
|
|
|
// 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 || "",
|
|
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;
|
|
}
|
|
|
|
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.password, 12);
|
|
|
|
const fullUser: User = {
|
|
...user,
|
|
id,
|
|
password: hashedPassword,
|
|
isAdmin: user.isAdmin ?? 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.password) {
|
|
updateData.password = await bcrypt.hash(updates.password, 12);
|
|
}
|
|
|
|
const updatedUser: User = {
|
|
...user,
|
|
...updateData
|
|
};
|
|
this.users.set(id, updatedUser);
|
|
return updatedUser;
|
|
}
|
|
|
|
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.password);
|
|
return isValid ? user : null;
|
|
}
|
|
|
|
// Upload operations
|
|
async createVideoUpload(upload: InsertVideoUpload): Promise<VideoUpload> {
|
|
const id = randomUUID();
|
|
const fullUpload: VideoUpload = {
|
|
...upload,
|
|
id,
|
|
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,
|
|
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 viewsCache: Map<string, number> = new Map();
|
|
|
|
constructor() {
|
|
this.bunnyService = new BunnyService();
|
|
}
|
|
|
|
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
|
try {
|
|
console.log(`Fetching videos: limit=${limit}, offset=${offset}, search=${search}`);
|
|
|
|
// For simple pagination, get a larger batch and slice on our side
|
|
// This is more reliable than complex page calculations
|
|
const batchSize = 100; // Get more videos to handle pagination properly
|
|
const page = Math.floor(offset / batchSize) + 1;
|
|
|
|
const { videos } = await this.bunnyService.getVideos(page, batchSize);
|
|
console.log(`Bunny API returned ${videos.length} videos`);
|
|
|
|
// 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))
|
|
);
|
|
console.log(`After search filtering: ${filteredVideos.length} videos`);
|
|
}
|
|
|
|
// Apply cached view counts
|
|
filteredVideos.forEach(video => {
|
|
if (this.viewsCache.has(video.id)) {
|
|
video.views += this.viewsCache.get(video.id)!;
|
|
}
|
|
});
|
|
|
|
// Simple offset/limit slicing
|
|
const startIndex = offset % batchSize;
|
|
const endIndex = startIndex + limit;
|
|
const result = filteredVideos.slice(startIndex, endIndex);
|
|
|
|
console.log(`Returning ${result.length} videos (slice ${startIndex}-${endIndex} from ${filteredVideos.length})`);
|
|
|
|
return result;
|
|
} 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> {
|
|
throw new Error("Creating videos is not supported with Bunny.net integration");
|
|
}
|
|
|
|
async updateVideo(id: string, updates: UpdateVideo): Promise<Video | undefined> {
|
|
throw new Error("Updating videos is not supported with Bunny.net integration");
|
|
}
|
|
|
|
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 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> {
|
|
// 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 {
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Storage selection logic - choose DatabaseStorage if PostgreSQL is 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;
|
|
|
|
// For now, use memory storage to ensure the backend works properly
|
|
// Database implementation is ready but needs proper database setup
|
|
console.log('📁 Using memory storage for reliable backend demonstration');
|
|
storage = new MemStorage();
|
|
|
|
export { storage };
|