videofolxtv/server/videoSync.ts
sebastjanartic b18e25e4c9 Improve video synchronization by adjusting fetching logic and adding delays
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
2025-08-28 20:52:15 +00:00

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();