Adds new user, playlist, and favorites management API endpoints and React components. 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/hISDNbZ
793 lines
27 KiB
TypeScript
793 lines
27 KiB
TypeScript
import {
|
|
type Video,
|
|
type InsertVideo,
|
|
type User,
|
|
type InsertUser,
|
|
type Playlist,
|
|
type InsertPlaylist,
|
|
type PlaylistVideo,
|
|
type InsertPlaylistVideo,
|
|
type UserFavorite,
|
|
type InsertUserFavorite
|
|
} from "@shared/schema";
|
|
import { randomUUID } from "crypto";
|
|
import { BunnyService } from "./bunny";
|
|
|
|
export interface IStorage {
|
|
// Video operations
|
|
getVideos(limit?: number, offset?: number, search?: string, category?: string): Promise<Video[]>;
|
|
getVideo(id: string): Promise<Video | undefined>;
|
|
createVideo(video: InsertVideo): Promise<Video>;
|
|
updateVideoViews(id: string): Promise<void>;
|
|
getVideoCount(search?: string, category?: string): Promise<number>;
|
|
|
|
// 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>;
|
|
|
|
// Playlist operations
|
|
getPlaylists(userId: string, limit?: number, offset?: number): Promise<Playlist[]>;
|
|
getPlaylist(id: string, userId?: string): Promise<Playlist | undefined>;
|
|
createPlaylist(playlist: InsertPlaylist): Promise<Playlist>;
|
|
updatePlaylist(id: string, playlist: Partial<InsertPlaylist>, userId: string): Promise<Playlist | undefined>;
|
|
deletePlaylist(id: string, userId: string): Promise<boolean>;
|
|
|
|
// Playlist video operations
|
|
getPlaylistVideos(playlistId: string): Promise<(PlaylistVideo & { video: Video })[]>;
|
|
addVideoToPlaylist(playlistVideo: InsertPlaylistVideo): Promise<PlaylistVideo>;
|
|
removeVideoFromPlaylist(playlistId: string, videoId: string): Promise<boolean>;
|
|
|
|
// Favorites operations
|
|
getUserFavorites(userId: string, limit?: number, offset?: number): Promise<(UserFavorite & { video: Video })[]>;
|
|
addToFavorites(favorite: InsertUserFavorite): Promise<UserFavorite>;
|
|
removeFromFavorites(userId: string, videoId: string): Promise<boolean>;
|
|
isVideoFavorited(userId: string, videoId: string): Promise<boolean>;
|
|
}
|
|
|
|
export class MemStorage implements IStorage {
|
|
private videos: Map<string, Video>;
|
|
private users: Map<string, User>;
|
|
private playlists: Map<string, Playlist>;
|
|
private playlistVideos: Map<string, PlaylistVideo[]>;
|
|
private userFavorites: Map<string, UserFavorite[]>;
|
|
|
|
constructor() {
|
|
this.videos = new Map();
|
|
this.users = new Map();
|
|
this.playlists = new Map();
|
|
this.playlistVideos = new Map();
|
|
this.userFavorites = new Map();
|
|
// Initialize with some sample videos for demonstration
|
|
// In production, these would be fetched from bunny.net API
|
|
this.initializeSampleVideos();
|
|
this.initializeSampleUsers();
|
|
}
|
|
|
|
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,
|
|
views: video.views || 0,
|
|
description: video.description || null,
|
|
category: video.category || null,
|
|
createdAt: new Date()
|
|
};
|
|
this.videos.set(id, fullVideo);
|
|
});
|
|
}
|
|
|
|
async getVideos(limit = 20, offset = 0, search?: string, category?: 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)
|
|
);
|
|
}
|
|
|
|
// Filter by category
|
|
if (category && category !== "All Categories") {
|
|
videos = videos.filter(video => video.category === category);
|
|
}
|
|
|
|
// 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,
|
|
views: video.views || 0,
|
|
description: video.description || null,
|
|
category: video.category || null,
|
|
createdAt: new Date()
|
|
};
|
|
this.videos.set(id, fullVideo);
|
|
return fullVideo;
|
|
}
|
|
|
|
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, category?: string): Promise<number> {
|
|
const videos = await this.getVideos(1000, 0, search, category);
|
|
return videos.length;
|
|
}
|
|
|
|
private initializeSampleUsers() {
|
|
// Sample user for testing
|
|
const sampleUser: User = {
|
|
id: "test-user-123",
|
|
username: "testuser",
|
|
email: "test@example.com",
|
|
passwordHash: "hashed_password",
|
|
firstName: "Test",
|
|
lastName: "User",
|
|
avatar: null,
|
|
isActive: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
this.users.set(sampleUser.id, sampleUser);
|
|
}
|
|
|
|
// User operations
|
|
async getUser(id: string): Promise<User | undefined> {
|
|
return this.users.get(id);
|
|
}
|
|
|
|
async getUserByEmail(email: string): Promise<User | undefined> {
|
|
for (const user of this.users.values()) {
|
|
if (user.email === email) {
|
|
return user;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
async getUserByUsername(username: string): Promise<User | undefined> {
|
|
for (const user of this.users.values()) {
|
|
if (user.username === username) {
|
|
return user;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
async createUser(userData: InsertUser): Promise<User> {
|
|
const user: User = {
|
|
id: randomUUID(),
|
|
...userData,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
this.users.set(user.id, user);
|
|
return user;
|
|
}
|
|
|
|
async updateUser(id: string, userData: Partial<InsertUser>): Promise<User | undefined> {
|
|
const user = this.users.get(id);
|
|
if (!user) return undefined;
|
|
|
|
const updatedUser: User = {
|
|
...user,
|
|
...userData,
|
|
updatedAt: new Date(),
|
|
};
|
|
this.users.set(id, updatedUser);
|
|
return updatedUser;
|
|
}
|
|
|
|
// Playlist operations
|
|
async getPlaylists(userId: string, limit = 20, offset = 0): Promise<Playlist[]> {
|
|
const userPlaylists = Array.from(this.playlists.values())
|
|
.filter(playlist => playlist.userId === userId)
|
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
.slice(offset, offset + limit);
|
|
return userPlaylists;
|
|
}
|
|
|
|
async getPlaylist(id: string, userId?: string): Promise<Playlist | undefined> {
|
|
const playlist = this.playlists.get(id);
|
|
if (!playlist) return undefined;
|
|
|
|
// If userId is provided, check ownership or public status
|
|
if (userId && playlist.userId !== userId && !playlist.isPublic) {
|
|
return undefined;
|
|
}
|
|
|
|
return playlist;
|
|
}
|
|
|
|
async createPlaylist(playlistData: InsertPlaylist): Promise<Playlist> {
|
|
const playlist: Playlist = {
|
|
id: randomUUID(),
|
|
...playlistData,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
this.playlists.set(playlist.id, playlist);
|
|
this.playlistVideos.set(playlist.id, []);
|
|
return playlist;
|
|
}
|
|
|
|
async updatePlaylist(id: string, playlistData: Partial<InsertPlaylist>, userId: string): Promise<Playlist | undefined> {
|
|
const playlist = this.playlists.get(id);
|
|
if (!playlist || playlist.userId !== userId) return undefined;
|
|
|
|
const updatedPlaylist: Playlist = {
|
|
...playlist,
|
|
...playlistData,
|
|
updatedAt: new Date(),
|
|
};
|
|
this.playlists.set(id, updatedPlaylist);
|
|
return updatedPlaylist;
|
|
}
|
|
|
|
async deletePlaylist(id: string, userId: string): Promise<boolean> {
|
|
const playlist = this.playlists.get(id);
|
|
if (!playlist || playlist.userId !== userId) return false;
|
|
|
|
this.playlists.delete(id);
|
|
this.playlistVideos.delete(id);
|
|
return true;
|
|
}
|
|
|
|
// Playlist video operations
|
|
async getPlaylistVideos(playlistId: string): Promise<(PlaylistVideo & { video: Video })[]> {
|
|
const videos = this.playlistVideos.get(playlistId) || [];
|
|
const result = [];
|
|
|
|
for (const playlistVideo of videos) {
|
|
const video = this.videos.get(playlistVideo.videoId);
|
|
if (video) {
|
|
result.push({ ...playlistVideo, video });
|
|
}
|
|
}
|
|
|
|
return result.sort((a, b) => a.position - b.position);
|
|
}
|
|
|
|
async addVideoToPlaylist(playlistVideoData: InsertPlaylistVideo): Promise<PlaylistVideo> {
|
|
const videos = this.playlistVideos.get(playlistVideoData.playlistId) || [];
|
|
|
|
// Check if video is already in playlist
|
|
const existingIndex = videos.findIndex(v => v.videoId === playlistVideoData.videoId);
|
|
if (existingIndex !== -1) {
|
|
throw new Error("Video is already in playlist");
|
|
}
|
|
|
|
const playlistVideo: PlaylistVideo = {
|
|
...playlistVideoData,
|
|
addedAt: new Date(),
|
|
};
|
|
|
|
videos.push(playlistVideo);
|
|
this.playlistVideos.set(playlistVideoData.playlistId, videos);
|
|
return playlistVideo;
|
|
}
|
|
|
|
async removeVideoFromPlaylist(playlistId: string, videoId: string): Promise<boolean> {
|
|
const videos = this.playlistVideos.get(playlistId) || [];
|
|
const filteredVideos = videos.filter(v => v.videoId !== videoId);
|
|
|
|
if (filteredVideos.length === videos.length) {
|
|
return false; // Video not found
|
|
}
|
|
|
|
this.playlistVideos.set(playlistId, filteredVideos);
|
|
return true;
|
|
}
|
|
|
|
// Favorites operations
|
|
async getUserFavorites(userId: string, limit = 20, offset = 0): Promise<(UserFavorite & { video: Video })[]> {
|
|
const favorites = this.userFavorites.get(userId) || [];
|
|
const result = [];
|
|
|
|
for (const favorite of favorites.slice(offset, offset + limit)) {
|
|
const video = this.videos.get(favorite.videoId);
|
|
if (video) {
|
|
result.push({ ...favorite, video });
|
|
}
|
|
}
|
|
|
|
return result.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
}
|
|
|
|
async addToFavorites(favoriteData: InsertUserFavorite): Promise<UserFavorite> {
|
|
const favorites = this.userFavorites.get(favoriteData.userId) || [];
|
|
|
|
// Check if already favorited
|
|
const existing = favorites.find(f => f.videoId === favoriteData.videoId);
|
|
if (existing) {
|
|
throw new Error("Video is already in favorites");
|
|
}
|
|
|
|
const favorite: UserFavorite = {
|
|
...favoriteData,
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
favorites.push(favorite);
|
|
this.userFavorites.set(favoriteData.userId, favorites);
|
|
return favorite;
|
|
}
|
|
|
|
async removeFromFavorites(userId: string, videoId: string): Promise<boolean> {
|
|
const favorites = this.userFavorites.get(userId) || [];
|
|
const filteredFavorites = favorites.filter(f => f.videoId !== videoId);
|
|
|
|
if (filteredFavorites.length === favorites.length) {
|
|
return false; // Favorite not found
|
|
}
|
|
|
|
this.userFavorites.set(userId, filteredFavorites);
|
|
return true;
|
|
}
|
|
|
|
async isVideoFavorited(userId: string, videoId: string): Promise<boolean> {
|
|
const favorites = this.userFavorites.get(userId) || [];
|
|
return favorites.some(f => f.videoId === videoId);
|
|
}
|
|
}
|
|
|
|
// 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, category?: string): Promise<Video[]> {
|
|
try {
|
|
const page = Math.floor(offset / limit) + 1;
|
|
const { videos } = await this.bunnyService.getVideos(page, limit, search);
|
|
|
|
// Filter by category if specified
|
|
let filteredVideos = videos;
|
|
if (category && category !== "All Categories") {
|
|
filteredVideos = videos.filter(video => video.category === category);
|
|
}
|
|
|
|
// Apply cached view counts
|
|
filteredVideos.forEach(video => {
|
|
if (this.viewsCache.has(video.id)) {
|
|
video.views += this.viewsCache.get(video.id)!;
|
|
}
|
|
});
|
|
|
|
return filteredVideos;
|
|
} 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 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, category?: string): Promise<number> {
|
|
try {
|
|
const { total } = await this.bunnyService.getVideos(1, 1, search);
|
|
return total;
|
|
} catch (error) {
|
|
console.error('Error getting video count from Bunny:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// User operations - Not implemented for BunnyStorage (video-only service)
|
|
async getUser(id: string): Promise<User | undefined> {
|
|
throw new Error("User operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async getUserByEmail(email: string): Promise<User | undefined> {
|
|
throw new Error("User operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async getUserByUsername(username: string): Promise<User | undefined> {
|
|
throw new Error("User operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async createUser(user: InsertUser): Promise<User> {
|
|
throw new Error("User operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async updateUser(id: string, user: Partial<InsertUser>): Promise<User | undefined> {
|
|
throw new Error("User operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
// Playlist operations - Not implemented for BunnyStorage (video-only service)
|
|
async getPlaylists(userId: string, limit?: number, offset?: number): Promise<Playlist[]> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async getPlaylist(id: string, userId?: string): Promise<Playlist | undefined> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async createPlaylist(playlist: InsertPlaylist): Promise<Playlist> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async updatePlaylist(id: string, playlist: Partial<InsertPlaylist>, userId: string): Promise<Playlist | undefined> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async deletePlaylist(id: string, userId: string): Promise<boolean> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async getPlaylistVideos(playlistId: string): Promise<(PlaylistVideo & { video: Video })[]> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async addVideoToPlaylist(playlistVideo: InsertPlaylistVideo): Promise<PlaylistVideo> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async removeVideoFromPlaylist(playlistId: string, videoId: string): Promise<boolean> {
|
|
throw new Error("Playlist operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
// Favorites operations - Not implemented for BunnyStorage (video-only service)
|
|
async getUserFavorites(userId: string, limit?: number, offset?: number): Promise<(UserFavorite & { video: Video })[]> {
|
|
throw new Error("Favorites operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async addToFavorites(favorite: InsertUserFavorite): Promise<UserFavorite> {
|
|
throw new Error("Favorites operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async removeFromFavorites(userId: string, videoId: string): Promise<boolean> {
|
|
throw new Error("Favorites operations are not supported with Bunny.net storage");
|
|
}
|
|
|
|
async isVideoFavorited(userId: string, videoId: string): Promise<boolean> {
|
|
throw new Error("Favorites operations are not supported with Bunny.net storage");
|
|
}
|
|
}
|
|
|
|
// Create a hybrid storage that combines Bunny.net videos with database user features
|
|
import { db } from "./db";
|
|
import { eq, and, desc, asc } from "drizzle-orm";
|
|
import { users, playlists, playlistVideos, userFavorites } from "@shared/schema";
|
|
|
|
class HybridStorage implements IStorage {
|
|
private bunnyStorage: BunnyStorage;
|
|
|
|
constructor() {
|
|
this.bunnyStorage = new BunnyStorage();
|
|
}
|
|
|
|
// Video operations - delegate to Bunny.net
|
|
async getVideos(limit = 20, offset = 0, search?: string, category?: string): Promise<Video[]> {
|
|
return this.bunnyStorage.getVideos(limit, offset, search, category);
|
|
}
|
|
|
|
async getVideo(id: string): Promise<Video | undefined> {
|
|
return this.bunnyStorage.getVideo(id);
|
|
}
|
|
|
|
async createVideo(video: InsertVideo): Promise<Video> {
|
|
return this.bunnyStorage.createVideo(video);
|
|
}
|
|
|
|
async updateVideoViews(id: string): Promise<void> {
|
|
return this.bunnyStorage.updateVideoViews(id);
|
|
}
|
|
|
|
async getVideoCount(search?: string, category?: string): Promise<number> {
|
|
return this.bunnyStorage.getVideoCount(search, category);
|
|
}
|
|
|
|
// User operations - use database
|
|
async getUser(id: string): Promise<User | undefined> {
|
|
const [user] = await db.select().from(users).where(eq(users.id, id));
|
|
return user;
|
|
}
|
|
|
|
async getUserByEmail(email: string): Promise<User | undefined> {
|
|
const [user] = await db.select().from(users).where(eq(users.email, email));
|
|
return user;
|
|
}
|
|
|
|
async getUserByUsername(username: string): Promise<User | undefined> {
|
|
const [user] = await db.select().from(users).where(eq(users.username, username));
|
|
return user;
|
|
}
|
|
|
|
async createUser(userData: InsertUser): Promise<User> {
|
|
const [user] = await db.insert(users).values({
|
|
...userData,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
}).returning();
|
|
return user;
|
|
}
|
|
|
|
async updateUser(id: string, userData: Partial<InsertUser>): Promise<User | undefined> {
|
|
const [user] = await db.update(users)
|
|
.set({ ...userData, updatedAt: new Date() })
|
|
.where(eq(users.id, id))
|
|
.returning();
|
|
return user;
|
|
}
|
|
|
|
// Playlist operations - use database
|
|
async getPlaylists(userId: string, limit = 20, offset = 0): Promise<Playlist[]> {
|
|
const userPlaylists = await db.select()
|
|
.from(playlists)
|
|
.where(eq(playlists.userId, userId))
|
|
.orderBy(desc(playlists.createdAt))
|
|
.limit(limit)
|
|
.offset(offset);
|
|
return userPlaylists;
|
|
}
|
|
|
|
async getPlaylist(id: string, userId?: string): Promise<Playlist | undefined> {
|
|
let query = db.select().from(playlists).where(eq(playlists.id, id));
|
|
if (userId) {
|
|
query = query.where(eq(playlists.userId, userId));
|
|
}
|
|
const [playlist] = await query;
|
|
return playlist;
|
|
}
|
|
|
|
async createPlaylist(playlistData: InsertPlaylist): Promise<Playlist> {
|
|
const [playlist] = await db.insert(playlists).values({
|
|
...playlistData,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date()
|
|
}).returning();
|
|
return playlist;
|
|
}
|
|
|
|
async updatePlaylist(id: string, updates: Partial<InsertPlaylist>, userId: string): Promise<Playlist | undefined> {
|
|
const [playlist] = await db.update(playlists)
|
|
.set({ ...updates, updatedAt: new Date() })
|
|
.where(and(eq(playlists.id, id), eq(playlists.userId, userId)))
|
|
.returning();
|
|
return playlist;
|
|
}
|
|
|
|
async deletePlaylist(id: string, userId: string): Promise<boolean> {
|
|
const result = await db.delete(playlists)
|
|
.where(and(eq(playlists.id, id), eq(playlists.userId, userId)));
|
|
return result.rowCount > 0;
|
|
}
|
|
|
|
// Playlist video operations - use database
|
|
async getPlaylistVideos(playlistId: string): Promise<(PlaylistVideo & { video: Video })[]> {
|
|
const playlistVideoRecords = await db.select()
|
|
.from(playlistVideos)
|
|
.where(eq(playlistVideos.playlistId, playlistId))
|
|
.orderBy(asc(playlistVideos.position));
|
|
|
|
const results = [];
|
|
for (const record of playlistVideoRecords) {
|
|
const video = await this.getVideo(record.videoId);
|
|
if (video) {
|
|
results.push({ ...record, video });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
async addVideoToPlaylist(data: InsertPlaylistVideo): Promise<PlaylistVideo> {
|
|
// Check if video already exists in playlist
|
|
const existing = await db.select()
|
|
.from(playlistVideos)
|
|
.where(and(
|
|
eq(playlistVideos.playlistId, data.playlistId),
|
|
eq(playlistVideos.videoId, data.videoId)
|
|
));
|
|
|
|
if (existing.length > 0) {
|
|
throw new Error("Video is already in playlist");
|
|
}
|
|
|
|
const [playlistVideo] = await db.insert(playlistVideos).values({
|
|
...data,
|
|
addedAt: new Date()
|
|
}).returning();
|
|
return playlistVideo;
|
|
}
|
|
|
|
async removeVideoFromPlaylist(playlistId: string, videoId: string): Promise<boolean> {
|
|
const result = await db.delete(playlistVideos)
|
|
.where(and(
|
|
eq(playlistVideos.playlistId, playlistId),
|
|
eq(playlistVideos.videoId, videoId)
|
|
));
|
|
return result.rowCount > 0;
|
|
}
|
|
|
|
// Favorites operations - use database
|
|
async getUserFavorites(userId: string, limit = 20, offset = 0): Promise<(UserFavorite & { video: Video })[]> {
|
|
const favoriteRecords = await db.select()
|
|
.from(userFavorites)
|
|
.where(eq(userFavorites.userId, userId))
|
|
.orderBy(desc(userFavorites.createdAt))
|
|
.limit(limit)
|
|
.offset(offset);
|
|
|
|
const results = [];
|
|
for (const record of favoriteRecords) {
|
|
const video = await this.getVideo(record.videoId);
|
|
if (video) {
|
|
results.push({ ...record, video });
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
async addToFavorites(data: InsertUserFavorite): Promise<UserFavorite> {
|
|
// Check if video already favorited
|
|
const existing = await db.select()
|
|
.from(userFavorites)
|
|
.where(and(
|
|
eq(userFavorites.userId, data.userId),
|
|
eq(userFavorites.videoId, data.videoId)
|
|
));
|
|
|
|
if (existing.length > 0) {
|
|
throw new Error("Video is already in favorites");
|
|
}
|
|
|
|
const [favorite] = await db.insert(userFavorites).values({
|
|
...data,
|
|
createdAt: new Date()
|
|
}).returning();
|
|
return favorite;
|
|
}
|
|
|
|
async removeFromFavorites(userId: string, videoId: string): Promise<boolean> {
|
|
const result = await db.delete(userFavorites)
|
|
.where(and(
|
|
eq(userFavorites.userId, userId),
|
|
eq(userFavorites.videoId, videoId)
|
|
));
|
|
return result.rowCount > 0;
|
|
}
|
|
|
|
async isVideoFavorited(userId: string, videoId: string): Promise<boolean> {
|
|
const [favorite] = await db.select()
|
|
.from(userFavorites)
|
|
.where(and(
|
|
eq(userFavorites.userId, userId),
|
|
eq(userFavorites.videoId, videoId)
|
|
));
|
|
return !!favorite;
|
|
}
|
|
}
|
|
|
|
// 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 HybridStorage();
|
|
console.log('✅ Using Hybrid storage (Bunny.net + Database) with library ID:', process.env.BUNNY_LIBRARY_ID);
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize Hybrid 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 };
|