Restored to '44d6f571a5a2b8bce0fd4e8a9eae76204284885b'

Replit-Restored-To: 44d6f571a5
This commit is contained in:
sebastjanartic 2025-08-28 17:43:16 +00:00
parent 8fe4a6f7f6
commit 42883d8409
12 changed files with 85 additions and 469 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

View File

@ -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
View File

@ -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": {

View File

@ -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",

View File

@ -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

View File

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

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

@ -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`),
});