+
๐ฌ Nuvio Trailer Server
+
+
+ โ
Server is running and ready!
+
+
+
+
GET
+
/health
+
Health check endpoint to verify server status
+
+
+
+
GET
+
/trailer?youtube_url=YOUTUBE_URL&title=TITLE&year=YEAR
+
Convert YouTube trailer URL to direct streaming link using yt-dlp
+
+
+
+
GET
+
/cache
+
View cached trailers (for debugging)
+
+
+
+
DELETE
+
/cache
+
Clear all cached trailers
+
+
+
+
+
+
diff --git a/trailer-server/server.js b/trailer-server/server.js
new file mode 100644
index 0000000..06fbc47
--- /dev/null
+++ b/trailer-server/server.js
@@ -0,0 +1,288 @@
+const express = require('express');
+const cors = require('cors');
+const helmet = require('helmet');
+const { RateLimiterMemory } = require('rate-limiter-flexible');
+const NodeCache = require('node-cache');
+const { exec } = require('child_process');
+const { promisify } = require('util');
+const { searchYouTubeTrailer } = require('./youtube-search');
+
+const execAsync = promisify(exec);
+
+const app = express();
+const PORT = process.env.PORT || 3001;
+
+// Cache configuration - cache trailer URLs for 24 hours
+const trailerCache = new NodeCache({
+ stdTTL: 24 * 60 * 60, // 24 hours
+ checkperiod: 60 * 60 // Check for expired keys every hour
+});
+
+// Rate limiting - 10 requests per minute per IP
+const rateLimiter = new RateLimiterMemory({
+ keyPrefix: 'trailer_api',
+ points: 10, // Number of requests
+ duration: 60, // Per 60 seconds
+});
+
+// Middleware
+app.use(helmet());
+app.use(cors());
+app.use(express.json());
+
+// Rate limiting middleware
+const rateLimiterMiddleware = async (req, res, next) => {
+ try {
+ await rateLimiter.consume(req.ip);
+ next();
+ } catch (rejRes) {
+ res.status(429).json({
+ error: 'Too many requests',
+ retryAfter: Math.round(rejRes.msBeforeNext / 1000) || 1
+ });
+ }
+};
+
+// Health check endpoint
+app.get('/health', (req, res) => {
+ res.json({
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ cache: {
+ keys: trailerCache.keys().length,
+ stats: trailerCache.getStats()
+ }
+ });
+});
+
+// Auto-search trailer endpoint (no YouTube URL needed)
+app.get('/search-trailer', rateLimiterMiddleware, async (req, res) => {
+ try {
+ const { title, year } = req.query;
+
+ // Validate required parameters
+ if (!title) {
+ return res.status(400).json({
+ error: 'title parameter is required'
+ });
+ }
+
+ // Create cache key
+ const cacheKey = `search_${title}_${year}`;
+
+ // Check cache first
+ const cachedResult = trailerCache.get(cacheKey);
+ if (cachedResult) {
+ console.log(`๐ฏ Cache hit for search: ${title} (${year})`);
+ return res.json(cachedResult);
+ }
+
+ console.log(`๐ Auto-searching trailer for: ${title} (${year})`);
+
+ // Search for YouTube trailer
+ const searchQuery = `${title} ${year || ''} official trailer`.trim();
+ const youtubeUrl = await searchYouTubeTrailer(searchQuery);
+
+ if (!youtubeUrl) {
+ console.log(`โ No trailer found for: ${title} (${year})`);
+ return res.status(404).json({
+ error: 'Trailer not found'
+ });
+ }
+
+ // Now get the direct streaming URL
+ const command = `yt-dlp -f "best[height<=720][ext=mp4]/best[height<=720]/best" -g --no-playlist "${youtubeUrl}"`;
+
+ const { stdout, stderr } = await execAsync(command, {
+ timeout: 30000,
+ maxBuffer: 1024 * 1024
+ });
+
+ if (stderr && !stderr.includes('WARNING')) {
+ console.error('yt-dlp stderr:', stderr);
+ }
+
+ const directUrl = stdout.trim();
+
+ if (!directUrl || !isValidUrl(directUrl)) {
+ console.log(`โ No valid streaming URL found for: ${title} (${year})`);
+ return res.status(404).json({
+ error: 'Trailer not found or invalid URL'
+ });
+ }
+
+ const result = {
+ url: directUrl,
+ title: title || 'Unknown',
+ year: year || 'Unknown',
+ source: 'youtube',
+ youtubeUrl: youtubeUrl,
+ cached: false,
+ timestamp: new Date().toISOString()
+ };
+
+ // Cache the result
+ trailerCache.set(cacheKey, result);
+ console.log(`โ
Successfully found and processed trailer for: ${title} (${year})`);
+
+ res.json(result);
+
+ } catch (error) {
+ console.error('Error in auto-search:', error);
+
+ if (error.code === 'TIMEOUT') {
+ return res.status(408).json({
+ error: 'Request timeout - video processing took too long'
+ });
+ }
+
+ res.status(500).json({
+ error: 'Internal server error',
+ message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'
+ });
+ }
+});
+
+// Main trailer endpoint
+app.get('/trailer', rateLimiterMiddleware, async (req, res) => {
+ try {
+ const { youtube_url, title, year } = req.query;
+
+ // Validate required parameters
+ if (!youtube_url) {
+ return res.status(400).json({
+ error: 'youtube_url parameter is required'
+ });
+ }
+
+ // Create cache key
+ const cacheKey = `trailer_${title}_${year}_${youtube_url}`;
+
+ // Check cache first
+ const cachedResult = trailerCache.get(cacheKey);
+ if (cachedResult) {
+ console.log(`๐ฏ Cache hit for: ${title} (${year})`);
+ return res.json(cachedResult);
+ }
+
+ console.log(`๐ Fetching trailer for: ${title} (${year})`);
+
+ // Use yt-dlp to get direct streaming URL
+ // Prefer MP4 format, max 720p for better compatibility
+ const command = `yt-dlp -f "best[height<=720][ext=mp4]/best[height<=720]/best" -g --no-playlist "${youtube_url}"`;
+
+ const { stdout, stderr } = await execAsync(command, {
+ timeout: 30000, // 30 second timeout
+ maxBuffer: 1024 * 1024 // 1MB buffer
+ });
+
+ if (stderr && !stderr.includes('WARNING')) {
+ console.error('yt-dlp stderr:', stderr);
+ }
+
+ const directUrl = stdout.trim();
+
+ if (!directUrl || !isValidUrl(directUrl)) {
+ console.log(`โ No valid URL found for: ${title} (${year})`);
+ return res.status(404).json({
+ error: 'Trailer not found or invalid URL'
+ });
+ }
+
+ const result = {
+ url: directUrl,
+ title: title || 'Unknown',
+ year: year || 'Unknown',
+ source: 'youtube',
+ cached: false,
+ timestamp: new Date().toISOString()
+ };
+
+ // Cache the result
+ trailerCache.set(cacheKey, result);
+ console.log(`โ
Successfully fetched trailer for: ${title} (${year})`);
+
+ res.json(result);
+
+ } catch (error) {
+ console.error('Error fetching trailer:', error);
+
+ if (error.code === 'TIMEOUT') {
+ return res.status(408).json({
+ error: 'Request timeout - video processing took too long'
+ });
+ }
+
+ if (error.message.includes('not found') || error.message.includes('unavailable')) {
+ return res.status(404).json({
+ error: 'Trailer not found'
+ });
+ }
+
+ res.status(500).json({
+ error: 'Internal server error',
+ message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'
+ });
+ }
+});
+
+// Get cached trailers (for debugging)
+app.get('/cache', (req, res) => {
+ const keys = trailerCache.keys();
+ const cacheData = {};
+
+ keys.forEach(key => {
+ cacheData[key] = trailerCache.get(key);
+ });
+
+ res.json({
+ count: keys.length,
+ keys: keys,
+ data: cacheData
+ });
+});
+
+// Clear cache endpoint (for maintenance)
+app.delete('/cache', (req, res) => {
+ trailerCache.flushAll();
+ res.json({
+ message: 'Cache cleared successfully',
+ timestamp: new Date().toISOString()
+ });
+});
+
+// Helper function to validate URLs
+function isValidUrl(string) {
+ try {
+ new URL(string);
+ return true;
+ } catch (_) {
+ return false;
+ }
+}
+
+// Error handling middleware
+app.use((error, req, res, next) => {
+ console.error('Unhandled error:', error);
+ res.status(500).json({
+ error: 'Internal server error'
+ });
+});
+
+// 404 handler
+app.use('*', (req, res) => {
+ res.status(404).json({
+ error: 'Endpoint not found',
+ availableEndpoints: ['/health', '/trailer', '/cache']
+ });
+});
+
+// Start server
+app.listen(PORT, () => {
+ console.log(`๐ Trailer server running on port ${PORT}`);
+ console.log(`๐ Health check: http://localhost:${PORT}/health`);
+ console.log(`๐ฌ Trailer endpoint: http://localhost:${PORT}/trailer`);
+ console.log(`๐พ Cache endpoint: http://localhost:${PORT}/cache`);
+});
+
+module.exports = app;
diff --git a/trailer-server/test.js b/trailer-server/test.js
new file mode 100644
index 0000000..b68fce4
--- /dev/null
+++ b/trailer-server/test.js
@@ -0,0 +1,87 @@
+const fetch = require('node-fetch');
+
+const SERVER_URL = 'http://localhost:3001';
+
+async function testServer() {
+ console.log('๐งช Testing Trailer Server...\n');
+
+ // Test 1: Health check
+ console.log('1๏ธโฃ Testing health endpoint...');
+ try {
+ const healthResponse = await fetch(`${SERVER_URL}/health`);
+ const healthData = await healthResponse.json();
+ console.log('โ
Health check passed:', healthData.status);
+ console.log('๐ Cache stats:', healthData.cache);
+ } catch (error) {
+ console.log('โ Health check failed:', error.message);
+ }
+
+ console.log('\n');
+
+ // Test 2: Trailer endpoint with sample YouTube URL
+ console.log('2๏ธโฃ Testing trailer endpoint...');
+ const testTrailer = {
+ youtube_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', // Rick Roll for testing
+ title: 'Test Movie',
+ year: '2023'
+ };
+
+ try {
+ const trailerResponse = await fetch(
+ `${SERVER_URL}/trailer?${new URLSearchParams(testTrailer)}`
+ );
+
+ if (trailerResponse.ok) {
+ const trailerData = await trailerResponse.json();
+ console.log('โ
Trailer fetch successful!');
+ console.log('๐น Title:', trailerData.title);
+ console.log('๐
Year:', trailerData.year);
+ console.log('๐ URL:', trailerData.url.substring(0, 50) + '...');
+ console.log('โฐ Timestamp:', trailerData.timestamp);
+ } else {
+ const errorData = await trailerResponse.json();
+ console.log('โ Trailer fetch failed:', errorData.error);
+ }
+ } catch (error) {
+ console.log('โ Trailer test failed:', error.message);
+ }
+
+ console.log('\n');
+
+ // Test 3: Cache endpoint
+ console.log('3๏ธโฃ Testing cache endpoint...');
+ try {
+ const cacheResponse = await fetch(`${SERVER_URL}/cache`);
+ const cacheData = await cacheResponse.json();
+ console.log('โ
Cache endpoint working');
+ console.log('๐ฆ Cached items:', cacheData.count);
+ } catch (error) {
+ console.log('โ Cache test failed:', error.message);
+ }
+
+ console.log('\n');
+
+ // Test 4: Rate limiting
+ console.log('4๏ธโฃ Testing rate limiting...');
+ try {
+ const promises = Array(12).fill().map(() =>
+ fetch(`${SERVER_URL}/trailer?youtube_url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&title=Test&year=2023`)
+ );
+
+ const responses = await Promise.all(promises);
+ const rateLimited = responses.some(r => r.status === 429);
+
+ if (rateLimited) {
+ console.log('โ
Rate limiting working correctly');
+ } else {
+ console.log('โ ๏ธ Rate limiting may not be working');
+ }
+ } catch (error) {
+ console.log('โ Rate limiting test failed:', error.message);
+ }
+
+ console.log('\n๐ Testing complete!');
+}
+
+// Run tests
+testServer().catch(console.error);
diff --git a/trailer-server/youtube-search.js b/trailer-server/youtube-search.js
new file mode 100644
index 0000000..de80eb3
--- /dev/null
+++ b/trailer-server/youtube-search.js
@@ -0,0 +1,58 @@
+const { exec } = require('child_process');
+const { promisify } = require('util');
+
+const execAsync = promisify(exec);
+
+/**
+ * Search YouTube for trailers using yt-dlp search functionality
+ * @param {string} query - Search query (e.g., "Avengers Endgame 2019 official trailer")
+ * @returns {Promise