trailer server test.

This commit is contained in:
tapframe 2025-09-13 17:02:11 +05:30
parent b03f550765
commit fe483ea7aa
16 changed files with 2671 additions and 15 deletions

@ -1 +1 @@
Subproject commit 37b9ae4298bd38b8119d45e8670f40bec2780a8b
Subproject commit e6c0e8c0d75a8595031fe450e29db0c40969b5b8

View file

@ -11,12 +11,12 @@ import {
RefreshControl,
StatusBar,
Platform,
Image,
ActivityIndicator,
Modal,
Dimensions,
Animated,
} from 'react-native';
import { Image } from 'expo-image';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
@ -1475,8 +1475,8 @@ const PluginsScreen: React.FC = () => {
{scraper.logo ? (
<Image
source={{ uri: scraper.logo }}
style={styles.scraperLogo}
resizeMode="contain"
style={styles.scraperLogo}
contentFit="contain"
/>
) : (
<View style={styles.scraperLogo} />

View file

@ -7,8 +7,11 @@ export interface TrailerData {
}
export class TrailerService {
private static readonly BASE_URL = 'https://db.xprime.tv/trailers';
private static readonly XPRIME_URL = 'https://db.xprime.tv/trailers';
private static readonly LOCAL_SERVER_URL = 'http://192.168.1.11:3001/trailer';
private static readonly AUTO_SEARCH_URL = 'http://192.168.1.11:3001/search-trailer';
private static readonly TIMEOUT = 10000; // 10 seconds
private static readonly USE_LOCAL_SERVER = true; // Toggle between local and XPrime
/**
* Fetches trailer URL for a given title and year
@ -17,13 +20,84 @@ export class TrailerService {
* @returns Promise<string | null> - The trailer URL or null if not found
*/
static async getTrailerUrl(title: string, year: number): Promise<string | null> {
if (this.USE_LOCAL_SERVER) {
// Try local server first, fallback to XPrime if it fails
const localResult = await this.getTrailerFromLocalServer(title, year);
if (localResult) {
return localResult;
}
logger.info('TrailerService', `Local server failed, falling back to XPrime for: ${title} (${year})`);
return this.getTrailerFromXPrime(title, year);
} else {
return this.getTrailerFromXPrime(title, year);
}
}
/**
* Fetches trailer from local server using auto-search (no YouTube URL needed)
* @param title - The movie/series title
* @param year - The release year
* @returns Promise<string | null> - The trailer URL or null if not found
*/
private static async getTrailerFromLocalServer(title: string, year: number): Promise<string | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
const url = `${this.BASE_URL}?title=${encodeURIComponent(title)}&year=${year}`;
const url = `${this.AUTO_SEARCH_URL}?title=${encodeURIComponent(title)}&year=${year}`;
logger.info('TrailerService', `Fetching trailer for: ${title} (${year})`);
logger.info('TrailerService', `Auto-searching trailer for: ${title} (${year})`);
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': 'Nuvio/1.0',
},
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn('TrailerService', `Auto-search failed: ${response.status} ${response.statusText}`);
return null;
}
const data = await response.json();
if (!data.url || !this.isValidTrailerUrl(data.url)) {
logger.warn('TrailerService', `Invalid trailer URL from auto-search: ${data.url}`);
return null;
}
logger.info('TrailerService', `Successfully found trailer: ${data.url.substring(0, 50)}...`);
return data.url;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
logger.warn('TrailerService', 'Auto-search request timed out');
} else {
logger.error('TrailerService', 'Error in auto-search:', error);
}
return null; // Return null to trigger XPrime fallback
}
}
/**
* Fetches trailer from XPrime API (original method)
* @param title - The movie/series title
* @param year - The release year
* @returns Promise<string | null> - The trailer URL or null if not found
*/
private static async getTrailerFromXPrime(title: string, year: number): Promise<string | null> {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT);
const url = `${this.XPRIME_URL}?title=${encodeURIComponent(title)}&year=${year}`;
logger.info('TrailerService', `Fetching trailer from XPrime for: ${title} (${year})`);
const response = await fetch(url, {
method: 'GET',
@ -37,32 +111,32 @@ export class TrailerService {
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn('TrailerService', `Failed to fetch trailer: ${response.status} ${response.statusText}`);
logger.warn('TrailerService', `XPrime failed: ${response.status} ${response.statusText}`);
return null;
}
const trailerUrl = await response.text();
// Validate the response is a valid URL
if (!trailerUrl || !this.isValidTrailerUrl(trailerUrl.trim())) {
logger.warn('TrailerService', `Invalid trailer URL received: ${trailerUrl}`);
logger.warn('TrailerService', `Invalid trailer URL from XPrime: ${trailerUrl}`);
return null;
}
const cleanUrl = trailerUrl.trim();
logger.info('TrailerService', `Successfully fetched trailer URL: ${cleanUrl}`);
logger.info('TrailerService', `Successfully fetched trailer from XPrime: ${cleanUrl}`);
return cleanUrl;
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
logger.warn('TrailerService', 'Trailer fetch request timed out');
logger.warn('TrailerService', 'XPrime request timed out');
} else {
logger.error('TrailerService', 'Error fetching trailer:', error);
logger.error('TrailerService', 'Error fetching from XPrime:', error);
}
return null;
}
}
/**
* Validates if the provided string is a valid trailer URL
* @param url - The URL to validate
@ -86,7 +160,12 @@ export class TrailerService {
'dailymotion.com',
'twitch.tv',
'amazonaws.com',
'cloudfront.net'
'cloudfront.net',
'googlevideo.com', // Google's CDN for YouTube videos
'sn-aigl6nzr.googlevideo.com', // Specific Google CDN servers
'sn-aigl6nze.googlevideo.com',
'sn-aigl6nsk.googlevideo.com',
'sn-aigl6ns6.googlevideo.com'
];
const hostname = urlObj.hostname.toLowerCase();
@ -94,13 +173,17 @@ export class TrailerService {
hostname.includes(domain) || hostname.endsWith(domain)
);
// Special check for Google Video CDN (YouTube direct streaming URLs)
const isGoogleVideoCDN = hostname.includes('googlevideo.com') ||
hostname.includes('sn-') && hostname.includes('.googlevideo.com');
// Check for video file extensions or streaming formats
const hasVideoFormat = /\.(mp4|m3u8|mpd|webm|mov|avi|mkv)$/i.test(urlObj.pathname) ||
url.includes('formats=') ||
url.includes('manifest') ||
url.includes('playlist');
return isValidDomain || hasVideoFormat;
return isValidDomain || hasVideoFormat || isGoogleVideoCDN;
} catch {
return false;
}
@ -161,6 +244,78 @@ export class TrailerService {
year
};
}
/**
* Switch between local server and XPrime API
* @param useLocal - true for local server, false for XPrime
*/
static setUseLocalServer(useLocal: boolean): void {
(this as any).USE_LOCAL_SERVER = useLocal;
logger.info('TrailerService', `Switched to ${useLocal ? 'local server' : 'XPrime API'}`);
}
/**
* Get current server status
* @returns object with server information
*/
static getServerStatus(): { usingLocal: boolean; localUrl: string; xprimeUrl: string; fallbackEnabled: boolean } {
return {
usingLocal: this.USE_LOCAL_SERVER,
localUrl: this.LOCAL_SERVER_URL,
xprimeUrl: this.XPRIME_URL,
fallbackEnabled: true // Always enabled now
};
}
/**
* Test both servers and return their status
* @returns Promise with server status information
*/
static async testServers(): Promise<{
localServer: { status: 'online' | 'offline'; responseTime?: number };
xprimeServer: { status: 'online' | 'offline'; responseTime?: number };
}> {
const results = {
localServer: { status: 'offline' as const },
xprimeServer: { status: 'offline' as const }
};
// Test local server
try {
const startTime = Date.now();
const response = await fetch(`${this.AUTO_SEARCH_URL}?title=test&year=2023`, {
method: 'GET',
signal: AbortSignal.timeout(5000) // 5 second timeout
});
if (response.ok || response.status === 404) { // 404 is ok, means server is running
results.localServer = {
status: 'online',
responseTime: Date.now() - startTime
};
}
} catch (error) {
logger.warn('TrailerService', 'Local server test failed:', error);
}
// Test XPrime server
try {
const startTime = Date.now();
const response = await fetch(`${this.XPRIME_URL}?title=test&year=2023`, {
method: 'GET',
signal: AbortSignal.timeout(5000) // 5 second timeout
});
if (response.ok || response.status === 404) { // 404 is ok, means server is running
results.xprimeServer = {
status: 'online',
responseTime: Date.now() - startTime
};
}
} catch (error) {
logger.warn('TrailerService', 'XPrime server test failed:', error);
}
return results;
}
}
export default TrailerService;

