nexstream-backend / backend /src /controllers /video.controller.ts
nexstream-deploy
deploy: nexstream backend (experiment)
f3fa747
Raw
History Blame Contribute Delete
10.7 kB
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; // skipcq: JS-C1003
import {
addClient,
removeClient,
sendEvent,
} from '../utils/network/sse.util.js';
import {
isSupportedUrl,
isValidSpotifyUrl,
decodeUrlIfNeeded,
} from '../utils/network/validation.util.js';
import { pipeWebStream } from '../utils/network/proxy.util.js';
import { verifyProxyParams } from '../utils/network/secrets.util.js';
import { recordFailure } from '../utils/infra/metrics.util.js';
import {
detectService,
getSanitizedFilename,
} from '../utils/media/video.util.js';
import { prepareFinalResponse } from '../utils/api/response.util.js';
import { getCookieArgs, initializeSession } from '../utils/api/controller.util.js';
import {
processBackgroundTracks,
resolveSeedTracks,
type SeedTrack,
} from '../services/seeder.service.js';
import {
fetchMediaInfo,
handleSpotifyRegistry,
} from '../services/video/info.js';
import {
parseRequestParams,
resolveManifests,
buildStreamResponse,
} from '../services/video/stream-resolver.js';
import {
executeDownload,
streamViaYtdlp,
} from '../services/video/download.js';
import { SpotifyMetadata } from '../types/index.js';
export const streamEvents = async (
req: Request,
res: Response
): Promise<void> => {
const id = (req.query.id as string) || undefined;
console.log(`[SSE] Client connecting: ${id}`);
if (!id) {
res.status(400).end();
return;
}
await addClient(id, res);
};
export const getVideoInformation = async (
req: Request,
res: Response
): Promise<void> => {
res.setHeader(
'Cache-Control',
'no-store, no-cache, must-revalidate, proxy-revalidate'
);
let videoURL = decodeUrlIfNeeded(req.query.url as string);
const clientId = (req.query.id as string) || undefined;
if (videoURL) {
videoURL = videoURL.split('&id=')[0].split('?id=')[0];
}
if (!videoURL || !isSupportedUrl(videoURL)) {
res.status(400).json({ error: 'No valid URL provided' });
return;
}
const serviceName = detectService(videoURL);
await initializeSession(clientId);
try {
const cookieArgs = await getCookieArgs(videoURL, clientId);
const isSpotify = videoURL.includes('spotify.com');
const info = await fetchMediaInfo(
videoURL,
clientId,
serviceName,
cookieArgs
);
// fast partial hit
if (!info || (!info.formats?.length && !info.isPartial)) {
res.json({
title: info?.title || 'Unknown',
thumbnail: info?.thumbnail || '',
formats: [],
audioFormats: [],
});
return;
}
const spotifyData = isSpotify
? ({ ...info, type: 'spotify' } as unknown as SpotifyMetadata)
: null;
const targetURL = isSpotify ? info.targetUrl || videoURL : videoURL;
const finalResponse = await prepareFinalResponse(
info,
isSpotify,
spotifyData,
videoURL
);
if (isSpotify) {
handleSpotifyRegistry(info, finalResponse, videoURL, targetURL);
}
res.json(finalResponse);
} catch (error: unknown) {
recordFailure('info');
console.error('[VideoInfo] Error:', (error as Error).message);
Sentry.captureException(error);
if (clientId) removeClient(clientId);
if (!res.headersSent)
res.status(500).json({ error: 'Failed to fetch video info' });
}
};
export const getStreamUrls = async (
req: Request,
res: Response
): Promise<void> => {
const { videoURL, clientId, formatId } = parseRequestParams(req);
const timestamp = new Date().toLocaleTimeString('en-US', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
});
if (!videoURL || !isSupportedUrl(videoURL)) {
res.status(400).json({ error: 'No valid URL provided' });
return;
}
console.log(`[${timestamp}] [EME] Resolving manifests for Edge Muxing...`);
try {
const {
info,
videoTunnel,
audioTunnel,
isAudioOnly,
filename,
totalSize,
outputMeta,
} = await resolveManifests(req, videoURL, clientId, formatId);
res.json(
buildStreamResponse(
info,
videoTunnel,
audioTunnel,
isAudioOnly,
filename,
totalSize,
outputMeta as Record<string, unknown>
)
);
} catch (error: unknown) {
recordFailure('stream_urls');
console.error('[StreamUrls] Error:', (error as Error).message);
Sentry.captureException(error);
if (!res.headersSent)
res.status(500).json({ error: 'Failed to resolve stream URLs' });
}
};
export const proxyStream = async (
req: Request,
res: Response
): Promise<void> => {
const queryData = req.query as Record<string, string | string[] | undefined>;
let { targetUrl, formatId, url: rawFallbackUrl, filename } = queryData;
if (Array.isArray(targetUrl)) targetUrl = targetUrl[0];
if (Array.isArray(formatId)) formatId = formatId[0];
if (Array.isArray(rawFallbackUrl)) rawFallbackUrl = rawFallbackUrl[0];
if (Array.isArray(filename)) filename = filename[0];
let rawUrl = queryData.rawUrl;
if (Array.isArray(rawUrl)) rawUrl = rawUrl[0];
// refuse forged or expired signed links
if (
!verifyProxyParams({
targetUrl: targetUrl as string | undefined,
rawUrl: rawUrl as string | undefined,
formatId: formatId as string | undefined,
exp: Number(req.query.exp),
sig: req.query.sig as string | undefined,
})
) {
res.status(403).json({ error: 'Invalid or expired proxy signature' });
return;
}
const urlToFetch = rawFallbackUrl || (req.query.rawUrl as string);
if (urlToFetch) {
const abortController = new AbortController();
req.on('close', () => abortController.abort());
try {
await pipeWebStream(
urlToFetch,
res,
filename as string,
req.headers as unknown as Record<string, string | undefined>,
0,
abortController.signal
);
return;
} catch (error: unknown) {
recordFailure('proxy');
console.error('[Proxy] Raw Pipe Error:', (error as Error).message);
Sentry.captureException(error);
if (!res.headersSent)
res.status(500).json({ error: 'Proxy fetch failed' });
res.end();
return;
}
}
if (targetUrl && formatId) {
await streamViaYtdlp(req, res, targetUrl, formatId, filename);
return;
}
res.status(400).end();
};
export const reportTelemetry = (req: Request, res: Response): void => {
const { event } = req.body;
const timestamp = new Date().toLocaleTimeString('en-US', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
});
const safeEvent = String(event || 'unknown').replaceAll(/[^\w]/gu, '_');
console.log(`[${timestamp}] [EME] Client-Side Handshake: ${safeEvent}`);
res.status(204).end();
};
export const convertVideo = (req: Request, res: Response): void => {
if (req.method === 'HEAD') {
res.status(200).end();
return;
}
res.setHeader(
'Cache-Control',
'no-store, no-cache, must-revalidate, proxy-revalidate'
);
const requestData = { ...req.query, ...req.body } as {
url?: string;
id?: string;
format?: string;
formatId?: string;
token?: string;
title?: string;
artist?: string;
targetUrl?: string;
};
const {
url: videoURL,
id: clientIdStr,
format = 'mp4',
formatId,
} = requestData;
const clientId = clientIdStr || undefined;
console.log(`[Convert] Request received for: ${videoURL} (ID: ${clientId})`);
if (!videoURL || !isSupportedUrl(videoURL)) {
console.warn(`[Convert] Invalid URL: ${videoURL}`);
res.status(400).json({ error: 'No valid URL provided' });
return;
}
const token = requestData.token || clientId;
if (token) {
res.setHeader('Set-Cookie', `download_token=${token}; Path=/; Max-Age=60`);
}
const isSpotifyRequest = videoURL.includes('spotify.com');
const timestamp = new Date().toLocaleTimeString('en-US', {
hour12: true,
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
});
let finalFormat = format;
if (formatId?.startsWith('photo')) finalFormat = 'jpg';
const filename = getSanitizedFilename(
requestData.title || 'video',
requestData.artist,
finalFormat,
isSpotifyRequest
);
console.log(
`[${timestamp}] [Turbo] Starting Server-Side muxing for: ${filename}`
);
if (clientId) {
sendEvent(clientId, {
status: 'initializing',
progress: 5,
subStatus: 'syncing core...',
text: 'initiating jump',
});
}
(async () => {
const result = await executeDownload(
res,
clientId,
videoURL as string,
requestData,
timestamp,
filename,
format,
formatId,
isSpotifyRequest
);
if (result) {
const { videoProcess, hardTimeoutId } = result;
req.on('close', () => {
clearTimeout(hardTimeoutId);
if (typeof videoProcess.kill === 'function') {
console.log(
`[${timestamp}] [Turbo] Client disconnected. Cleaning up stream for: ${clientId}`
);
videoProcess.kill();
}
if (!res.writableEnded) res.end();
});
}
})().catch((error) => {
console.error(
`[${timestamp}] [ConvertVideo] Unhandled exception in download workflow:`,
error
);
Sentry.captureException(error);
if (!res.headersSent)
res
.status(500)
.json({ error: 'Internal server error during processing' });
if (!res.writableEnded) res.end();
});
};
export const seedIntelligence = async (
req: Request,
res: Response
): Promise<void> => {
const { url, id: clientIdStr } = req.query as { url: string; id: string };
const clientId = clientIdStr || 'admin-seeder';
if (!url || !isValidSpotifyUrl(url)) {
res
.status(400)
.json({ error: 'Invalid Spotify Artist/Album URL provided' });
return;
}
try {
const tracks = await resolveSeedTracks(url);
if (!tracks || tracks.length === 0) throw new Error('No tracks found.');
res.json({
message: 'Intelligence Gathering Started in Background',
trackCount: tracks.length,
target: url,
});
processBackgroundTracks(tracks as SeedTrack[], clientId).catch(
(error: Error) => {
console.error('[Seeder] Background Process Crashed:', error.message);
Sentry.captureException(error);
}
);
} catch (error: unknown) {
if (!res.headersSent) {
Sentry.captureException(error);
res
.status(500)
.json({
error: (error as Error).message || 'An unknown error occurred',
});
}
}
};