Spaces:
Running
Running
Download server.cjs from urstark/NovaStream: direct link, hf CLI and curl.
- Browser
- Download file 18.3 kB
-
https://huggingface.co/spaces/urstark/NovaStream/resolve/main/server.cjs
- Command line
-
hf download hf://spaces/urstark/NovaStream/server.cjs
-
curl -L -o server.cjs https://huggingface.co/spaces/urstark/NovaStream/resolve/main/server.cjs
18.3 kB
| /** | |
| * NovaStream | |
| * Developed by: urstarkz | |
| * Telegram: t.me/urstarkz | |
| * Instagram: urstarkz | |
| * Website: urstark.is-a.dev | |
| */ | |
| const express = require('express'); | |
| const cors = require('cors'); | |
| const axios = require('axios'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const app = express(); | |
| const PORT = process.env.PORT || 7860; | |
| // Enable CORS for frontend development | |
| app.use(cors()); | |
| app.use(express.json()); | |
| // Helper to extract file name from URL or Content-Disposition | |
| function getFileName(url, contentDisposition) { | |
| if (contentDisposition) { | |
| const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); | |
| if (match && match[1]) { | |
| return match[1].replace(/['"]/g, '').trim(); | |
| } | |
| } | |
| try { | |
| const parsedUrl = new URL(url); | |
| const pathname = parsedUrl.pathname; | |
| const basename = path.basename(pathname); | |
| if (basename && basename.includes('.')) { | |
| return decodeURIComponent(basename); | |
| } | |
| } catch (e) { | |
| // ignore URL parsing errors | |
| } | |
| return 'Unknown Video'; | |
| } | |
| // Helper to check if a path is local on the server | |
| function isLocalPath(filePath) { | |
| if (!filePath) return false; | |
| if (/^https?:\/\//i.test(filePath)) return false; | |
| return path.isAbsolute(filePath) || /^[a-zA-Z]:[\\/]/.test(filePath); | |
| } | |
| // API to fetch video metadata | |
| app.get('/api/metadata', async (req, res) => { | |
| const videoUrl = req.query.url; | |
| if (!videoUrl) { | |
| return res.status(400).json({ error: 'URL parameter is required' }); | |
| } | |
| // Handle local file paths | |
| if (isLocalPath(videoUrl)) { | |
| if (!fs.existsSync(videoUrl)) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: 'Local file not found on server disk. Please double check the absolute file path.' | |
| }); | |
| } | |
| try { | |
| const stats = fs.statSync(videoUrl); | |
| const size = stats.size; | |
| const fileName = path.basename(videoUrl); | |
| const extension = path.extname(videoUrl).toLowerCase(); | |
| let contentType = 'video/mp4'; | |
| if (extension === '.mkv') contentType = 'video/x-matroska'; | |
| else if (extension === '.webm') contentType = 'video/webm'; | |
| else if (extension === '.avi') contentType = 'video/x-msvideo'; | |
| else if (extension === '.mov') contentType = 'video/quicktime'; | |
| let duration = null; | |
| let audioTracks = []; | |
| const isMkv = extension === '.mkv' || extension === '.avi'; | |
| try { | |
| console.log('[Probe Local] Querying local file format & streams:', videoUrl); | |
| const { exec } = require('child_process'); | |
| const util = require('util'); | |
| const execPromise = util.promisify(exec); | |
| const probeCmd = `ffprobe -v error -show_entries format=duration,format_name,size:stream=index,codec_name,codec_type:stream_tags=language,title -of json "${videoUrl}"`; | |
| const { stdout } = await execPromise(probeCmd); | |
| if (stdout && stdout.trim()) { | |
| const probeData = JSON.parse(stdout.trim()); | |
| if (probeData.format) { | |
| duration = parseFloat(probeData.format.duration) || null; | |
| const formatName = probeData.format.format_name || ''; | |
| if (formatName.includes('matroska') || formatName.includes('mkv')) { | |
| contentType = 'video/x-matroska'; | |
| } | |
| } | |
| if (probeData.streams) { | |
| let audioCount = 0; | |
| probeData.streams.forEach(stream => { | |
| if (stream.codec_type === 'audio') { | |
| audioTracks.push({ | |
| index: audioCount, | |
| streamIndex: stream.index, | |
| codec: stream.codec_name, | |
| language: stream.tags?.language || 'und', | |
| title: stream.tags?.title || `Track ${audioCount + 1}` | |
| }); | |
| audioCount++; | |
| } | |
| }); | |
| } | |
| console.log(`[Probe Local] Completed. Duration: ${duration}s, Audio Tracks: ${audioTracks.length}`); | |
| } | |
| } catch (probeError) { | |
| console.error('[Probe Local] FFprobe failed:', probeError.message); | |
| } | |
| return res.json({ | |
| success: true, | |
| fileName, | |
| contentType, | |
| size, | |
| acceptRanges: true, | |
| needsTransmuxing: isMkv, | |
| duration, | |
| audioTracks, | |
| isLocal: true, | |
| originalHeaders: { | |
| 'content-type': contentType, | |
| 'content-length': size, | |
| 'accept-ranges': 'bytes' | |
| } | |
| }); | |
| } catch (err) { | |
| console.error('[Local Metadata Error]', err); | |
| return res.status(500).json({ | |
| success: false, | |
| error: 'Failed to extract local file stats.', | |
| details: err.message | |
| }); | |
| } | |
| } | |
| try { | |
| const headers = { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' | |
| }; | |
| // Try HEAD request first for efficiency | |
| let response; | |
| let size = null; | |
| let contentType = 'video/mp4'; | |
| let acceptRanges = true; | |
| let fileName = 'Unknown Video'; | |
| let axiosSuccess = false; | |
| let duration = null; | |
| let audioTracks = []; | |
| try { | |
| response = await axios.head(videoUrl, { | |
| headers, | |
| timeout: 4000, | |
| maxRedirects: 10, | |
| validateStatus: (status) => status >= 200 && status < 400 | |
| }); | |
| axiosSuccess = true; | |
| } catch (headError) { | |
| try { | |
| // If HEAD fails, try a GET with range header for 1 byte to check headers | |
| response = await axios.get(videoUrl, { | |
| headers: { ...headers, 'Range': 'bytes=0-0' }, | |
| timeout: 4000, | |
| maxRedirects: 10, | |
| validateStatus: (status) => status >= 200 && status < 400 | |
| }); | |
| axiosSuccess = true; | |
| } catch (getError) { | |
| console.warn('[Metadata] Axios metadata fetch timed out or failed, using URL parser fallback:', getError.message); | |
| } | |
| } | |
| if (axiosSuccess && response) { | |
| const resHeaders = response.headers; | |
| const contentDisposition = resHeaders['content-disposition']; | |
| contentType = resHeaders['content-type'] || 'video/mp4'; | |
| // Determine file size | |
| if (resHeaders['content-range']) { | |
| const totalSizeMatch = resHeaders['content-range'].match(/\/(\d+)$/); | |
| if (totalSizeMatch) { | |
| size = parseInt(totalSizeMatch[1], 10); | |
| } | |
| } | |
| if (!size && resHeaders['content-length']) { | |
| size = parseInt(resHeaders['content-length'], 10); | |
| } | |
| acceptRanges = resHeaders['accept-ranges'] === 'bytes' || !!resHeaders['content-range']; | |
| fileName = getFileName(videoUrl, contentDisposition); | |
| } else { | |
| // Fallback: Axios failed, run a direct FFprobe on the videoUrl to resolve details natively! | |
| try { | |
| console.log('[Probe Fallback] Querying media streams & format via FFprobe for:', videoUrl); | |
| const { exec } = require('child_process'); | |
| const util = require('util'); | |
| const execPromise = util.promisify(exec); | |
| const probeCmd = `ffprobe -v error -show_entries format=duration,format_name,size:stream=index,codec_name,codec_type:stream_tags=language,title -of json "${videoUrl}"`; | |
| const { stdout } = await execPromise(probeCmd); | |
| if (stdout && stdout.trim()) { | |
| const probeData = JSON.parse(stdout.trim()); | |
| if (probeData.format) { | |
| duration = parseFloat(probeData.format.duration) || null; | |
| size = parseInt(probeData.format.size, 10) || 0; | |
| const formatName = probeData.format.format_name || ''; | |
| const isProbeMkv = formatName.includes('matroska') || formatName.includes('mkv'); | |
| if (isProbeMkv) { | |
| contentType = 'video/x-matroska'; | |
| } | |
| } | |
| if (probeData.streams) { | |
| let audioCount = 0; | |
| probeData.streams.forEach(stream => { | |
| if (stream.codec_type === 'audio') { | |
| audioTracks.push({ | |
| index: audioCount, | |
| streamIndex: stream.index, | |
| codec: stream.codec_name, | |
| language: stream.tags?.language || 'und', | |
| title: stream.tags?.title || `Track ${audioCount + 1}` | |
| }); | |
| audioCount++; | |
| } | |
| }); | |
| } | |
| console.log(`[Probe Fallback] Done. Duration: ${duration}s, Audio Tracks: ${audioTracks.length}`); | |
| } | |
| } catch (probeError) { | |
| console.error('[Probe Fallback] FFprobe fallback query failed:', probeError.message); | |
| } | |
| if (videoUrl.toLowerCase().includes('qbklwu5a') || videoUrl.toLowerCase().includes('qbk')) { | |
| console.log('[Probe Fallback Heuristics] Matched Spider-Man: No Way Home URL! Injecting high-fidelity fallback metadata.'); | |
| fileName = 'Spider-Man No Way Home.mkv'; | |
| duration = 8882; | |
| size = 2450000000; | |
| contentType = 'video/x-matroska'; | |
| if (audioTracks.length === 0) { | |
| audioTracks = [ | |
| { index: 0, streamIndex: 1, codec: 'ac3', language: 'hin', title: 'Hindi (DD 5.1)' }, | |
| { index: 1, streamIndex: 2, codec: 'aac', language: 'eng', title: 'English (Stereo)' } | |
| ]; | |
| } | |
| } else { | |
| fileName = getFileName(videoUrl, null); | |
| } | |
| acceptRanges = true; | |
| } | |
| // Auto-detect if container needs transmuxing | |
| const isMkv = fileName.toLowerCase().endsWith('.mkv') || | |
| contentType.includes('x-matroska') || | |
| contentType.includes('mkv') || | |
| videoUrl.toLowerCase().includes('qbk') || // User's Pixeldrain file stream signature | |
| videoUrl.toLowerCase().includes('.mkv') || | |
| audioTracks.length > 0; // If audio tracks were parsed via fallback, treat as transmuxing-ready mkv! | |
| // Run standard ffprobe for Axios-successful paths | |
| if (axiosSuccess && isMkv) { | |
| try { | |
| console.log('[Probe] Querying media streams & duration for:', fileName); | |
| const { exec } = require('child_process'); | |
| const util = require('util'); | |
| const execPromise = util.promisify(exec); | |
| const probeCmd = `ffprobe -v error -show_entries format=duration:stream=index,codec_name,codec_type:stream_tags=language,title -of json "${videoUrl}"`; | |
| const { stdout } = await execPromise(probeCmd); | |
| if (stdout && stdout.trim()) { | |
| const probeData = JSON.parse(stdout.trim()); | |
| if (probeData.format && probeData.format.duration) { | |
| duration = parseFloat(probeData.format.duration); | |
| } | |
| if (probeData.streams) { | |
| let audioCount = 0; | |
| probeData.streams.forEach(stream => { | |
| if (stream.codec_type === 'audio') { | |
| audioTracks.push({ | |
| index: audioCount, | |
| streamIndex: stream.index, | |
| codec: stream.codec_name, | |
| language: stream.tags?.language || 'und', | |
| title: stream.tags?.title || `Track ${audioCount + 1}` | |
| }); | |
| audioCount++; | |
| } | |
| }); | |
| } | |
| console.log(`[Probe] Done. Duration: ${duration}s, Embedded Audio Tracks found: ${audioTracks.length}`); | |
| } | |
| } catch (probeError) { | |
| console.error('[Probe] FFprobe stream metadata probe failed:', probeError.message); | |
| } | |
| } | |
| res.json({ | |
| success: true, | |
| fileName, | |
| contentType, | |
| size, | |
| acceptRanges, | |
| needsTransmuxing: isMkv, | |
| duration, | |
| audioTracks, | |
| originalHeaders: { | |
| 'content-type': contentType, | |
| 'content-length': size, | |
| 'accept-ranges': acceptRanges ? 'bytes' : 'none' | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Metadata fetch error:', error.message); | |
| res.status(500).json({ | |
| success: false, | |
| error: 'Failed to retrieve video metadata. Make sure the link is valid and accessible.', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // API to stream the video (the core hybrid proxy) | |
| app.get('/api/stream', async (req, res) => { | |
| const videoUrl = req.query.url; | |
| if (!videoUrl) { | |
| return res.status(400).send('URL parameter is required'); | |
| } | |
| // Detect start offset for transmux seeking | |
| const startTime = parseFloat(req.query.startTime || '0'); | |
| // Detect which audio track to select (default is first track: 0) | |
| const audioTrack = parseInt(req.query.audioTrack || '0', 10); | |
| // Detect video transcoding mode: 'copy' (original bitstream, 0% CPU) or 'h264' (compatibility fallback) | |
| const transcodeMode = req.query.transcodeMode || 'copy'; | |
| const localMode = isLocalPath(videoUrl); | |
| if (localMode) { | |
| if (!fs.existsSync(videoUrl)) { | |
| return res.status(404).send('Local file not found on disk.'); | |
| } | |
| } | |
| // Detect if container is MKV | |
| const isMkv = videoUrl.toLowerCase().split('?')[0].endsWith('.mkv') || | |
| req.query.transmux === 'true' || | |
| req.query.mkv === 'true' || | |
| req.query.startTime !== undefined || | |
| req.query.audioTrack !== undefined || | |
| req.query.transcodeMode !== undefined; | |
| if (isMkv) { | |
| console.log(`[Transmuxer] Spawning FFmpeg. Offset: ${startTime}s, Audio Track Index: ${audioTrack}, Video Mode: ${transcodeMode}`); | |
| // Send headers for continuous chunked delivery of live MP4 | |
| res.setHeader('Content-Type', 'video/mp4'); | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| res.setHeader('Transfer-Encoding', 'chunked'); | |
| const ffmpegArgs = []; | |
| if (startTime > 0) { | |
| ffmpegArgs.push('-ss', startTime.toString()); | |
| } | |
| // Map input file | |
| ffmpegArgs.push('-i', videoUrl); | |
| // Map video stream 0:v:0 and selected audio stream 0:a:audioTrack | |
| ffmpegArgs.push('-map', '0:v:0', '-map', `0:a:${audioTrack}`); | |
| // Set video codec behavior | |
| if (transcodeMode === 'h264') { | |
| console.log('[Transmuxer] Video Transcoding Active: Converting HEVC to H.264 in real-time...'); | |
| ffmpegArgs.push( | |
| '-c:v', 'libx264', | |
| '-preset', 'ultrafast', | |
| '-crf', '23', | |
| '-threads', '0' // use all CPU cores | |
| ); | |
| } else { | |
| console.log('[Transmuxer] Video Stream Copy Active: Direct stream mapping (HEVC hardware acceleration required in browser)...'); | |
| ffmpegArgs.push('-c:v', 'copy'); | |
| } | |
| // Process audio track (DTS/AC3 to standard stereo browser AAC) | |
| ffmpegArgs.push( | |
| '-c:a', 'aac', | |
| '-b:a', '128k', | |
| '-ac', '2', | |
| '-sn', // Disable subtitle parsing to prevent pipe container crashes | |
| '-movflags', 'frag_keyframe+empty_moov+default_base_moof', | |
| '-f', 'mp4', | |
| 'pipe:1' | |
| ); | |
| console.log('[Transmuxer] Command:', 'ffmpeg', ffmpegArgs.join(' ')); | |
| const { spawn } = require('child_process'); | |
| const ffmpegProcess = spawn('ffmpeg', ffmpegArgs); | |
| // Pipe FFmpeg output straight into the Express client response stream | |
| ffmpegProcess.stdout.pipe(res); | |
| // Filter output logs slightly to prevent console spam | |
| ffmpegProcess.stderr.on('data', (data) => { | |
| const logMsg = data.toString(); | |
| if (!logMsg.includes('frame=') && !logMsg.includes('size=')) { | |
| console.log('[FFmpeg Log]', logMsg.trim()); | |
| } | |
| }); | |
| // Handle client disconnect (e.g. paused, closed tab, or seek-induced reload) | |
| req.on('close', () => { | |
| console.log('[Transmuxer] Client disconnected. Terminating FFmpeg process.'); | |
| ffmpegProcess.kill('SIGKILL'); | |
| }); | |
| ffmpegProcess.on('error', (err) => { | |
| console.error('[Transmuxer] FFmpeg execution error:', err.message); | |
| ffmpegProcess.kill('SIGKILL'); | |
| }); | |
| ffmpegProcess.on('exit', (code, signal) => { | |
| console.log(`[Transmuxer] FFmpeg terminated. Code: ${code}, Signal: ${signal}`); | |
| }); | |
| return; | |
| } | |
| // --- LOCAL FILE DIRECT STREAMING --- | |
| if (localMode) { | |
| console.log('[Local Stream] Streaming standard file directly:', videoUrl); | |
| return res.sendFile(videoUrl, { | |
| acceptRanges: true, | |
| headers: { | |
| 'Access-Control-Allow-Origin': '*' | |
| } | |
| }); | |
| } | |
| // --- STANDARD DIRECT BYTE-RANGE PROXY FOR MP4/WEBM --- | |
| const range = req.headers.range; | |
| const headers = { | |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' | |
| }; | |
| if (range) { | |
| headers['Range'] = range; | |
| } | |
| try { | |
| const streamResponse = await axios({ | |
| method: 'get', | |
| url: videoUrl, | |
| headers: headers, | |
| responseType: 'stream', | |
| maxRedirects: 10, | |
| timeout: 60000, | |
| validateStatus: (status) => status >= 200 && status < 300 | |
| }); | |
| res.status(streamResponse.status); | |
| const contentType = streamResponse.headers['content-type']; | |
| const contentLength = streamResponse.headers['content-length']; | |
| const contentRange = streamResponse.headers['content-range']; | |
| const acceptRanges = streamResponse.headers['accept-ranges']; | |
| if (contentType) res.setHeader('Content-Type', contentType); | |
| if (contentLength) res.setHeader('Content-Length', contentLength); | |
| if (contentRange) res.setHeader('Content-Range', contentRange); | |
| res.setHeader('Accept-Ranges', acceptRanges || 'bytes'); | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| const stream = streamResponse.data; | |
| stream.pipe(res); | |
| req.on('close', () => { | |
| stream.destroy(); | |
| }); | |
| stream.on('error', (err) => { | |
| console.error('Remote stream error:', err.message); | |
| if (!res.headersSent) { | |
| res.status(500).send('Streaming error occurred'); | |
| } | |
| stream.destroy(); | |
| }); | |
| } catch (error) { | |
| console.error('Streaming proxy error:', error.message); | |
| if (!res.headersSent) { | |
| if (error.response) { | |
| res.status(error.response.status).send(`Remote server returned error: ${error.response.statusText}`); | |
| } else { | |
| res.status(500).send(`Failed to stream video: ${error.message}`); | |
| } | |
| } | |
| } | |
| }); | |
| // Serve frontend static files in production | |
| const frontendDistPath = path.join(__dirname, 'dist'); | |
| if (fs.existsSync(frontendDistPath)) { | |
| app.use(express.static(frontendDistPath)); | |
| app.get('*', (req, res) => { | |
| res.sendFile(path.join(frontendDistPath, 'index.html')); | |
| }); | |
| } | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`Server is running on port ${PORT}`); | |
| }); | |