View file

@ -0,0 +1,54 @@
// Quick test to verify TrailerService integration
// Run this from the main Nuvio directory
const TrailerService = require('./src/services/trailerService.ts');
async function testTrailerIntegration() {
console.log('🧪 Testing TrailerService Integration...\n');
// Test 1: Check server status
console.log('1⃣ Server Status:');
const status = TrailerService.getServerStatus();
console.log('✅ Using Local Server:', status.usingLocal);
console.log('🔗 Local URL:', status.localUrl);
console.log('🔗 XPrime URL:', status.xprimeUrl);
console.log('\n');
// Test 2: Try to fetch a trailer
console.log('2⃣ Testing trailer fetch...');
try {
const trailerUrl = await TrailerService.getTrailerUrl('Test Movie', 2023);
if (trailerUrl) {
console.log('✅ Trailer URL fetched successfully!');
console.log('🔗 URL:', trailerUrl.substring(0, 80) + '...');
} else {
console.log('❌ No trailer URL returned');
}
} catch (error) {
console.log('❌ Error fetching trailer:', error.message);
}
console.log('\n');
// Test 3: Test trailer data
console.log('3⃣ Testing trailer data...');
try {
const trailerData = await TrailerService.getTrailerData('Test Movie', 2023);
if (trailerData) {
console.log('✅ Trailer data fetched successfully!');
console.log('📹 Title:', trailerData.title);
console.log('📅 Year:', trailerData.year);
console.log('🔗 URL:', trailerData.url.substring(0, 80) + '...');
} else {
console.log('❌ No trailer data returned');
}
} catch (error) {
console.log('❌ Error fetching trailer data:', error.message);
}
console.log('\n🏁 Integration test complete!');
}
// Run the test
testTrailerIntegration().catch(console.error);

