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";
|
import go4LogoPath from "@assets/go4_1756394900352.png";
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const formatViews = (views: number | undefined): string => {
|
const formatViews = (views: number): string => {
|
||||||
if (!views) return '0';
|
|
||||||
if (views >= 1000000) return `${(views / 1000000).toFixed(1)}M`;
|
if (views >= 1000000) return `${(views / 1000000).toFixed(1)}M`;
|
||||||
if (views >= 1000) return `${(views / 1000).toFixed(1)}K`;
|
if (views >= 1000) return `${(views / 1000).toFixed(1)}K`;
|
||||||
return views.toString();
|
return views.toString();
|
||||||
@ -32,9 +31,7 @@ const formatDate = (date: Date | string): string => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Share2, X, Edit3 } from "lucide-react";
|
||||||
import { Share2, X, Edit3, Search } from "lucide-react";
|
|
||||||
import Hls from "hls.js";
|
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
import {
|
import {
|
||||||
FacebookShareButton,
|
FacebookShareButton,
|
||||||
@ -55,7 +52,6 @@ export default function VideoPage() {
|
|||||||
const [, params] = useRoute("/video/:id");
|
const [, params] = useRoute("/video/:id");
|
||||||
const videoId = params?.id;
|
const videoId = params?.id;
|
||||||
const [showShareMenu, setShowShareMenu] = useState(false);
|
const [showShareMenu, setShowShareMenu] = useState(false);
|
||||||
const [sidebarSearchQuery, setSidebarSearchQuery] = useState("");
|
|
||||||
|
|
||||||
// Fetch current video
|
// Fetch current video
|
||||||
const { data: currentVideo, isLoading: videoLoading } = useQuery<Video>({
|
const { data: currentVideo, isLoading: videoLoading } = useQuery<Video>({
|
||||||
@ -64,26 +60,14 @@ export default function VideoPage() {
|
|||||||
enabled: !!videoId,
|
enabled: !!videoId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch recommended videos using the same search endpoint as main page
|
// Fetch recommended videos (excluding current video)
|
||||||
const { data: recommendedResponse } = useQuery<VideosResponse>({
|
const { data: recommendedResponse } = useQuery<VideosResponse>({
|
||||||
queryKey: ["/api/videos", sidebarSearchQuery],
|
queryKey: ["/api/videos"],
|
||||||
queryFn: () => {
|
queryFn: () => fetch("/api/videos?limit=20&offset=0").then(res => res.json()),
|
||||||
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());
|
|
||||||
},
|
|
||||||
enabled: !!videoId,
|
enabled: !!videoId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simply exclude current video - search filtering is now done server-side
|
const recommendedVideos = recommendedResponse?.videos?.filter(v => v.id !== videoId) || [];
|
||||||
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`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -255,18 +239,23 @@ export default function VideoPage() {
|
|||||||
<div className="flex flex-col lg:flex-row gap-6 relative z-10">
|
<div className="flex flex-col lg:flex-row gap-6 relative z-10">
|
||||||
{/* Main video section */}
|
{/* Main video section */}
|
||||||
<div className="flex-1">
|
<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">
|
<div className="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden mb-4">
|
||||||
<iframe
|
{currentVideo.videoUrlIframe ? (
|
||||||
src={`https://iframe.mediadelivery.net/embed/${process.env.BUNNY_LIBRARY_ID || "384105"}/${currentVideo.id}?preroll=false&postroll=false&ads=false`}
|
<iframe
|
||||||
className="absolute inset-0 w-full h-full"
|
src={currentVideo.videoUrlIframe}
|
||||||
frameBorder="0"
|
className="absolute inset-0 w-full h-full"
|
||||||
allowFullScreen
|
frameBorder="0"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allowFullScreen
|
||||||
onLoad={handleVideoPlay}
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
title={currentVideo.title}
|
onLoad={handleVideoPlay}
|
||||||
referrerPolicy="origin"
|
title={currentVideo.title}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center text-white">
|
||||||
|
<p>Video not available</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Video info */}
|
{/* Video info */}
|
||||||
@ -340,40 +329,10 @@ export default function VideoPage() {
|
|||||||
|
|
||||||
{/* Recommended videos sidebar */}
|
{/* Recommended videos sidebar */}
|
||||||
<div className="lg:w-96">
|
<div className="lg:w-96">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<h2 className="text-lg font-semibold text-bunny-light mb-4">Recommended Videos</h2>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Show search results info */}
|
{recommendedVideos.slice(0, 10).map((video) => (
|
||||||
{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) => (
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
key={video.id}
|
key={video.id}
|
||||||
onClick={() => window.location.href = `/video/${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",
|
"express-session": "^1.18.2",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"google-auth-library": "^10.2.1",
|
"google-auth-library": "^10.2.1",
|
||||||
"hls.js": "^1.6.11",
|
"hls.js": "^1.6.7",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memoizee": "^0.4.17",
|
"memoizee": "^0.4.17",
|
||||||
@ -7441,9 +7441,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hls.js": {
|
"node_modules/hls.js": {
|
||||||
"version": "1.6.11",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.11.tgz",
|
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.7.tgz",
|
||||||
"integrity": "sha512-tdDwOAgPGXohSiNE4oxGr3CI9Hx9lsGLFe6TULUvRk2TfHS+w1tSAJntrvxsHaxvjtr6BXsDZM7NOqJFhU4mmg==",
|
"integrity": "sha512-QW2fnwDGKGc9DwQUGLbmMOz8G48UZK7PVNJPcOUql1b8jubKx4/eMHNP5mGqr6tYlJNDG1g10Lx2U/qPzL6zwQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/html-entities": {
|
"node_modules/html-entities": {
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
"express-session": "^1.18.2",
|
"express-session": "^1.18.2",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"google-auth-library": "^10.2.1",
|
"google-auth-library": "^10.2.1",
|
||||||
"hls.js": "^1.6.11",
|
"hls.js": "^1.6.7",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memoizee": "^0.4.17",
|
"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)
|
## Recent Changes (August 2025)
|
||||||
|
|
||||||
### Latest Updates (January 28, 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
|
- ✅ **Automatic Video Synchronization**: Implemented comprehensive video sync service that checks Bunny.net for new uploads every 60 seconds
|
||||||
- ✅ **Direct Bunny.net Video Embedding**: Implemented iframe-based video playback using https://iframe.mediadelivery.net/embed/384105/{videoId} for seamless video streaming
|
- ✅ **Performance Optimization**: Enhanced search response time from 2.5s to instant with client-side caching and 150ms search debounce
|
||||||
- ✅ **Database-Driven Search**: Replaced unreliable cache-based search with direct PostgreSQL queries for consistent search results across all pages
|
- ✅ **Smart Caching System**: Videos are cached in memory and refreshed automatically, eliminating repeated API calls during browsing
|
||||||
- ✅ **Automatic Synchronization**: Set up periodic sync every 5 minutes to ensure all new Bunny.net videos are automatically added to database
|
- ✅ **English Interface Complete**: All text, messages, and interface elements converted to English language
|
||||||
- ✅ **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
|
|
||||||
|
|
||||||
- ✅ **Complete Backend Infrastructure**: Full PostgreSQL database with user authentication, video upload tracking, categories, and tags management
|
- ✅ **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
|
- ✅ **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 { type Video, type InsertVideo } from "@shared/schema";
|
||||||
import crypto from 'crypto';
|
|
||||||
|
|
||||||
interface BunnyVideo {
|
interface BunnyVideo {
|
||||||
guid: string;
|
guid: string;
|
||||||
@ -154,23 +153,7 @@ export class BunnyService {
|
|||||||
console.log(`Fetching video with description from Bunny: ${guid}`);
|
console.log(`Fetching video with description from Bunny: ${guid}`);
|
||||||
const bunnyVideo: BunnyVideoDetails = await this.makeRequest(`videos/${guid}`);
|
const bunnyVideo: BunnyVideoDetails = await this.makeRequest(`videos/${guid}`);
|
||||||
console.log(`Fetching video: ${bunnyVideo.title} - Description available: ${!!bunnyVideo.description}`);
|
console.log(`Fetching video: ${bunnyVideo.title} - Description available: ${!!bunnyVideo.description}`);
|
||||||
// Use direct iframe URL instead of signed URL to avoid complexity
|
return this.bunnyVideoToVideo(bunnyVideo);
|
||||||
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),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching video ${guid} from Bunny:`, error);
|
console.error(`Error fetching video ${guid} from Bunny:`, error);
|
||||||
return null;
|
return null;
|
||||||
@ -237,19 +220,12 @@ export class BunnyService {
|
|||||||
|
|
||||||
// Generate signed URL for private video access
|
// Generate signed URL for private video access
|
||||||
generateSignedUrl(videoId: string, expirationTime: number = 3600): string {
|
generateSignedUrl(videoId: string, expirationTime: number = 3600): string {
|
||||||
// Use the pull zone hostname for video streaming
|
|
||||||
const baseUrl = `https://${this.hostname}/${videoId}/playlist.m3u8`;
|
const baseUrl = `https://${this.hostname}/${videoId}/playlist.m3u8`;
|
||||||
const expires = Math.floor(Date.now() / 1000) + expirationTime;
|
const expires = Math.floor(Date.now() / 1000) + expirationTime;
|
||||||
|
|
||||||
// Generate security token using library ID and API key
|
// Simple token generation (in production, use proper HMAC signing)
|
||||||
const securityKey = this.apiKey;
|
const token = Buffer.from(`${videoId}:${expires}:${this.apiKey.substring(0, 8)}`).toString('base64');
|
||||||
const hashableBase = securityKey + videoId + expires.toString();
|
|
||||||
|
|
||||||
// Create a simple hash for the token
|
return `${baseUrl}?token=${token}&expires=${expires}`;
|
||||||
const hash = crypto.createHash('md5').update(hashableBase).digest('hex');
|
|
||||||
|
|
||||||
return `${baseUrl}?token=${hash}&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
|
// Initialize video sync service for automatic Bunny.net updates
|
||||||
await videoSyncService.initialize();
|
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);
|
const server = await registerRoutes(app);
|
||||||
|
|
||||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||||
|
|||||||
@ -54,77 +54,29 @@ export interface IStorage {
|
|||||||
export class DatabaseStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
// Video operations
|
// Video operations
|
||||||
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
async getVideos(limit = 20, offset = 0, search?: string): Promise<Video[]> {
|
||||||
try {
|
let query = db.select().from(videos);
|
||||||
let sqlQuery = sql`
|
|
||||||
SELECT * FROM video_metadata
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (search && search.length >= 2) {
|
if (search) {
|
||||||
const searchTerm = `%${search.toLowerCase()}%`;
|
const searchTerm = `%${search}%`;
|
||||||
sqlQuery = sql`
|
query = query.where(
|
||||||
SELECT * FROM video_metadata
|
or(
|
||||||
WHERE LOWER(title) LIKE ${searchTerm}
|
like(videos.title, searchTerm),
|
||||||
OR LOWER(description) LIKE ${searchTerm}
|
like(videos.description, searchTerm)
|
||||||
`;
|
)
|
||||||
}
|
) as any;
|
||||||
|
|
||||||
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 [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await query
|
||||||
|
.orderBy(desc(videos.createdAt))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVideo(id: string): Promise<Video | undefined> {
|
async getVideo(id: string): Promise<Video | undefined> {
|
||||||
try {
|
const result = await db.select().from(videos).where(eq(videos.id, id));
|
||||||
const result = await db.execute(sql`
|
return result[0];
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createVideo(video: InsertVideo): Promise<Video> {
|
async createVideo(video: InsertVideo): Promise<Video> {
|
||||||
@ -144,13 +96,9 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateVideoViews(id: string): Promise<void> {
|
async updateVideoViews(id: string): Promise<void> {
|
||||||
try {
|
await db.update(videos)
|
||||||
await db.execute(sql`
|
.set({ views: sql`${videos.views} + 1` })
|
||||||
UPDATE video_metadata SET views = views + 1 WHERE id = ${id}
|
.where(eq(videos.id, id));
|
||||||
`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Database update views failed:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getVideoCount(search?: string): Promise<number> {
|
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;
|
let storage: IStorage;
|
||||||
|
|
||||||
const hasDatabase = process.env.DATABASE_URL;
|
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 {
|
try {
|
||||||
storage = new DatabaseStorage();
|
storage = new DatabaseStorage();
|
||||||
console.log('✅ Using PostgreSQL database for video metadata storage');
|
console.log('✅ Using PostgreSQL database storage');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to initialize database storage:', error);
|
console.error('❌ Failed to initialize database storage:', error);
|
||||||
console.log('📁 Falling back to memory storage');
|
console.log('📁 Falling back to memory storage');
|
||||||
storage = new MemStorage();
|
storage = new MemStorage();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('📁 Using memory storage (no database found)');
|
console.log('📁 Using memory storage (no database or Bunny.net config found)');
|
||||||
storage = new MemStorage();
|
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";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const videos = pgTable("videos", {
|
export const videos = pgTable("videos", {
|
||||||
id: varchar("id").primaryKey(),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
title: text("title").notNull(),
|
title: text("title").notNull(),
|
||||||
description: text("description").default("").notNull(),
|
description: text("description").default("").notNull(),
|
||||||
thumbnailUrl: text("thumbnail_url").notNull(),
|
thumbnailUrl: text("thumbnail_url").notNull(),
|
||||||
|
customThumbnailUrl: text("custom_thumbnail_url"),
|
||||||
videoUrl: text("video_url").notNull(),
|
videoUrl: text("video_url").notNull(),
|
||||||
|
videoUrlMp4: text("video_url_mp4"),
|
||||||
|
videoUrlIframe: text("video_url_iframe"),
|
||||||
duration: integer("duration").notNull(), // in seconds
|
duration: integer("duration").notNull(), // in seconds
|
||||||
views: integer("views").notNull().default(0),
|
views: integer("views").notNull().default(0),
|
||||||
category: text("category").default("").notNull(),
|
category: text("category").default("").notNull(),
|
||||||
tags: text("tags").array().default([]).notNull(),
|
tags: text("tags").array().default([]).notNull(),
|
||||||
isPublic: boolean("is_public").default(true).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"),
|
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`),
|
createdAt: timestamp("created_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
updatedAt: timestamp("updated_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user