Adjust the video synchronization service to correctly determine if more pages of videos exist by checking the total fetched count against the total available videos and the number of videos returned per page. A small delay is introduced between paginated requests to prevent potential rate limiting issues from the video source. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 2eb1084e-b728-4449-9231-f1665924c8d5 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2eb1084e-b728-4449-9231-f1665924c8d5/h92OTmA
237 lines
7.9 KiB
TypeScript
237 lines
7.9 KiB
TypeScript
import { BunnyService } from './bunny';
|
|
import { db } from './db';
|
|
import { videos } from '@shared/schema';
|
|
import { eq, sql } from 'drizzle-orm';
|
|
|
|
interface VideoSyncCache {
|
|
videos: any[];
|
|
lastUpdate: number;
|
|
isUpdating: boolean;
|
|
}
|
|
|
|
class VideoSyncService {
|
|
private cache: VideoSyncCache = {
|
|
videos: [],
|
|
lastUpdate: 0,
|
|
isUpdating: false
|
|
};
|
|
private bunnyService: BunnyService;
|
|
private syncInterval: NodeJS.Timeout | null = null;
|
|
|
|
constructor() {
|
|
this.bunnyService = new BunnyService();
|
|
}
|
|
|
|
private async getAllVideos(): Promise<any[]> {
|
|
let allVideos: any[] = [];
|
|
let page = 1;
|
|
const itemsPerPage = 100; // Maximum per request
|
|
let hasMore = true;
|
|
|
|
console.log('📥 Starting to fetch all videos with pagination...');
|
|
|
|
while (hasMore) {
|
|
try {
|
|
console.log(`📄 Fetching page ${page}...`);
|
|
const result = await this.bunnyService.getVideos(page, itemsPerPage);
|
|
|
|
allVideos = allVideos.concat(result.videos);
|
|
|
|
// Check if there are more pages
|
|
const totalFetched = allVideos.length;
|
|
hasMore = totalFetched < result.total && result.videos.length > 0;
|
|
|
|
console.log(`📊 Page ${page}: ${result.videos.length} videos (Total so far: ${allVideos.length}/${result.total})`);
|
|
page++;
|
|
|
|
// Add small delay to avoid rate limiting
|
|
if (hasMore) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
// Safety limit to prevent infinite loops
|
|
if (page > 100) {
|
|
console.log('⚠️ Reached page limit (100), stopping fetch');
|
|
break;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Error fetching page ${page}:`, error);
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log(`✅ Completed pagination fetch: ${allVideos.length} total videos`);
|
|
return allVideos;
|
|
}
|
|
|
|
private async syncVideosToDatabase() {
|
|
if (this.cache.videos.length === 0) {
|
|
console.log('⚠️ No videos in cache to sync to database');
|
|
return;
|
|
}
|
|
|
|
console.log('🔄 Syncing cached videos to PostgreSQL database...');
|
|
const startTime = Date.now();
|
|
|
|
let insertedCount = 0;
|
|
let updatedCount = 0;
|
|
|
|
try {
|
|
for (const video of this.cache.videos) {
|
|
try {
|
|
// Check if video exists in database using raw SQL
|
|
const existingVideo = await db.execute(sql`SELECT id FROM videos WHERE id = ${video.id} LIMIT 1`);
|
|
|
|
const videoData = {
|
|
id: video.id,
|
|
title: video.title,
|
|
description: video.description || '',
|
|
thumbnailUrl: video.thumbnailUrl,
|
|
customThumbnailUrl: video.customThumbnailUrl || null,
|
|
videoUrl: video.videoUrl,
|
|
duration: video.duration,
|
|
views: video.views,
|
|
category: video.category || '',
|
|
tags: Array.isArray(video.tags) ? video.tags : [],
|
|
isPublic: video.isPublic !== false,
|
|
createdAt: video.createdAt ? new Date(video.createdAt) : new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
if (existingVideo.rows.length === 0) {
|
|
// Insert new video using raw SQL to avoid schema issues
|
|
await db.execute(sql`
|
|
INSERT INTO videos (id, title, description, thumbnail_url, video_url, duration, views, category, custom_thumbnail_url, tags, is_public, created_at, updated_at)
|
|
VALUES (${videoData.id}, ${videoData.title}, ${videoData.description}, ${videoData.thumbnailUrl}, ${videoData.videoUrl}, ${videoData.duration}, ${videoData.views}, ${videoData.category}, ${videoData.customThumbnailUrl}, ${'{' + videoData.tags.join(',') + '}'}, ${videoData.isPublic}, ${videoData.createdAt.toISOString()}, ${videoData.updatedAt.toISOString()})
|
|
`);
|
|
insertedCount++;
|
|
} else {
|
|
// Update existing video using raw SQL
|
|
await db.execute(sql`
|
|
UPDATE videos
|
|
SET title = ${videoData.title}, description = ${videoData.description}, thumbnail_url = ${videoData.thumbnailUrl},
|
|
video_url = ${videoData.videoUrl}, duration = ${videoData.duration}, views = ${videoData.views},
|
|
category = ${videoData.category}, custom_thumbnail_url = ${videoData.customThumbnailUrl},
|
|
tags = ${'{' + videoData.tags.join(',') + '}'}, is_public = ${videoData.isPublic}, updated_at = ${videoData.updatedAt.toISOString()}
|
|
WHERE id = ${video.id}
|
|
`);
|
|
updatedCount++;
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ Failed to sync video ${video.id}:`, error);
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
console.log(`✅ Database sync completed in ${duration}ms:`);
|
|
console.log(` 📥 Inserted: ${insertedCount} new videos`);
|
|
console.log(` 🔄 Updated: ${updatedCount} existing videos`);
|
|
console.log(` 📊 Database total: ${insertedCount + updatedCount} videos`);
|
|
console.log(` 🎥 Cache total: ${this.cache.videos.length} videos`);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Database sync failed:', error);
|
|
}
|
|
}
|
|
|
|
async initialize() {
|
|
console.log('🔄 Initializing video sync service...');
|
|
try {
|
|
await this.syncVideos();
|
|
await this.syncVideosToDatabase();
|
|
this.startPeriodicSync();
|
|
console.log('✅ Video sync service initialized successfully');
|
|
} catch (error) {
|
|
console.error('❌ Failed to initialize video sync service:', error);
|
|
// Continue without crashing the server
|
|
}
|
|
}
|
|
|
|
private async syncVideos() {
|
|
if (this.cache.isUpdating) {
|
|
console.log('⏳ Video sync already in progress, skipping...');
|
|
return;
|
|
}
|
|
|
|
this.cache.isUpdating = true;
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
console.log('🔍 Fetching ALL videos from Bunny.net...');
|
|
const allVideos = await this.getAllVideos();
|
|
|
|
this.cache.videos = allVideos;
|
|
this.cache.lastUpdate = Date.now();
|
|
|
|
const duration = Date.now() - startTime;
|
|
console.log(`✅ Video sync completed: ${allVideos.length} videos cached in ${duration}ms`);
|
|
} catch (error) {
|
|
console.error('❌ Video sync failed:', error);
|
|
} finally {
|
|
this.cache.isUpdating = false;
|
|
}
|
|
}
|
|
|
|
private startPeriodicSync() {
|
|
// Sync every 60 seconds
|
|
this.syncInterval = setInterval(() => {
|
|
console.log('⏰ Starting scheduled video sync...');
|
|
this.syncVideos();
|
|
}, 60 * 1000);
|
|
|
|
console.log('📅 Scheduled video sync every 60 seconds');
|
|
}
|
|
|
|
getVideos(limit: number = 20, offset: number = 0, search?: string) {
|
|
let filteredVideos = this.cache.videos;
|
|
|
|
// Fast client-side search
|
|
if (search && search.length >= 2) {
|
|
const searchLower = search.toLowerCase();
|
|
filteredVideos = this.cache.videos.filter(video =>
|
|
video.title.toLowerCase().includes(searchLower) ||
|
|
video.description?.toLowerCase().includes(searchLower)
|
|
);
|
|
}
|
|
|
|
const paginatedVideos = filteredVideos.slice(offset, offset + limit);
|
|
|
|
return {
|
|
videos: paginatedVideos,
|
|
total: filteredVideos.length,
|
|
hasMore: offset + limit < filteredVideos.length,
|
|
cacheAge: Date.now() - this.cache.lastUpdate
|
|
};
|
|
}
|
|
|
|
async getVideo(id: string) {
|
|
// First try cache
|
|
const cachedVideo = this.cache.videos.find(v => v.id === id);
|
|
if (cachedVideo) {
|
|
return cachedVideo;
|
|
}
|
|
|
|
// Fallback to direct API call
|
|
return await this.bunnyService.getVideo(id);
|
|
}
|
|
|
|
getCacheStats() {
|
|
return {
|
|
videosCount: this.cache.videos.length,
|
|
lastUpdate: this.cache.lastUpdate,
|
|
isUpdating: this.cache.isUpdating,
|
|
cacheAge: Date.now() - this.cache.lastUpdate
|
|
};
|
|
}
|
|
|
|
stop() {
|
|
if (this.syncInterval) {
|
|
clearInterval(this.syncInterval);
|
|
this.syncInterval = null;
|
|
console.log('🛑 Video sync service stopped');
|
|
}
|
|
}
|
|
}
|
|
|
|
export const videoSyncService = new VideoSyncService(); |