View file

@ -0,0 +1,137 @@
# 🚀 Deployment Guide
## Netlify Deployment
### Option 1: Deploy via Netlify CLI
1. **Install Netlify CLI:**
```bash
npm install -g netlify-cli
```
2. **Login to Netlify:**
```bash
netlify login
```
3. **Deploy:**
```bash
netlify deploy --prod --dir=.
```
### Option 2: Deploy via GitHub
1. **Push to GitHub:**
```bash
git init
git add .
git commit -m "Initial trailer server"
git remote add origin https://github.com/yourusername/nuvio-trailer-server.git
git push -u origin main
```
2. **Connect to Netlify:**
- Go to [netlify.com](https://netlify.com)
- Click "New site from Git"
- Connect your GitHub repository
- Build settings will be auto-detected from `netlify.toml`
### Option 3: Manual Deploy
1. **Build the functions:**
```bash
npm run build
```
2. **Upload to Netlify:**
- Zip the entire folder
- Upload via Netlify dashboard
## Important Notes
### ⚠️ yt-dlp Limitation
**Netlify Functions don't support yt-dlp by default.** You have a few options:
1. **Use Railway/Render instead** (recommended)
2. **Use a different approach** (see alternatives below)
3. **Custom Netlify build** (complex)
### Alternative Platforms
#### Railway (Recommended)
```bash
# Install Railway CLI
npm install -g @railway/cli
# Login and deploy
railway login
railway init
railway up
```
#### Render
1. Connect GitHub repository
2. Set build command: `npm install`
3. Set start command: `npm start`
4. Deploy
#### Vercel
```bash
# Install Vercel CLI
npm install -g vercel
# Deploy
vercel --prod
```
## Update Your App
After deployment, update your TrailerService:
```typescript
// In src/services/trailerService.ts
private static readonly LOCAL_SERVER_URL = 'https://your-deployed-url.netlify.app/trailer';
```
## Testing Deployment
```bash
# Test health endpoint
curl https://your-deployed-url.netlify.app/health
# Test trailer endpoint
curl "https://your-deployed-url.netlify.app/trailer?youtube_url=https://www.youtube.com/watch?v=dQw4w9WgXcQ&title=Test&year=2023"
```
## Environment Variables
Set these in your deployment platform:
- `NODE_ENV`: `production`
- `PORT`: `3001` (if needed)
## Monitoring
- Check Netlify Functions dashboard for logs
- Monitor function execution time
- Watch for rate limiting issues
## Troubleshooting
### Common Issues:
1. **yt-dlp not found**: Use Railway/Render instead of Netlify
2. **Function timeout**: Increase timeout in platform settings
3. **Rate limiting**: Implement better caching
4. **CORS issues**: Check headers in functions
### Debug Commands:
```bash
# Test locally
npm test
# Check function logs
netlify functions:list
netlify functions:invoke trailer
```

182
trailer-server/README.md Normal file
View file

@ -0,0 +1,182 @@
# Nuvio Trailer Server
A Node.js server that converts YouTube trailer URLs to direct streaming links using yt-dlp.
## Features
- 🎬 Convert YouTube URLs to direct streaming links
- 💾 Intelligent caching (24-hour TTL)
- 🚦 Rate limiting (10 requests/minute per IP)
- 🔒 Security headers with Helmet
- 📊 Health monitoring endpoint
- 🧪 Built-in testing suite
## Prerequisites
- Node.js 16+
- yt-dlp installed on your system
### Install yt-dlp
**macOS:**
```bash
brew install yt-dlp
```
**Linux:**
```bash
pip install yt-dlp
```
**Windows:**
```bash
pip install yt-dlp
```
## Installation
1. **Clone/Navigate to the server directory:**
```bash
cd trailer-server
```
2. **Install dependencies:**
```bash
npm install
```
3. **Start the server:**
```bash
# Development mode (with auto-restart)
npm run dev
# Production mode
npm start
```
The server will start on `http://localhost:3001`
## API Endpoints
### GET /health
Health check endpoint
```bash
curl http://localhost:3001/health
```
### GET /trailer
Get direct streaming URL for a YouTube trailer
**Parameters:**
- `youtube_url` (required): YouTube URL of the trailer
- `title` (optional): Movie/show title
- `year` (optional): Release year
**Example:**
```bash
curl "http://localhost:3001/trailer?youtube_url=https://www.youtube.com/watch?v=example&title=Avengers&year=2019"
```
**Response:**
```json
{
"url": "https://direct-streaming-url.com/video.mp4",
"title": "Avengers",
"year": "2019",
"source": "youtube",
"cached": false,
"timestamp": "2023-12-01T10:00:00.000Z"
}
```
### GET /cache
View cached trailers (for debugging)
### DELETE /cache
Clear all cached trailers
## Testing
Run the test suite:
```bash
npm test
```
This will test:
- Health endpoint
- Trailer fetching
- Cache functionality
- Rate limiting
## Integration with Nuvio App
Update your `TrailerService.ts` to use the local server:
```typescript
// In src/services/trailerService.ts
export class TrailerService {
private static readonly BASE_URL = 'http://localhost:3001/trailer';
static async getTrailerUrl(title: string, year: number): Promise<string | null> {
try {
// You'll need to find the YouTube URL first
const youtubeUrl = await this.findYouTubeTrailer(title, year);
if (!youtubeUrl) return null;
const response = await fetch(
`${this.BASE_URL}?youtube_url=${encodeURIComponent(youtubeUrl)}&title=${encodeURIComponent(title)}&year=${year}`
);
if (!response.ok) return null;
const data = await response.json();
return data.url;
} catch (error) {
logger.error('TrailerService', 'Error fetching trailer:', error);
return null;
}
}
}
```
## Environment Variables
- `PORT`: Server port (default: 3001)
- `NODE_ENV`: Environment (development/production)
## Deployment
### Netlify Functions
1. Create `netlify/functions/trailer.js`
2. Adapt the server code for serverless
3. Deploy to Netlify
### Vercel
1. Create `api/trailer.js`
2. Adapt for Vercel's serverless functions
3. Deploy to Vercel
### Railway/Render
1. Push to GitHub
2. Connect to Railway/Render
3. Set environment variables
4. Deploy
## Troubleshooting
**yt-dlp not found:**
- Ensure yt-dlp is installed and in PATH
- Try: `which yt-dlp` to verify installation
**Rate limited:**
- Wait 1 minute or clear cache
- Check rate limiting settings
**Trailer not found:**
- Verify YouTube URL is valid
- Check if video is available in your region
- Try different quality settings
## License
MIT

View file

@ -0,0 +1,27 @@
[build]
command = "npm install"
functions = "netlify/functions"
publish = "public"
[functions]
node_bundler = "esbuild"
[[redirects]]
from = "/trailer"
to = "/.netlify/functions/trailer"
status = 200
[[redirects]]
from = "/health"
to = "/.netlify/functions/health"
status = 200
[[redirects]]
from = "/cache"
to = "/.netlify/functions/cache"
status = 200
[dev]
command = "npm run dev"
port = 3001
publish = "public"

View file

@ -0,0 +1,44 @@
exports.handler = async (event, context) => {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, DELETE, OPTIONS',
};
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
if (event.httpMethod === 'GET') {
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: 'Cache endpoint available',
timestamp: new Date().toISOString(),
note: 'Cache is managed per function instance in Netlify'
}),
};
}
if (event.httpMethod === 'DELETE') {
return {
statusCode: 200,
headers,
body: JSON.stringify({
message: 'Cache cleared (per function instance)',
timestamp: new Date().toISOString()
}),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: 'Method not allowed' }),
};
};

