require("dotenv").config(); const express = require("express"); const mongoose = require("mongoose"); const fs = require("fs").promises; const fsSync = require("fs"); const path = require("path"); const { exec } = require("child_process"); const cors = require('cors'); const bcrypt = require("bcryptjs"); const jwt = require("jsonwebtoken"); const nodemailer = require("nodemailer"); const Razorpay = require("razorpay"); const crypto = require("crypto"); const Wishlist = require("./models/Wishlist"); const Rental = require("./models/Rental"); const Payment = require("./models/Payment"); const app = express(); // Middleware setup app.use(cors({ origin: '*', credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); app.use(express.json()); const port = process.env.PORT || 5000; const authenticateJWT = (req, res, next) => { const authHeader = req.header("Authorization"); logger.info(`Auth header received: ${authHeader}`); const token = authHeader?.split(" ")[1]; if (!token) { logger.error('No token provided'); return res.status(403).json({ error: "No token provided" }); } jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { logger.error(`Token verification failed: ${err.message}`); return res.status(403).json({ error: "Token verification failed", details: err.message }); } logger.info(`Token verified for user: ${user.id}`); req.user = user; next(); }); }; // ... existing code ... // const User = require("./models/User"); // Ensure this is included // const bcrypt = require("bcryptjs"); // const jwt = require("jsonwebtoken"); // User Registration app.post("/auth/register", async (req, res) => { const { username, password, email } = req.body; const hashedPassword = await bcrypt.hash(password, 10); const newUser = new User({ username, password: hashedPassword, email }); await newUser.save(); res.status(201).json({ message: "User registered successfully" }); }); // User Login app.post("/auth/login", async (req, res) => { const { email, password } = req.body; const user = await User.findOne({ email }); if (!user || !(await bcrypt.compare(password, user.password))) { return res.status(401).json({ error: "Invalid credentials" }); } const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "24h" }); res.json({ token }); }); // Get Current User Details app.get("/auth/me", authenticateJWT, async (req, res) => { const user = await User.findById(req.user.id).select("-password"); res.json(user); }); // ... existing code ... // MongoDB Models const movieSchema = new mongoose.Schema({ title: { type: String, required: true }, category: { type: String, required: true }, description: { type: String }, type: { type: String, required: true }, rentalPrice: { type: Number }, status: { type: String, enum: ['pending', 'processing', 'completed', 'failed'], default: 'pending' }, processingProgress: { type: Number, default: 0 }, hlsUrl: String, thumbnailUrl: String, trailerUrl: String, errorDetails: String, createdAt: { type: Date, default: Date.now } }); const Movie = mongoose.model('Movie', movieSchema); const userSchema = new mongoose.Schema({ username: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, resetPasswordOtp: String, resetPasswordExpires: Date }); const User = mongoose.model('User', userSchema); // Enhanced Logger const logger = { info: (message, data = '') => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ℹ️ ${message}`, data); }, success: (message, data = '') => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ✅ ${message}`, data); }, error: (message, error = '') => { const timestamp = new Date().toISOString(); console.error(`[${timestamp}] ❌ ${message}`, error); }, warn: (message, data = '') => { const timestamp = new Date().toISOString(); console.warn(`[${timestamp}] ⚠️ ${message}`, data); } }; // MongoDB connection with enhanced monitoring mongoose .connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => logger.success("Connected to MongoDB")) .catch((err) => logger.error("MongoDB connection error:", err)); // MongoDB connection monitoring setInterval(async () => { try { await mongoose.connection.db.admin().ping(); logger.info('MongoDB connection alive'); } catch (error) { logger.error('MongoDB connection lost:', error); } }, 30000); // Define upload directories const UPLOAD_DIR = '/home/theapix/public_html/uploads'; const WATCH_DIRS = { pending: path.join(UPLOAD_DIR, 'pending'), processing: path.join(UPLOAD_DIR, 'processing'), completed: path.join(UPLOAD_DIR, 'completed'), failed: path.join(UPLOAD_DIR, 'failed') }; // Directory setup and permission checks Object.values(WATCH_DIRS).forEach(dir => { try { if (!fsSync.existsSync(dir)) { fsSync.mkdirSync(dir, { recursive: true }); fsSync.chmodSync(dir, '0755'); logger.success(`Created directory with permissions: ${dir}`); } // Test write permissions const testFile = path.join(dir, '.test'); fsSync.writeFileSync(testFile, 'test'); fsSync.unlinkSync(testFile); logger.success(`Directory ${dir} is writable`); } catch (error) { logger.error(`Permission error for ${dir}:`, error); } }); // Validate metadata.json and structure async function validateUploadStructure(uploadDir) { try { logger.info(`Validating upload structure for: ${uploadDir}`); const files = await fs.readdir(uploadDir); logger.info(`Found files:`, files); if (!files.includes('metadata.json')) { throw new Error('metadata.json is required'); } const metadataPath = path.join(uploadDir, 'metadata.json'); const metadataContent = await fs.readFile(metadataPath, 'utf8'); logger.info(`Reading metadata from: ${metadataPath}`); let metadata; try { metadata = JSON.parse(metadataContent); logger.info(`Parsed metadata:`, metadata); } catch (error) { throw new Error(`Invalid metadata JSON: ${error.message}`); } const requiredFields = ['title', 'category', 'type', 'description']; const missingFields = requiredFields.filter(field => !metadata[field]); if (missingFields.length > 0) { throw new Error(`Missing required fields: ${missingFields.join(', ')}`); } const requiredFiles = ['video.mp4', 'thumbnail.jpg']; for (const file of requiredFiles) { const filePath = path.join(uploadDir, file); if (!files.includes(file)) { throw new Error(`Required file ${file} is missing`); } const stats = await fs.stat(filePath); logger.info(`File ${file} stats:`, stats); if (stats.size === 0) { throw new Error(`File ${file} is empty`); } if (file === 'video.mp4' && stats.size < 1024 * 1024) { throw new Error('Video file is too small, might be corrupted'); } } logger.success(`Upload structure validation successful for: ${uploadDir}`); return metadata; } catch (error) { logger.error(`Upload structure validation failed for: ${uploadDir}`, error); throw new Error(`Validation error: ${error.message}`); } } // Convert video to HLS async function convertToHLS(inputPath, outputFolder, movieId) { return new Promise((resolve, reject) => { logger.info(`Starting HLS conversion for movie ID: ${movieId}`); const hlsPath = path.join(outputFolder, "master.m3u8"); // Get video duration first exec(`ffprobe -v quiet -print_format json -show_format "${inputPath}"`, async (error, stdout) => { if (error) { logger.error(`FFprobe failed for movie ID: ${movieId}`, error); reject(error); return; } try { const metadata = JSON.parse(stdout); const duration = parseFloat(metadata.format.duration); logger.info(`Video duration: ${duration} seconds`); const ffmpegCmd = ` ffmpeg -i "${inputPath}" \ -progress pipe:1 \ -map 0:v -map 0:a? -c:v libx264 -crf 23 -maxrate 2000k -bufsize 4000k -vf "scale=1280:720" -c:a aac -ar 48000 -b:a 128k \ -hls_time 6 -hls_list_size 0 -hls_segment_filename "${outputFolder}/720p_%03d.ts" -hls_playlist_type vod "${outputFolder}/720p.m3u8" \ -map 0:v -map 0:a? -c:v libx264 -crf 23 -maxrate 1000k -bufsize 2000k -vf "scale=854:480" -c:a aac -ar 48000 -b:a 128k \ -hls_time 6 -hls_list_size 0 -hls_segment_filename "${outputFolder}/480p_%03d.ts" -hls_playlist_type vod "${outputFolder}/480p.m3u8" \ -map 0:v -map 0:a? -c:v libx264 -crf 23 -maxrate 600k -bufsize 1200k -vf "scale=640:360" -c:a aac -ar 48000 -b:a 128k \ -hls_time 6 -hls_list_size 0 -hls_segment_filename "${outputFolder}/360p_%03d.ts" -hls_playlist_type vod "${outputFolder}/360p.m3u8" `; logger.info(`Executing FFmpeg command for ${movieId}`); const process = exec(ffmpegCmd); // Track progress process.stderr.on('data', async (data) => { const match = data.match(/time=(\d{2}):(\d{2}):(\d{2}.\d{2})/); if (match) { const [_, hours, minutes, seconds] = match; const time = (hours * 3600) + (minutes * 60) + parseFloat(seconds); const progress = Math.round((time / duration) * 100); await Movie.findByIdAndUpdate(movieId, { processingProgress: progress }); if (progress % 10 === 0) { logger.info(`Conversion progress for movie ID ${movieId}: ${progress}%`); } } }); process.on('exit', async (code) => { if (code === 0) { const masterContent = `#EXTM3U #EXT-X-VERSION:3 #EXT-X-STREAM-INF:BANDWIDTH=2128000,RESOLUTION=1280x720 720p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=1128000,RESOLUTION=854x480 480p.m3u8 #EXT-X-STREAM-INF:BANDWIDTH=728000,RESOLUTION=640x360 360p.m3u8`; await fs.writeFile(hlsPath, masterContent); logger.success(`HLS conversion completed for movie ID: ${movieId}`); resolve(hlsPath); } else { logger.error(`FFmpeg process failed for movie ID: ${movieId}`, `Exit code: ${code}`); reject(new Error(`FFmpeg process exited with code ${code}`)); } }); } catch (error) { logger.error(`Error processing FFprobe output`, error); reject(error); } }); }); } // Add this helper function for recursive directory copying async function copyRecursive(src, dest) { const stats = await fs.stat(src); if (stats.isDirectory()) { // Create the destination directory await fs.mkdir(dest, { recursive: true }); // Read directory contents const entries = await fs.readdir(src); // Recursively copy each entry for (const entry of entries) { const srcPath = path.join(src, entry); const destPath = path.join(dest, entry); await copyRecursive(srcPath, destPath); } } else { // It's a file, copy it directly await fs.copyFile(src, dest); } } // Process uploaded directory async function processUploadedDirectory(dirPath) { const dirName = path.basename(dirPath); const processingPath = path.join(WATCH_DIRS.processing, dirName); const completedPath = path.join(WATCH_DIRS.completed, dirName); const failedPath = path.join(WATCH_DIRS.failed, dirName); let movieDoc; logger.info(`Starting processing for directory: ${dirName}`); try { // Ensure processing directory for this movie exists and is empty if (fsSync.existsSync(processingPath)) { await fs.rm(processingPath, { recursive: true, force: true }); logger.info(`Cleaned existing processing directory for movie: ${processingPath}`); } await fs.mkdir(processingPath, { recursive: true }); // Copy all files from pending to processing using recursive copy logger.info(`Copying files from ${dirPath} to ${processingPath}`); await copyRecursive(dirPath, processingPath); logger.info(`Copied all files from pending to processing`); // Remove the source directory after successful copy await fs.rm(dirPath, { recursive: true, force: true }); logger.info(`Removed source directory: ${dirPath}`); // Validate structure and get metadata const metadata = await validateUploadStructure(processingPath); // Create movie document movieDoc = new Movie({ title: metadata.title, category: metadata.category, description: metadata.description, type: metadata.type, rentalPrice: metadata.rentalPrice, status: 'processing' }); await movieDoc.save(); logger.success(`Created movie document with ID: ${movieDoc._id}`); // Process video const videoPath = path.join(processingPath, 'video.mp4'); const outputDir = path.join(processingPath, 'video'); await fs.mkdir(outputDir, { recursive: true }); // Convert to HLS await convertToHLS(videoPath, outputDir, movieDoc._id); // Update movie document with URLs const baseUrl = `https://theapix.in/uploads/completed/${dirName}`; await Movie.findByIdAndUpdate(movieDoc._id, { hlsUrl: `${baseUrl}/video/master.m3u8`, thumbnailUrl: `${baseUrl}/thumbnail.jpg`, trailerUrl: fsSync.existsSync(path.join(processingPath, 'trailer.mp4')) ? `${baseUrl}/trailer.mp4` : null, status: 'completed', processingProgress: 100 }); // Ensure only this movie's directory in completed is handled if (fsSync.existsSync(completedPath)) { logger.info(`Found existing directory for this movie in completed: ${completedPath}`); await fs.rm(completedPath, { recursive: true, force: true }); logger.info(`Cleaned existing directory for this movie in completed: ${completedPath}`); } await fs.mkdir(completedPath, { recursive: true }); // Copy all files from processing to completed using recursive copy logger.info(`Copying files from ${processingPath} to ${completedPath}`); await copyRecursive(processingPath, completedPath); logger.info(`Copied all files from processing to completed`); // Remove the processing directory after successful copy await fs.rm(processingPath, { recursive: true, force: true }); logger.info(`Removed processing directory: ${processingPath}`); logger.success(`Processing completed for: ${dirName}`); return movieDoc._id; } catch (error) { logger.error(`Processing failed for ${dirName}`, error); // Move to failed directory try { // Only handle this specific movie's directory in failed if (fsSync.existsSync(failedPath)) { await fs.rm(failedPath, { recursive: true, force: true }); logger.info(`Cleaned existing failed directory for this movie: ${failedPath}`); } await fs.mkdir(failedPath, { recursive: true }); // Copy files to failed directory using recursive copy const sourceDir = fsSync.existsSync(processingPath) ? processingPath : dirPath; if (fsSync.existsSync(sourceDir)) { logger.info(`Copying failed files from ${sourceDir} to ${failedPath}`); await copyRecursive(sourceDir, failedPath); logger.info(`Copied all failed files`); // Clean up source directory await fs.rm(sourceDir, { recursive: true, force: true }); logger.info(`Removed source directory after failure: ${sourceDir}`); } } catch (moveError) { logger.error(`Failed to move to failed directory: ${dirName}`, moveError); } // Update movie status if document was created if (movieDoc) { await Movie.findByIdAndUpdate(movieDoc._id, { status: 'failed', errorDetails: error.message }); } throw error; } } // Add this new function before your watcher code async function checkDirectoryDetails(dirPath) { try { logger.info(`Checking directory: ${dirPath}`); // Check if directory exists if (!fsSync.existsSync(dirPath)) { logger.error(`Directory does not exist: ${dirPath}`); return false; } // Check directory permissions try { fsSync.accessSync(dirPath, fsSync.constants.R_OK | fsSync.constants.W_OK); logger.info(`Directory permissions OK: ${dirPath}`); } catch (error) { logger.error(`Permission error on directory: ${dirPath}`, error); return false; } // List contents const contents = await fs.readdir(dirPath); logger.info(`Directory contents for ${dirPath}:`, contents); // Check required files const requiredFiles = ['metadata.json', 'video.mp4', 'thumbnail.jpg']; const missingFiles = requiredFiles.filter(file => !contents.includes(file)); if (missingFiles.length > 0) { logger.error(`Missing required files in ${dirPath}:`, missingFiles); return false; } // Check metadata.json const metadataPath = path.join(dirPath, 'metadata.json'); try { const metadata = JSON.parse(await fs.readFile(metadataPath, 'utf8')); logger.info(`Metadata content for ${dirPath}:`, metadata); } catch (error) { logger.error(`Error reading metadata.json in ${dirPath}:`, error); return false; } return true; } catch (error) { logger.error(`Error checking directory ${dirPath}:`, error); return false; } } // Enhanced directory watcher with debug logging const watcher = fsSync.watch(WATCH_DIRS.pending, { persistent: true }); logger.info(`Starting enhanced watcher for: ${WATCH_DIRS.pending}`); // Function to process all pending directories async function processAllPendingDirectories() { try { const contents = await fs.readdir(WATCH_DIRS.pending); logger.info('Checking pending directories:', contents); for (const dirName of contents) { const dirPath = path.join(WATCH_DIRS.pending, dirName); const stats = await fs.stat(dirPath); if (stats.isDirectory()) { logger.info(`Found directory: ${dirName}`); const isValid = await checkDirectoryDetails(dirPath); if (isValid) { logger.info(`Starting processing for directory: ${dirName}`); try { await processUploadedDirectory(dirPath); } catch (error) { logger.error(`Failed to process directory ${dirName}:`, error); } } else { logger.warn(`Invalid directory structure: ${dirName}`); } } } } catch (error) { logger.error('Error processing pending directories:', error); } } // Process existing directories on startup processAllPendingDirectories(); // Set up periodic check for new directories const checkInterval = 30000; // 30 seconds setInterval(processAllPendingDirectories, checkInterval); // Watch for new directories watcher.on('rename', async (filename) => { const uploadPath = path.join(WATCH_DIRS.pending, filename); logger.info(`File event detected: ${filename}`); // Wait briefly for upload to complete setTimeout(async () => { try { if (fsSync.existsSync(uploadPath)) { const stats = await fs.stat(uploadPath); if (stats.isDirectory()) { logger.info(`New directory detected: ${filename}`); const isValid = await checkDirectoryDetails(uploadPath); if (isValid) { logger.info(`Starting processing for new directory: ${filename}`); try { await processUploadedDirectory(uploadPath); } catch (error) { logger.error(`Failed to process directory ${filename}:`, error); } } else { logger.warn(`Invalid directory structure: ${filename}`); } } } } catch (error) { logger.error(`Error handling new directory ${filename}:`, error); } }, 5000); // 5 second delay to ensure upload is complete }); watcher.on('error', (error) => { logger.error(`Watch error:`, error); // Attempt to restart watcher on error try { watcher.close(); const newWatcher = fsSync.watch(WATCH_DIRS.pending, { persistent: true }); Object.assign(watcher, newWatcher); logger.info('Watcher restarted successfully'); } catch (restartError) { logger.error('Failed to restart watcher:', restartError); } }); // API Endpoints // Get movie status app.get("/movies/:id/status", async (req, res) => { try { const movie = await Movie.findById(req.params.id); if (!movie) { logger.error(`Movie not found: ${req.params.id}`); return res.status(404).json({ error: "Movie not found" }); } logger.info(`Status requested for movie: ${req.params.id}`, { status: movie.status, progress: movie.processingProgress }); res.json({ id: movie._id, title: movie.title, status: movie.status, processingProgress: movie.processingProgress || 0, errorDetails: movie.errorDetails, hlsUrl: movie.hlsUrl, thumbnailUrl: movie.thumbnailUrl, trailerUrl: movie.trailerUrl, category: movie.category, type: movie.type, description: movie.description }); } catch (err) { logger.error(`Failed to fetch movie status: ${req.params.id}`, err); res.status(500).json({ error: "Failed to fetch movie status" }); } }); // Get all completed movies app.get("/movies", async (req, res) => { try { const { category, type, limit = 20, page = 1 } = req.query; const skip = (parseInt(page) - 1) * parseInt(limit); const query = { status: 'completed' // Only show completed movies }; if (category) query.category = category; if (type) query.type = type; logger.info(`Fetching completed movies with query:`, query); logger.info(`Pagination: skip=${skip}, limit=${limit}`); const [movies, total] = await Promise.all([ Movie.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) .lean(), Movie.countDocuments(query) ]); logger.info(`Found ${movies.length} completed movies out of ${total} total`); res.json({ movies, pagination: { total, page: parseInt(page), pages: Math.ceil(total / limit) } }); } catch (err) { logger.error(`Failed to fetch movies: ${err.message}`); res.status(500).json({ error: "Failed to fetch movies" }); } }); // Get failed movies app.get("/movies/failed", async (req, res) => { try { const { category, type, limit = 20, page = 1 } = req.query; const skip = (parseInt(page) - 1) * parseInt(limit); const query = { status: 'failed' }; if (category) query.category = category; if (type) query.type = type; logger.info(`Fetching failed movies with query:`, query); const [movies, total] = await Promise.all([ Movie.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) .lean(), Movie.countDocuments(query) ]); logger.info(`Found ${movies.length} failed movies out of ${total} total`); res.json({ movies, pagination: { total, page: parseInt(page), pages: Math.ceil(total / limit) } }); } catch (err) { logger.error(`Failed to fetch failed movies: ${err.message}`); res.status(500).json({ error: "Failed to fetch failed movies" }); } }); // Get pending/processing movies app.get("/movies/pending", async (req, res) => { try { const { category, type, limit = 20, page = 1 } = req.query; const skip = (parseInt(page) - 1) * parseInt(limit); const query = { status: { $in: ['pending', 'processing'] } }; if (category) query.category = category; if (type) query.type = type; logger.info(`Fetching pending movies with query:`, query); const [movies, total] = await Promise.all([ Movie.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(parseInt(limit)) .lean(), Movie.countDocuments(query) ]); logger.info(`Found ${movies.length} pending movies out of ${total} total`); res.json({ movies, pagination: { total, page: parseInt(page), pages: Math.ceil(total / limit) } }); } catch (err) { logger.error(`Failed to fetch pending movies: ${err.message}`); res.status(500).json({ error: "Failed to fetch pending movies" }); } }); // Health check endpoint app.get("/health", (req, res) => { const health = { uptime: process.uptime(), timestamp: Date.now(), mongoConnection: mongoose.connection.readyState === 1 }; // Check directory permissions Object.entries(WATCH_DIRS).forEach(([key, dir]) => { try { fsSync.accessSync(dir, fsSync.constants.R_OK | fsSync.constants.W_OK); health[`${key}DirAccess`] = true; } catch (error) { health[`${key}DirAccess`] = false; } }); logger.info("Health check performed", health); res.json(health); }); // Add this code after requiring nodemailer const transporter = nodemailer.createTransport({ host: process.env.EMAIL_HOST, // e.g., 'smtp.gmail.com' port: process.env.EMAIL_PORT, // e.g., 587 secure: false, // true for 465, false for other ports auth: { user: process.env.EMAIL_USER, // your email pass: process.env.EMAIL_PASS, // your email password }, }); // Forgot password endpoint to send OTP app.post("/forgot-password", async (req, res) => { try { const { email } = req.body; const user = await User.findOne({ email }); if (!user) { logger.warn(`Password reset attempted for non-existent email: ${email}`); return res.status(404).json({ error: "User not found" }); } // Generate OTP const otp = Math.floor(100000 + Math.random() * 900000).toString(); // 6-digit OTP user.resetPasswordOtp = otp; user.resetPasswordExpires = Date.now() + 3600000; // OTP valid for 1 hour await user.save(); // Send OTP email const mailOptions = { from: process.env.EMAIL_USER, to: email, subject: 'Password Reset OTP', html: `

