Restored to '44d6f571a5a2b8bce0fd4e8a9eae76204284885b'
Replit-Restored-To: 44d6f571a5
This commit is contained in:
parent
8fe4a6f7f6
commit
42883d8409
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 398 KiB |
@ -5,8 +5,7 @@ import { type Video } from "@shared/schema";
|
||||
|
||||
import go4LogoPath from "@assets/go4_1756394900352.png";
|
||||
// Helper functions
|
||||
const formatViews = (views: number | undefined): string => {
|
||||
if (!views) return '0';
|
||||
const formatViews = (views: number): string => {
|
||||
if (views >= 1000000) return `${(views / 1000000).toFixed(1)}M`;
|
||||
if (views >= 1000) return `${(views / 1000).toFixed(1)}K`;
|
||||
return views.toString();
|
||||
@ -32,9 +31,7 @@ const formatDate = (date: Date | string): string => {
|
||||
});
|
||||
};
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Share2, X, Edit3, Search } from "lucide-react";
|
||||
import Hls from "hls.js";
|
||||
import { Share2, X, Edit3 } from "lucide-react";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import {
|
||||
FacebookShareButton,
|
||||
@ -55,7 +52,6 @@ export default function VideoPage() {
|
||||
const [, params] = useRoute("/video/:id");
|
||||
const videoId = params?.id;
|
||||
const [showShareMenu, setShowShareMenu] = useState(false);
|
||||
const [sidebarSearchQuery, setSidebarSearchQuery] = useState("");
|
||||
|
||||
// Fetch current video
|
||||
const { data: currentVideo, isLoading: videoLoading } = useQuery<Video>({
|
||||
@ -64,26 +60,14 @@ export default function VideoPage() {
|
||||
enabled: !!videoId,
|
||||
});
|
||||
|
||||
// Fetch recommended videos using the same search endpoint as main page
|
||||
// Fetch recommended videos (excluding current video)
|
||||
const { data: recommendedResponse } = useQuery<VideosResponse>({
|
||||
queryKey: ["/api/videos", sidebarSearchQuery],
|
||||
queryFn: () => {
|
||||
const url = sidebarSearchQuery && sidebarSearchQuery.length >= 2
|
||||
? `/api/videos?limit=50&offset=0&search=${encodeURIComponent(sidebarSearchQuery)}`
|
||||
: "/api/videos?limit=50&offset=0";
|
||||
return fetch(url).then(res => res.json());
|
||||
},
|
||||
queryKey: ["/api/videos"],
|
||||
queryFn: () => fetch("/api/videos?limit=20&offset=0").then(res => res.json()),
|
||||
enabled: !!videoId,
|
||||
});
|
||||
|
||||
// Simply exclude current video - search filtering is now done server-side
|
||||
const filteredRecommendedVideos = (recommendedResponse?.videos || [])
|
||||
.filter(v => v.id !== videoId);
|
||||
|
||||
// Debug logging for troubleshooting
|
||||
if (sidebarSearchQuery.length >= 2) {
|
||||
console.log(`Video page search "${sidebarSearchQuery}": Got ${recommendedResponse?.videos?.length || 0} videos from server, showing ${filteredRecommendedVideos.length} after filtering current video`);
|
||||
}
|
||||
const recommendedVideos = recommendedResponse?.videos?.filter(v => v.id !== videoId) || [];
|
||||
|
||||
|
||||
|
||||
@ -255,18 +239,23 @@ export default function VideoPage() {
|
||||
<div className="flex flex-col lg:flex-row gap-6 relative z-10">
|
||||
{/* Main video section */}
|
||||
<div className="flex-1">
|
||||
{/* Bunny.net iframe player */}
|
||||
{/* Video player */}
|
||||
<div className="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden mb-4">
|
||||
<iframe
|
||||
src={`https://iframe.mediadelivery.net/embed/${process.env.BUNNY_LIBRARY_ID || "384105"}/${currentVideo.id}?preroll=false&postroll=false&ads=false`}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
onLoad={handleVideoPlay}
|
||||
title={currentVideo.title}
|
||||
referrerPolicy="origin"
|
||||
/>
|
||||
{currentVideo.videoUrlIframe ? (
|
||||
<iframe
|
||||
src={currentVideo.videoUrlIframe}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
onLoad={handleVideoPlay}
|
||||
title={currentVideo.title}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-white">
|
||||
<p>Video not available</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Video info */}
|
||||
@ -340,40 +329,10 @@ export default function VideoPage() {
|
||||
|
||||
{/* Recommended videos sidebar */}
|
||||
<div className="lg:w-96">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-bunny-light">Recommended Videos</h2>
|
||||
</div>
|
||||
|
||||
{/* Search input for recommended videos */}
|
||||
<div className="relative mb-4">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search videos..."
|
||||
value={sidebarSearchQuery}
|
||||
onChange={(e) => setSidebarSearchQuery(e.target.value)}
|
||||
className="bg-bunny-gray/50 border-bunny-gray/70 text-white placeholder-gray-400 pr-10 focus:border-bunny-blue transition-colors"
|
||||
data-testid="input-sidebar-search"
|
||||
/>
|
||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-bunny-light mb-4">Recommended Videos</h2>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Show search results info */}
|
||||
{sidebarSearchQuery.length >= 2 && (
|
||||
<div className="mb-2 text-xs text-bunny-muted">
|
||||
{filteredRecommendedVideos.length === 0 ? (
|
||||
<div className="text-center py-4">
|
||||
<Search className="w-8 h-8 mx-auto mb-1 opacity-50" />
|
||||
<p>No videos found for "{sidebarSearchQuery}"</p>
|
||||
</div>
|
||||
) : (
|
||||
<p>Found {filteredRecommendedVideos.length} videos for "{sidebarSearchQuery}"</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredRecommendedVideos.slice(0, 10).map((video) => (
|
||||
|
||||
{recommendedVideos.slice(0, 10).map((video) => (
|
||||
<div
|
||||
key={video.id}
|
||||
onClick={() => window.location.href = `/video/${video.id}`}
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -67,7 +67,7 @@
|
||||
"express-session": "^1.18.2",
|
||||
"framer-motion": "^11.13.1",
|
||||
"google-auth-library": "^10.2.1",
|
||||
"hls.js": "^1.6.11",
|
||||
"hls.js": "^1.6.7",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.453.0",
|
||||
"memoizee": "^0.4.17",
|
||||
@ -7441,9 +7441,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.11.tgz",
|
||||
"integrity": "sha512-tdDwOAgPGXohSiNE4oxGr3CI9Hx9lsGLFe6TULUvRk2TfHS+w1tSAJntrvxsHaxvjtr6BXsDZM7NOqJFhU4mmg==",
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.7.tgz",
|
||||
"integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
"express-session": "^1.18.2",
|
||||
"framer-motion": "^11.13.1",
|
||||
"google-auth-library": "^10.2.1",
|
||||
"hls.js": "^1.6.11",
|
||||
"hls.js": "^1.6.7",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.453.0",
|
||||
"memoizee": "^0.4.17",
|
||||
|
||||
10
replit.md
10
replit.md
@ -7,12 +7,10 @@ go4.video is a fully functional professional video streaming platform with a com
|
||||
## Recent Changes (August 2025)
|
||||
|
||||
### Latest Updates (January 28, 2025)
|
||||
- ✅ **PostgreSQL Database Migration**: Successfully migrated all 100 video metadata records from Bunny.net to PostgreSQL for reliable search functionality
|
||||
- ✅ **Direct Bunny.net Video Embedding**: Implemented iframe-based video playback using https://iframe.mediadelivery.net/embed/384105/{videoId} for seamless video streaming
|
||||
- ✅ **Database-Driven Search**: Replaced unreliable cache-based search with direct PostgreSQL queries for consistent search results across all pages
|
||||
- ✅ **Automatic Synchronization**: Set up periodic sync every 5 minutes to ensure all new Bunny.net videos are automatically added to database
|
||||
- ✅ **View Tracking**: Implemented proper video view counting system with PostgreSQL storage
|
||||
- ✅ **Complete System Reliability**: All 100 videos now searchable and playable with consistent metadata across main page and video pages
|
||||
- ✅ **Automatic Video Synchronization**: Implemented comprehensive video sync service that checks Bunny.net for new uploads every 60 seconds
|
||||
- ✅ **Performance Optimization**: Enhanced search response time from 2.5s to instant with client-side caching and 150ms search debounce
|
||||
- ✅ **Smart Caching System**: Videos are cached in memory and refreshed automatically, eliminating repeated API calls during browsing
|
||||
- ✅ **English Interface Complete**: All text, messages, and interface elements converted to English language
|
||||
|
||||
- ✅ **Complete Backend Infrastructure**: Full PostgreSQL database with user authentication, video upload tracking, categories, and tags management
|
||||
- ✅ **Video Upload System**: Comprehensive video upload functionality with progress tracking, metadata editing, and file management
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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();
|
||||
@ -4,18 +4,26 @@ import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export const videos = pgTable("videos", {
|
||||
id: varchar("id").primaryKey(),
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
title: text("title").notNull(),
|
||||
description: text("description").default("").notNull(),
|
||||
thumbnailUrl: text("thumbnail_url").notNull(),
|
||||
customThumbnailUrl: text("custom_thumbnail_url"),
|
||||
videoUrl: text("video_url").notNull(),
|
||||
videoUrlMp4: text("video_url_mp4"),
|
||||
videoUrlIframe: text("video_url_iframe"),
|
||||
duration: integer("duration").notNull(), // in seconds
|
||||
views: integer("views").notNull().default(0),
|
||||
category: text("category").default("").notNull(),
|
||||
tags: text("tags").array().default([]).notNull(),
|
||||
isPublic: boolean("is_public").default(true).notNull(),
|
||||
uploadStatus: text("upload_status").default("completed").notNull(), // pending, processing, completed, failed
|
||||
uploadStatus: text("upload_status").default("pending").notNull(), // pending, processing, completed, failed
|
||||
originalFileName: text("original_file_name"),
|
||||
fileSize: integer("file_size"), // in bytes
|
||||
bitrate: integer("bitrate"), // in kbps
|
||||
resolution: text("resolution"), // e.g., "1920x1080"
|
||||
format: text("format"), // e.g., "mp4", "avi", "mov"
|
||||
encoding: text("encoding"), // e.g., "h264", "h265"
|
||||
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user