View file

@ -0,0 +1,26 @@
exports.handler = async (event, context) => {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
};
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
return {
statusCode: 200,
headers,
body: JSON.stringify({
status: 'healthy',
timestamp: new Date().toISOString(),
environment: 'netlify',
function: 'health'
}),
};
};

View file

@ -0,0 +1,148 @@
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
// Simple in-memory cache for Netlify functions
const cache = new Map();
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
exports.handler = async (event, context) => {
// Enable CORS
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
};
// Handle preflight requests
if (event.httpMethod === 'OPTIONS') {
return {
statusCode: 200,
headers,
body: '',
};
}
try {
const { youtube_url, title, year } = event.queryStringParameters || {};
// Validate required parameters
if (!youtube_url) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: 'youtube_url parameter is required'
}),
};
}
// Create cache key
const cacheKey = `trailer_${title}_${year}_${youtube_url}`;
// Check cache first
const cachedResult = cache.get(cacheKey);
if (cachedResult && (Date.now() - cachedResult.timestamp) < CACHE_TTL) {
console.log(`🎯 Cache hit for: ${title} (${year})`);
return {
statusCode: 200,
headers,
body: JSON.stringify(cachedResult.data),
};
}
console.log(`🔍 Fetching trailer for: ${title} (${year})`);
// Use yt-dlp to get direct streaming URL
// Note: yt-dlp needs to be available in the Netlify environment
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 {
statusCode: 404,
headers,
body: JSON.stringify({
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
cache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
console.log(`✅ Successfully fetched trailer for: ${title} (${year})`);
return {
statusCode: 200,
headers,
body: JSON.stringify(result),
};
} catch (error) {
console.error('Error fetching trailer:', error);
if (error.code === 'TIMEOUT') {
return {
statusCode: 408,
headers,
body: JSON.stringify({
error: 'Request timeout - video processing took too long'
}),
};
}
if (error.message.includes('not found') || error.message.includes('unavailable')) {
return {
statusCode: 404,
headers,
body: JSON.stringify({
error: 'Trailer not found'
}),
};
}
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'
}),
};
}
};
// Helper function to validate URLs
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}