Password Reset OTP

Your OTP for password reset is: ${otp}

This OTP will expire in 1 hour.

If you didn't request this, please ignore this email.

` }; await transporter.sendMail(mailOptions); logger.success(`Password reset OTP sent to: ${email}`); res.json({ message: "Password reset OTP sent" }); } catch (err) { logger.error("Failed to process forgot password request", err); res.status(500).json({ error: "Failed to process request" }); } }); // OTP verification endpoint app.post("/verify-otp", async (req, res) => { try { const { email, otp } = req.body; const user = await User.findOne({ email, resetPasswordExpires: { $gt: Date.now() } }); if (!user) { logger.warn(`Invalid or expired OTP verification attempt for email: ${email}`); return res.status(400).json({ error: "Invalid or expired OTP" }); } // Verify OTP if (user.resetPasswordOtp !== otp) { logger.warn(`Invalid OTP used for email: ${email}`); return res.status(400).json({ error: "Invalid OTP" }); } // OTP is valid, allow user to set a new password res.json({ message: "OTP verified successfully. You can now set a new password." }); } catch (err) { logger.error("Failed to verify OTP", err); res.status(500).json({ error: "Failed to verify OTP" }); } }); // Reset password endpoint to update password app.post("/reset-password", async (req, res) => { try { const { email, newPassword } = req.body; const user = await User.findOne({ email }); if (!user) { logger.warn(`Password reset attempted for non-existent email: ${email}`); return res.status(404).json({ error: "User not found" }); } // Update password const hashedPassword = await bcrypt.hash(newPassword, 10); user.password = hashedPassword; user.resetPasswordOtp = undefined; // Clear OTP user.resetPasswordExpires = undefined; // Clear expiration await user.save(); logger.success(`Password reset successful for user: ${email}`); res.json({ message: "Password reset successful" }); } catch (err) { logger.error("Failed to reset password", err); res.status(500).json({ error: "Failed to reset password" }); } }); // Search Movies Endpoint app.get("/movies/search", async (req, res) => { try { const { query } = req.query; const searchRegex = new RegExp(query, 'i'); // Case-insensitive search const movies = await Movie.find({ $or: [ { title: searchRegex }, { category: searchRegex } ] }).lean(); res.json(movies); } catch (err) { logger.error("Failed to search movies", err); res.status(500).json({ error: "Failed to search movies" }); } }); // Initialize Razorpay instance const razorpay = new Razorpay({ key_id: process.env.RAZORPAY_KEY_ID, key_secret: process.env.RAZORPAY_SECRET }); // Rent a Movie app.post("/purchase/movie/:id", authenticateJWT, async (req, res) => { try { const movieId = req.params.id; const userId = req.user.id; logger.info(`Rental request received - Movie: ${movieId}, User: ${userId}`); // Validate movieId format if (!mongoose.Types.ObjectId.isValid(movieId)) { logger.error(`Invalid movie ID format: ${movieId}`); return res.status(400).json({ error: "Invalid movie ID format" }); } const movie = await Movie.findById(movieId); if (!movie) { logger.error(`Movie not found: ${movieId}`); return res.status(404).json({ error: "Movie not found" }); } // Check if user already has an active rental const existingRental = await Rental.findOne({ userId, contentId: movieId, status: 'active', paymentStatus: { $in: ['completed', 'pending'] } }); if (existingRental) { logger.warn(`User ${userId} already has an active rental for movie ${movieId}`); return res.json({ message: "Rental already exists", rental: existingRental }); } // Calculate rental expiration (48 hours from now) const rentalEnd = new Date(); rentalEnd.setHours(rentalEnd.getHours() + 48); const rental = new Rental({ userId, contentId: movieId, contentType: 'movie', rentalEnd, status: 'active', paymentStatus: 'pending' }); await rental.save(); logger.success(`Rental created for movie: ${movieId}, user: ${userId}`); res.json({ message: "Rental initiated", rental: rental.toObject() }); } catch (err) { logger.error(`Failed to create rental: ${err.message}`); res.status(500).json({ error: "Failed to create rental", details: err.message }); } }); // Rent a Show app.post("/purchase/show/:id", authenticateJWT, async (req, res) => { try { const showId = req.params.id; const userId = req.user.id; const show = await Movie.findById(showId); if (!show || show.type !== 'show') { logger.error(`Show not found: ${showId}`); return res.status(404).json({ error: "Show not found" }); } // Calculate rental expiration (48 hours from now) const rentalEnd = new Date(); rentalEnd.setHours(rentalEnd.getHours() + 48); const rental = new Rental({ userId, contentId: showId, contentType: 'show', rentalEnd, paymentStatus: 'pending' }); await rental.save(); logger.success(`Rental created for show: ${showId}, user: ${userId}`); res.json({ message: "Show rental initiated", rental }); } catch (err) { logger.error(`Failed to rent show: ${err.message}`); res.status(500).json({ error: "Failed to rent show" }); } }); // Rent an Episode app.post("/purchase/show/:id/season/:seasonNumber/episode/:episodeNumber", authenticateJWT, async (req, res) => { try { const showId = req.params.id; const seasonNumber = req.params.seasonNumber; const episodeNumber = req.params.episodeNumber; const userId = req.user.id; const show = await Movie.findById(showId); if (!show || show.type !== 'show') { logger.error(`Show not found: ${showId}`); return res.status(404).json({ error: "Show not found" }); } const season = show.seasons.find(s => s.seasonNumber === parseInt(seasonNumber)); if (!season) { logger.error(`Season not found: ${seasonNumber} for show: ${showId}`); return res.status(404).json({ error: "Season not found" }); } const episode = season.episodes[episodeNumber - 1]; // Assuming episode numbers start from 1 if (!episode) { logger.error(`Episode not found: ${episodeNumber} for show: ${showId}`); return res.status(404).json({ error: "Episode not found" }); } // Calculate rental expiration (48 hours from now) const rentalEnd = new Date(); rentalEnd.setHours(rentalEnd.getHours() + 48); const rental = new Rental({ userId, contentId: showId, contentType: 'episode', rentalEnd, paymentStatus: 'pending' }); await rental.save(); logger.success(`Rental created for episode: ${episode.title}, user: ${userId}`); res.json({ message: "Episode rental initiated", rental }); } catch (err) { logger.error(`Failed to rent episode: ${err.message}`); res.status(500).json({ error: "Failed to rent episode" }); } }); // Get Active Rentals app.get("/purchase/active", authenticateJWT, async (req, res) => { try { const userId = req.user.id; const activeRentals = await Rental.find({ userId, rentalEnd: { $gt: new Date() } }).populate('contentId'); logger.info(`Retrieved active rentals for user: ${userId}`); res.json(activeRentals); } catch (err) { logger.error(`Failed to fetch active rentals: ${err.message}`); res.status(500).json({ error: "Failed to fetch active rentals" }); } }); // Stream Content app.get("/stream/:id", authenticateJWT, async (req, res) => { try { const contentId = req.params.id; const userId = req.user.id; const rental = await Rental.findOne({ userId, contentId, rentalEnd: { $gt: new Date() } }); if (!rental) { logger.warn(`Unauthorized streaming attempt for content: ${contentId}, user: ${userId}`); return res.status(403).json({ error: "Access denied. Rental has expired or does not exist." }); } const content = await Movie.findById(contentId); if (!content) { logger.error(`Content not found: ${contentId}`); return res.status(404).json({ error: "Content not found" }); } logger.info(`Streaming content: ${contentId} for user: ${userId}`); res.json({ streamingUrl: content.hlsUrl }); } catch (err) { logger.error(`Failed to stream content: ${err.message}`); res.status(500).json({ error: "Failed to stream content" }); } }); // Wishlist Endpoints // Add to Wishlist app.post("/wishlist/:id", authenticateJWT, async (req, res) => { try { const userId = req.user.id; const contentId = req.params.id; const existingItem = await Wishlist.findOne({ userId, contentId }); if (existingItem) { logger.warn(`Item already in wishlist: ${contentId}`); return res.status(400).json({ error: "Item already in wishlist" }); } const wishlistItem = new Wishlist({ userId, contentId }); await wishlistItem.save(); logger.success(`Item added to wishlist: ${contentId}`); res.json({ message: "Item added to wishlist", wishlistItem }); } catch (err) { logger.error(`Failed to add to wishlist: ${err.message}`); res.status(500).json({ error: "Failed to add to wishlist" }); } }); // Remove from Wishlist app.delete("/wishlist/:id", authenticateJWT, async (req, res) => { try { const userId = req.user.id; const contentId = req.params.id; await Wishlist.findOneAndDelete({ userId, contentId }); logger.success(`Item removed from wishlist: ${contentId}`); res.json({ message: "Item removed from wishlist" }); } catch (err) { logger.error(`Failed to remove from wishlist: ${err.message}`); res.status(500).json({ error: "Failed to remove from wishlist" }); } }); // Get Wishlist app.get("/wishlist", authenticateJWT, async (req, res) => { try { const userId = req.user.id; const wishlistItems = await Wishlist.find({ userId }).populate('contentId'); logger.info(`Retrieved wishlist for user: ${userId}`); res.json(wishlistItems); } catch (err) { logger.error(`Failed to fetch wishlist: ${err.message}`); res.status(500).json({ error: "Failed to fetch wishlist" }); } }); // Get User Purchase History app.get("/purchase/history", authenticateJWT, async (req, res) => { try { const userId = req.user.id; const rentals = await Rental.find({ userId }) .populate('contentId') .sort({ rentalEnd: -1 }); const purchaseHistory = rentals.map(rental => ({ rentalId: rental._id, contentId: rental.contentId._id, title: rental.contentId.title, rentalEnd: rental.rentalEnd, status: rental.paymentStatus, contentType: rental.contentType })); logger.info(`Retrieved purchase history for user: ${userId}`); res.json(purchaseHistory); } catch (err) { logger.error(`Failed to fetch purchase history: ${err.message}`); res.status(500).json({ error: "Failed to fetch purchase history" }); } }); // Create Payment Order app.post("/payment/create", authenticateJWT, async (req, res) => { try { const { amount } = req.body; if (!amount || amount <= 0) { logger.error('Invalid amount for payment order:', amount); return res.status(400).json({ error: "Invalid amount" }); } logger.info(`Creating payment order for amount: ${amount}`); logger.info(`Using Razorpay key: ${process.env.RAZORPAY_KEY_ID}`); // Create a shorter receipt ID using timestamp and last 4 chars of user ID const shortUserId = req.user.id.slice(-4); const timestamp = Date.now().toString().slice(-8); const receiptId = `rcpt_${timestamp}_${shortUserId}`; const options = { amount: Math.round(amount), // Ensure amount is rounded currency: "INR", receipt: receiptId, payment_capture: 1 }; logger.info('Creating order with options:', options); // Wrap Razorpay order creation in a Promise try { const order = await new Promise((resolve, reject) => { razorpay.orders.create(options, (err, order) => { if (err) { logger.error('Razorpay create order error:', err); reject(err); } else { resolve(order); } }); }); logger.success(`Payment order created: ${order.id}`); res.json({ id: order.id, amount: order.amount, currency: order.currency }); } catch (razorpayError) { logger.error('Razorpay error:', razorpayError); res.status(500).json({ error: "Failed to create payment order", details: razorpayError.error?.description || razorpayError.message || 'Razorpay error' }); } } catch (err) { logger.error(`Failed to create payment order: ${err.stack}`); res.status(500).json({ error: "Failed to create payment order", details: err.message }); } }); // Verify Payment app.post("/payment/verify", authenticateJWT, async (req, res) => { try { const { paymentId, orderId, signature, rentalId } = req.body; const userId = req.user.id; logger.info(`Verifying payment - Order: ${orderId}, Payment: ${paymentId}, User: ${userId}`); const secret = process.env.RAZORPAY_SECRET; if (!secret) { logger.error('Razorpay secret key not found in environment variables'); return res.status(500).json({ error: "Payment verification configuration error" }); } const generatedSignature = crypto .createHmac('sha256', secret) .update(orderId + "|" + paymentId) .digest('hex'); if (generatedSignature !== signature) { logger.warn(`Invalid payment signature for order: ${orderId}`); return res.status(400).json({ error: "Invalid payment signature" }); } await Rental.findByIdAndUpdate(rentalId, { paymentStatus: 'completed' }); logger.success(`Payment verified for order: ${orderId}, user: ${userId}`); res.json({ success: true }); } catch (err) { logger.error(`Payment verification failed: ${err.message}`); res.status(500).json({ error: "Payment verification failed" }); } }); // Delete a movie app.delete("/movies/:id", authenticateJWT, async (req, res) => { try { const movieId = req.params.id; // Find the movie first const movie = await Movie.findById(movieId); if (!movie) { logger.error(`Movie not found for deletion: ${movieId}`); return res.status(404).json({ error: "Movie not found" }); } logger.info(`Attempting to delete movie: ${movie.title} (${movieId})`); // Extract directory name from URLs if available let dirName = ''; if (movie.hlsUrl) { // Extract from URL like "https://theapix.in/uploads/completed/MovieName/video/master.m3u8" const match = movie.hlsUrl.match(/uploads\/(completed|failed|processing)\/([^\/]+)/); if (match) { dirName = match[2]; } } // If we found a directory name, clean up files if (dirName) { // Clean up from all possible locations for (const status of ['completed', 'failed', 'processing']) { const dirPath = path.join(WATCH_DIRS[status], dirName); if (fsSync.existsSync(dirPath)) { logger.info(`Removing directory: ${dirPath}`); await fs.rm(dirPath, { recursive: true, force: true }); logger.success(`Removed ${status} directory for movie: ${dirName}`); } } } // Delete the movie document await Movie.findByIdAndDelete(movieId); // Delete associated rentals await Rental.deleteMany({ contentId: movieId }); // Delete from wishlists await Wishlist.deleteMany({ contentId: movieId }); logger.success(`Successfully deleted movie: ${movie.title} (${movieId})`); res.json({ message: "Movie deleted successfully", movieId, title: movie.title }); } catch (err) { logger.error(`Failed to delete movie: ${err.message}`); res.status(500).json({ error: "Failed to delete movie", details: err.message }); } }); // Start the server app.listen(port, () => { logger.success(`Server running on http://localhost:${port}`); logger.info('Watch directories:', WATCH_DIRS); }); // Handle process termination process.on('SIGTERM', () => { logger.info('SIGTERM received. Shutting down gracefully...'); watcher.close(); mongoose.connection.close(); process.exit(0); }); process.on('uncaughtException', (error) => { logger.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason, promise) => { logger.error('Unhandled Rejection:', reason); });