Restored to '44d6f571a5a2b8bce0fd4e8a9eae76204284885b'
Replit-Restored-To: 44d6f571a5
This commit is contained in:
+5
-29
@@ -1,5 +1,4 @@
|
||||
import { type Video, type InsertVideo } from "@shared/schema";
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface BunnyVideo {
|
||||
guid: string;
|
||||
@@ -154,23 +153,7 @@ export class BunnyService {
|
||||
console.log(`Fetching video with description from Bunny: ${guid}`);
|
||||
const bunnyVideo: BunnyVideoDetails = await this.makeRequest(`videos/${guid}`);
|
||||
console.log(`Fetching video: ${bunnyVideo.title} - Description available: ${!!bunnyVideo.description}`);
|
||||
// Use direct iframe URL instead of signed URL to avoid complexity
|
||||
return {
|
||||
id: bunnyVideo.guid,
|
||||
title: bunnyVideo.title,
|
||||
description: bunnyVideo.description || "",
|
||||
thumbnailUrl: this.getThumbnailUrl(bunnyVideo.guid, bunnyVideo.thumbnailFileName),
|
||||
videoUrl: `https://iframe.mediadelivery.net/embed/384105/${bunnyVideo.guid}`,
|
||||
duration: bunnyVideo.length,
|
||||
views: bunnyVideo.views,
|
||||
category: bunnyVideo.category || "",
|
||||
tags: bunnyVideo.metaTags?.map(tag => tag.value) || [],
|
||||
isPublic: bunnyVideo.status === 4,
|
||||
uploadStatus: bunnyVideo.status === 4 ? "completed" : "processing",
|
||||
originalFileName: bunnyVideo.title,
|
||||
createdAt: new Date(bunnyVideo.dateUploaded),
|
||||
updatedAt: new Date(bunnyVideo.dateUploaded),
|
||||
};
|
||||
return this.bunnyVideoToVideo(bunnyVideo);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching video ${guid} from Bunny:`, error);
|
||||
return null;
|
||||
@@ -237,19 +220,12 @@ export class BunnyService {
|
||||
|
||||
// Generate signed URL for private video access
|
||||
generateSignedUrl(videoId: string, expirationTime: number = 3600): string {
|
||||
// Use the pull zone hostname for video streaming
|
||||
const baseUrl = `https://${this.hostname}/${videoId}/playlist.m3u8`;
|
||||
const expires = Math.floor(Date.now() / 1000) + expirationTime;
|
||||
|
||||
// Generate security token using library ID and API key
|
||||
const securityKey = this.apiKey;
|
||||
const hashableBase = securityKey + videoId + expires.toString();
|
||||
// Simple token generation (in production, use proper HMAC signing)
|
||||
const token = Buffer.from(`${videoId}:${expires}:${this.apiKey.substring(0, 8)}`).toString('base64');
|
||||
|
||||
// Create a simple hash for the token
|
||||
const hash = crypto.createHash('md5').update(hashableBase).digest('hex');
|
||||
|
||||
return `${baseUrl}?token=${hash}&expires=${expires}`;
|
||||
return `${baseUrl}?token=${token}&expires=${expires}`;
|
||||
}
|
||||
}
|
||||
|
||||
export const bunnyService = new BunnyService();
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { db } from "./db";
|
||||
import { videos } from "@shared/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { bunnyService } from "./bunny";
|
||||
|
||||
export class BunnyMigrationService {
|
||||
private migrationInProgress = false;
|
||||
|
||||
async migrateAllVideosToDatabase(): Promise<void> {
|
||||
if (this.migrationInProgress) {
|
||||
console.log("🔄 Migration already in progress, skipping...");
|
||||
return;
|
||||
}
|
||||
|
||||
this.migrationInProgress = true;
|
||||
console.log("🔄 Starting migration of all Bunny.net videos to database...");
|
||||
|
||||
try {
|
||||
// Fetch all videos from Bunny.net
|
||||
const bunnyVideos = await bunnyService.getAllVideos();
|
||||
console.log(`📥 Found ${bunnyVideos.length} videos in Bunny.net`);
|
||||
|
||||
let insertedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const bunnyVideo of bunnyVideos) {
|
||||
try {
|
||||
// Check if video already exists in database
|
||||
const existingVideo = await db
|
||||
.select()
|
||||
.from(videos)
|
||||
.where(eq(videos.id, bunnyVideo.id))
|
||||
.limit(1);
|
||||
|
||||
const videoData = {
|
||||
id: bunnyVideo.id,
|
||||
title: bunnyVideo.title,
|
||||
description: bunnyVideo.description || "",
|
||||
thumbnailUrl: bunnyVideo.thumbnailUrl,
|
||||
videoUrl: bunnyVideo.videoUrl,
|
||||
videoUrlMp4: bunnyVideo.videoUrlMp4,
|
||||
videoUrlIframe: bunnyVideo.videoUrlIframe,
|
||||
duration: bunnyVideo.duration,
|
||||
views: bunnyVideo.views,
|
||||
category: bunnyVideo.category || "",
|
||||
tags: bunnyVideo.tags || [],
|
||||
isPublic: true,
|
||||
uploadStatus: "completed",
|
||||
originalFileName: bunnyVideo.title,
|
||||
bunnyId: bunnyVideo.id,
|
||||
bunnyLibraryId: process.env.BUNNY_LIBRARY_ID || "476412",
|
||||
source: "bunny",
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (existingVideo.length === 0) {
|
||||
// Insert new video
|
||||
await db.insert(videos).values({
|
||||
...videoData,
|
||||
createdAt: bunnyVideo.createdAt ? new Date(bunnyVideo.createdAt) : new Date(),
|
||||
});
|
||||
insertedCount++;
|
||||
} else {
|
||||
// Update existing video
|
||||
await db
|
||||
.update(videos)
|
||||
.set(videoData)
|
||||
.where(eq(videos.id, bunnyVideo.id));
|
||||
updatedCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error processing video ${bunnyVideo.title}:`, error);
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✅ Migration completed:`);
|
||||
console.log(` 📥 Inserted: ${insertedCount} videos`);
|
||||
console.log(` 🔄 Updated: ${updatedCount} videos`);
|
||||
console.log(` ⚠️ Skipped: ${skippedCount} videos`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Migration failed:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.migrationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async syncVideoFromBunny(bunnyVideoId: string): Promise<void> {
|
||||
try {
|
||||
const bunnyVideo = await bunnyService.getVideo(bunnyVideoId);
|
||||
if (!bunnyVideo) {
|
||||
console.warn(`⚠️ Video ${bunnyVideoId} not found in Bunny.net`);
|
||||
return;
|
||||
}
|
||||
|
||||
const videoData = {
|
||||
id: bunnyVideo.id,
|
||||
title: bunnyVideo.title,
|
||||
description: bunnyVideo.description || "",
|
||||
thumbnailUrl: bunnyVideo.thumbnailUrl,
|
||||
videoUrl: bunnyVideo.videoUrl,
|
||||
videoUrlMp4: bunnyVideo.videoUrlMp4,
|
||||
videoUrlIframe: bunnyVideo.videoUrlIframe,
|
||||
duration: bunnyVideo.duration,
|
||||
views: bunnyVideo.views,
|
||||
category: bunnyVideo.category || "",
|
||||
tags: bunnyVideo.tags || [],
|
||||
isPublic: true,
|
||||
uploadStatus: "completed",
|
||||
originalFileName: bunnyVideo.title,
|
||||
bunnyId: bunnyVideo.id,
|
||||
bunnyLibraryId: process.env.BUNNY_LIBRARY_ID || "476412",
|
||||
source: "bunny",
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
// Upsert the video
|
||||
await db
|
||||
.insert(videos)
|
||||
.values({
|
||||
...videoData,
|
||||
createdAt: bunnyVideo.createdAt ? new Date(bunnyVideo.createdAt) : new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: videos.id,
|
||||
set: videoData,
|
||||
});
|
||||
|
||||
console.log(`✅ Synced video: ${bunnyVideo.title}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error syncing video ${bunnyVideoId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
async getVideoCount(): Promise<{ database: number; bunny: number }> {
|
||||
try {
|
||||
const [dbCount, bunnyVideos] = await Promise.all([
|
||||
db.select().from(videos).then(result => result.length),
|
||||
bunnyService.getAllVideos()
|
||||
]);
|
||||
|
||||
return {
|
||||
database: dbCount,
|
||||
bunny: bunnyVideos.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Error getting video count:", error);
|
||||
return { database: 0, bunny: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const bunnyMigration = new BunnyMigrationService();
|
||||
@@ -41,13 +41,6 @@ app.use((req, res, next) => {
|
||||
// Initialize video sync service for automatic Bunny.net updates
|
||||
await videoSyncService.initialize();
|
||||
|
||||
// Initialize video metadata migrator with periodic sync
|
||||
const { videoMigrator } = await import('./videoMigrator');
|
||||
console.log("🔄 Initializing video metadata system...");
|
||||
videoMigrator.initialize().catch(error => {
|
||||
console.error("❌ Video metadata initialization failed:", error);
|
||||
});
|
||||
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
|
||||
+37
-78
@@ -54,77 +54,29 @@ export interface IStorage {
|
||||
export class DatabaseStorage implements IStorage {
|
||||
// Video operations
|
||||
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
||||
try {
|
||||
let sqlQuery = sql`
|
||||
SELECT * FROM video_metadata
|
||||
`;
|
||||
|
||||
if (search && search.length >= 2) {
|
||||
const searchTerm = `%${search.toLowerCase()}%`;
|
||||
sqlQuery = sql`
|
||||
SELECT * FROM video_metadata
|
||||
WHERE LOWER(title) LIKE ${searchTerm}
|
||||
OR LOWER(description) LIKE ${searchTerm}
|
||||
`;
|
||||
}
|
||||
|
||||
sqlQuery = sql`${sqlQuery} ORDER BY created_at DESC LIMIT ${limit} OFFSET ${offset}`;
|
||||
|
||||
const result = await db.execute(sqlQuery);
|
||||
console.log(`📊 DatabaseStorage: Found ${result.rows.length} videos (search: "${search || 'none'}")`);
|
||||
|
||||
// Transform database rows to Video objects with original Bunny iframe URLs
|
||||
return result.rows.map((row: any) => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
thumbnailUrl: row.thumbnail_url,
|
||||
videoUrl: `https://iframe.mediadelivery.net/embed/384105/${row.id}`, // Original Bunny iframe
|
||||
duration: row.duration,
|
||||
views: row.views,
|
||||
category: row.category,
|
||||
tags: [],
|
||||
isPublic: true,
|
||||
uploadStatus: "completed",
|
||||
originalFileName: row.title,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error("❌ Database query failed:", error);
|
||||
return [];
|
||||
let query = db.select().from(videos);
|
||||
|
||||
if (search) {
|
||||
const searchTerm = `%${search}%`;
|
||||
query = query.where(
|
||||
or(
|
||||
like(videos.title, searchTerm),
|
||||
like(videos.description, searchTerm)
|
||||
)
|
||||
) as any;
|
||||
}
|
||||
|
||||
const result = await query
|
||||
.orderBy(desc(videos.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getVideo(id: string): Promise<Video | undefined> {
|
||||
try {
|
||||
const result = await db.execute(sql`
|
||||
SELECT * FROM video_metadata WHERE id = ${id} LIMIT 1
|
||||
`);
|
||||
|
||||
if (result.rows.length === 0) return undefined;
|
||||
|
||||
const row = result.rows[0] as any;
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
thumbnailUrl: row.thumbnail_url,
|
||||
videoUrl: `https://iframe.mediadelivery.net/embed/384105/${row.id}`, // Original Bunny iframe
|
||||
duration: row.duration,
|
||||
views: row.views,
|
||||
category: row.category,
|
||||
tags: [],
|
||||
isPublic: true,
|
||||
uploadStatus: "completed",
|
||||
originalFileName: row.title,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Database get video failed:", error);
|
||||
return undefined;
|
||||
}
|
||||
const result = await db.select().from(videos).where(eq(videos.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async createVideo(video: InsertVideo): Promise<Video> {
|
||||
@@ -144,13 +96,9 @@ export class DatabaseStorage implements IStorage {
|
||||
}
|
||||
|
||||
async updateVideoViews(id: string): Promise<void> {
|
||||
try {
|
||||
await db.execute(sql`
|
||||
UPDATE video_metadata SET views = views + 1 WHERE id = ${id}
|
||||
`);
|
||||
} catch (error) {
|
||||
console.error("❌ Database update views failed:", error);
|
||||
}
|
||||
await db.update(videos)
|
||||
.set({ views: sql`${videos.views} + 1` })
|
||||
.where(eq(videos.id, id));
|
||||
}
|
||||
|
||||
async getVideoCount(search?: string): Promise<number> {
|
||||
@@ -853,22 +801,33 @@ class BunnyStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// Storage selection logic - use PostgreSQL for video metadata
|
||||
// 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;
|
||||
|
||||
if (hasDatabase) {
|
||||
// Prioritize Bunny.net storage for user's video content
|
||||
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 for video metadata storage');
|
||||
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 found)');
|
||||
console.log('📁 Using memory storage (no database or Bunny.net config found)');
|
||||
storage = new MemStorage();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { db } from "./db";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { videoSyncService } from "./videoSync";
|
||||
|
||||
export class VideoMigrator {
|
||||
private isRunning = false;
|
||||
private syncInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
console.log("🔄 Initializing video metadata migrator...");
|
||||
|
||||
// Run initial migration
|
||||
await this.migrateAllVideoMetadata();
|
||||
|
||||
// Set up periodic sync every 5 minutes to ensure database is always complete
|
||||
this.syncInterval = setInterval(async () => {
|
||||
console.log("⏰ Starting periodic video metadata sync...");
|
||||
try {
|
||||
await this.migrateAllVideoMetadata();
|
||||
const count = await this.getVideoCount();
|
||||
console.log(`✅ Periodic sync completed - Database: ${count.database}, Bunny: ${count.bunny}`);
|
||||
} catch (error) {
|
||||
console.error("❌ Periodic sync failed:", error);
|
||||
}
|
||||
}, 5 * 60 * 1000); // Every 5 minutes
|
||||
|
||||
console.log("✅ Video metadata migrator initialized with periodic sync");
|
||||
}
|
||||
|
||||
async migrateAllVideoMetadata(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
console.log("🔄 Migration already running...");
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
console.log("🔄 Migrating all video metadata from Bunny.net to PostgreSQL...");
|
||||
|
||||
try {
|
||||
// Get all videos from cache
|
||||
const videoList = videoSyncService.getVideos(1000, 0).videos;
|
||||
console.log(`📥 Found ${videoList.length} videos in cache`);
|
||||
|
||||
let inserted = 0;
|
||||
let updated = 0;
|
||||
|
||||
for (const video of videoList) {
|
||||
try {
|
||||
// Upsert video metadata with simple SQL
|
||||
await db.execute(sql`
|
||||
INSERT INTO video_metadata (
|
||||
id, title, description, thumbnail_url, video_url,
|
||||
duration, views, category, created_at, updated_at
|
||||
) VALUES (
|
||||
${video.id}, ${video.title}, ${video.description || ""},
|
||||
${video.thumbnailUrl}, ${video.videoUrl},
|
||||
${video.duration}, ${video.views}, ${video.category || ""},
|
||||
${video.createdAt ? new Date(video.createdAt) : new Date()},
|
||||
${new Date()}
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
title = EXCLUDED.title,
|
||||
description = EXCLUDED.description,
|
||||
thumbnail_url = EXCLUDED.thumbnail_url,
|
||||
video_url = EXCLUDED.video_url,
|
||||
duration = EXCLUDED.duration,
|
||||
views = EXCLUDED.views,
|
||||
category = EXCLUDED.category,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`);
|
||||
|
||||
inserted++;
|
||||
|
||||
// Log progress every 10 videos
|
||||
if ((inserted + updated) % 10 === 0) {
|
||||
console.log(`📊 Progress: ${inserted + updated}/${videoList.length} videos processed`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error processing ${video.title}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const dbCount = await this.getVideoCount();
|
||||
|
||||
console.log(`✅ Migration completed:`);
|
||||
console.log(` 📥 Inserted: ${inserted} new videos`);
|
||||
console.log(` 🔄 Updated: ${updated} existing videos`);
|
||||
console.log(` 📊 Database total: ${dbCount.database} videos`);
|
||||
console.log(` 🎥 Bunny.net total: ${dbCount.bunny} videos`);
|
||||
|
||||
// Verify all Bunny videos are in database
|
||||
if (dbCount.database < dbCount.bunny) {
|
||||
console.warn(`⚠️ Warning: Database has ${dbCount.database} videos but Bunny.net has ${dbCount.bunny}. Some videos may be missing!`);
|
||||
} else {
|
||||
console.log("✅ All Bunny.net videos are synchronized with database");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Migration failed:", error);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
async getVideoCount(): Promise<{ database: number; bunny: number }> {
|
||||
try {
|
||||
const dbResult = await db.execute(sql`SELECT COUNT(*) FROM video_metadata`);
|
||||
const cachedVideos = videoSyncService.getVideos(1000, 0).videos;
|
||||
|
||||
return {
|
||||
database: Number(dbResult.rows[0]?.count || 0),
|
||||
bunny: cachedVideos.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("❌ Error counting videos:", error);
|
||||
return { database: 0, bunny: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const videoMigrator = new VideoMigrator();
|
||||
Reference in New Issue
Block a user