1317
trailer-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
{
"name": "nuvio-trailer-server",
"version": "1.0.0",
"description": "Trailer server for Nuvio app using yt-dlp",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "node test.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"rate-limiter-flexible": "^2.4.1",
"node-cache": "^5.1.2",
"node-fetch": "^2.7.0"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"engines": {
"node": ">=16.0.0"
},
"keywords": ["trailer", "yt-dlp", "streaming", "nuvio"],
"author": "Nuvio Team",
"license": "MIT"
}

View file

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nuvio Trailer Server</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.endpoint {
background: rgba(255, 255, 255, 0.2);
padding: 20px;
margin: 20px 0;
border-radius: 10px;
border-left: 4px solid #4CAF50;
}
.method {
font-weight: bold;
color: #4CAF50;
font-size: 1.2em;
}
.url {
font-family: 'Courier New', monospace;
background: rgba(0, 0, 0, 0.3);
padding: 10px;
border-radius: 5px;
margin: 10px 0;
word-break: break-all;
}
.description {
margin-top: 10px;
opacity: 0.9;
}
.status {
text-align: center;
margin: 30px 0;
font-size: 1.5em;
color: #4CAF50;
}
.footer {
text-align: center;
margin-top: 40px;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<h1>🎬 Nuvio Trailer Server</h1>
<div class="status">
✅ Server is running and ready!
</div>
<div class="endpoint">
<div class="method">GET</div>
<div class="url">/health</div>
<div class="description">Health check endpoint to verify server status</div>
</div>
<div class="endpoint">
<div class="method">GET</div>
<div class="url">/trailer?youtube_url=YOUTUBE_URL&title=TITLE&year=YEAR</div>
<div class="description">Convert YouTube trailer URL to direct streaming link using yt-dlp</div>
</div>
<div class="endpoint">
<div class="method">GET</div>
<div class="url">/cache</div>
<div class="description">View cached trailers (for debugging)</div>
</div>
<div class="endpoint">
<div class="method">DELETE</div>
<div class="url">/cache</div>
<div class="description">Clear all cached trailers</div>
</div>
<div class="footer">
<p>🚀 Powered by yt-dlp and Express.js</p>
<p>Built for Nuvio Streaming App</p>
</div>
</div>
</body>
</html>

288
trailer-server/server.js Normal file
View file

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

87
trailer-server/test.js Normal file
View file

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

View file

@ -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<string|null>} - YouTube URL or null if not found
*/
async function searchYouTubeTrailer(query) {
try {
console.log(`🔍 Searching YouTube for: ${query}`);
// Use yt-dlp to search YouTube and get the YouTube URL (not direct streaming URL)
// --get-url returns direct streaming URLs, we need --get-id to get YouTube video ID
const command = `yt-dlp --get-id --no-playlist "ytsearch1:${query}"`;
const { stdout, stderr } = await execAsync(command, {
timeout: 15000, // 15 second timeout
maxBuffer: 1024 * 1024 // 1MB buffer
});
if (stderr && !stderr.includes('WARNING')) {
console.error('yt-dlp search stderr:', stderr);
}
const videoId = stdout.trim();
if (!videoId || videoId.length !== 11) {
console.log(`❌ No valid YouTube video ID found for: ${query}`);
return null;
}
const youtubeUrl = `https://www.youtube.com/watch?v=${videoId}`;
console.log(`✅ Found YouTube URL: ${youtubeUrl}`);
return youtubeUrl;
} catch (error) {
console.error('Error searching YouTube:', error);
return null;
}
}
/**
* Validate if the URL is a valid YouTube URL
* @param {string} url - URL to validate
* @returns {boolean} - True if valid YouTube URL
*/
function isValidYouTubeUrl(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname.includes('youtube.com') || urlObj.hostname.includes('youtu.be');
} catch {
return false;
}
}
module.exports = { searchYouTubeTrailer };