Enable users to manage video playlists and mark favorite content

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
This commit is contained in:
sebastjanartic
2025-08-04 20:04:23 +00:00
parent 60cf545f79
commit e02d52998a
19 changed files with 1957 additions and 20 deletions
+15
View File
@@ -0,0 +1,15 @@
import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from "ws";
import * as schema from "@shared/schema";
neonConfig.webSocketConstructor = ws;
if (!process.env.DATABASE_URL) {
throw new Error(
"DATABASE_URL must be set. Did you forget to provision a database?",
);
}
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle({ client: pool, schema });
+192
View File
@@ -247,6 +247,198 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// User routes
app.get("/api/users/:id", async (req, res) => {
try {
const user = await storage.getUser(req.params.id);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
// Don't send password hash
const { passwordHash, ...safeUser } = user;
res.json(safeUser);
} catch (error) {
res.status(500).json({ message: "Failed to fetch user" });
}
});
app.post("/api/users", async (req, res) => {
try {
const userData = req.body;
const user = await storage.createUser(userData);
const { passwordHash, ...safeUser } = user;
res.status(201).json(safeUser);
} catch (error) {
res.status(500).json({ message: "Failed to create user" });
}
});
// Playlist routes
app.get("/api/playlists", async (req, res) => {
try {
const userId = req.query.userId as string;
const limit = parseInt(req.query.limit as string) || 20;
const offset = parseInt(req.query.offset as string) || 0;
if (!userId) {
return res.status(400).json({ message: "userId is required" });
}
const playlists = await storage.getPlaylists(userId, limit, offset);
res.json(playlists);
} catch (error) {
res.status(500).json({ message: "Failed to fetch playlists" });
}
});
app.get("/api/playlists/:id", async (req, res) => {
try {
const userId = req.query.userId as string;
const playlist = await storage.getPlaylist(req.params.id, userId);
if (!playlist) {
return res.status(404).json({ message: "Playlist not found" });
}
res.json(playlist);
} catch (error) {
res.status(500).json({ message: "Failed to fetch playlist" });
}
});
app.post("/api/playlists", async (req, res) => {
try {
const playlist = await storage.createPlaylist(req.body);
res.status(201).json(playlist);
} catch (error) {
res.status(500).json({ message: "Failed to create playlist" });
}
});
app.put("/api/playlists/:id", async (req, res) => {
try {
const userId = req.query.userId as string;
if (!userId) {
return res.status(400).json({ message: "userId is required" });
}
const playlist = await storage.updatePlaylist(req.params.id, req.body, userId);
if (!playlist) {
return res.status(404).json({ message: "Playlist not found or access denied" });
}
res.json(playlist);
} catch (error) {
res.status(500).json({ message: "Failed to update playlist" });
}
});
app.delete("/api/playlists/:id", async (req, res) => {
try {
const userId = req.query.userId as string;
if (!userId) {
return res.status(400).json({ message: "userId is required" });
}
const success = await storage.deletePlaylist(req.params.id, userId);
if (!success) {
return res.status(404).json({ message: "Playlist not found or access denied" });
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ message: "Failed to delete playlist" });
}
});
// Playlist videos routes
app.get("/api/playlists/:id/videos", async (req, res) => {
try {
const videos = await storage.getPlaylistVideos(req.params.id);
res.json(videos);
} catch (error) {
res.status(500).json({ message: "Failed to fetch playlist videos" });
}
});
app.post("/api/playlists/:id/videos", async (req, res) => {
try {
const playlistVideo = await storage.addVideoToPlaylist({
playlistId: req.params.id,
videoId: req.body.videoId,
position: req.body.position || 0
});
res.status(201).json(playlistVideo);
} catch (error) {
if (error instanceof Error && error.message.includes("already in playlist")) {
return res.status(409).json({ message: error.message });
}
res.status(500).json({ message: "Failed to add video to playlist" });
}
});
app.delete("/api/playlists/:playlistId/videos/:videoId", async (req, res) => {
try {
const success = await storage.removeVideoFromPlaylist(req.params.playlistId, req.params.videoId);
if (!success) {
return res.status(404).json({ message: "Video not found in playlist" });
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ message: "Failed to remove video from playlist" });
}
});
// Favorites routes
app.get("/api/favorites", async (req, res) => {
try {
const userId = req.query.userId as string;
const limit = parseInt(req.query.limit as string) || 20;
const offset = parseInt(req.query.offset as string) || 0;
if (!userId) {
return res.status(400).json({ message: "userId is required" });
}
const favorites = await storage.getUserFavorites(userId, limit, offset);
res.json(favorites);
} catch (error) {
res.status(500).json({ message: "Failed to fetch favorites" });
}
});
app.post("/api/favorites", async (req, res) => {
try {
const favorite = await storage.addToFavorites({
userId: req.body.userId,
videoId: req.body.videoId
});
res.status(201).json(favorite);
} catch (error) {
if (error instanceof Error && error.message.includes("already in favorites")) {
return res.status(409).json({ message: error.message });
}
res.status(500).json({ message: "Failed to add to favorites" });
}
});
app.delete("/api/favorites/:userId/:videoId", async (req, res) => {
try {
const success = await storage.removeFromFavorites(req.params.userId, req.params.videoId);
if (!success) {
return res.status(404).json({ message: "Favorite not found" });
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ message: "Failed to remove from favorites" });
}
});
app.get("/api/favorites/:userId/:videoId", async (req, res) => {
try {
const isFavorited = await storage.isVideoFavorited(req.params.userId, req.params.videoId);
res.json({ isFavorited });
} catch (error) {
res.status(500).json({ message: "Failed to check favorite status" });
}
});
const httpServer = createServer(app);
return httpServer;
}
+551 -4
View File
@@ -1,23 +1,69 @@
import { type Video, type InsertVideo } from "@shared/schema";
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() {
@@ -145,6 +191,222 @@ export class MemStorage implements IStorage {
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
@@ -220,6 +482,291 @@ class BunnyStorage implements IStorage {
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
@@ -230,10 +777,10 @@ const hasBunnyConfig = process.env.BUNNY_API_KEY && process.env.BUNNY_LIBRARY_ID
if (hasBunnyConfig) {
try {
storage = new BunnyStorage();
console.log('✅ Using Bunny.net storage with library ID:', process.env.BUNNY_LIBRARY_ID);
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 Bunny.net storage:', error);
console.error('❌ Failed to initialize Hybrid storage:', error);
console.log('📁 Falling back to memory storage');
storage = new MemStorage();
}