Skip to content

Commit dd95111

Browse files
committed
feat: add direct link to fast download
1 parent 7c2cf0b commit dd95111

4 files changed

Lines changed: 564 additions & 150 deletions

File tree

backend/src/youtube/youtube.controller.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@ import {
66
Body,
77
Param,
88
Res,
9-
Headers,
109
BadRequestException,
1110
} from '@nestjs/common';
1211
import { Response } from 'express';
13-
import { DownloadRequest, DownloadResult, VideoInfo, PlaylistInfo, YouTubeService } from './youtube.service';
12+
import { DownloadRequest, DownloadResult, VideoInfo, PlaylistInfo, DirectLinkResult, YouTubeService } from './youtube.service';
1413

1514

1615
@Controller('youtube')
@@ -39,11 +38,37 @@ export class YouTubeController {
3938
return this.youtubeService.getPlaylistInfo(url);
4039
}
4140

41+
/**
42+
* Get direct download link for low-quality videos or audio
43+
* - Video: 720p and below (combined video+audio streams)
44+
* - Audio: any quality (bestaudio, 128kbps, 192kbps, etc.)
45+
* Saves server bandwidth compared to download-then-serve approach
46+
*/
47+
@Get('direct-link')
48+
async getDirectLink(
49+
@Query('url') url: string,
50+
@Query('formatType') formatType: 'video' | 'audio',
51+
@Query('quality') quality: string,
52+
): Promise<DirectLinkResult> {
53+
if (!url) {
54+
throw new BadRequestException('URL is required');
55+
}
56+
if (!formatType || !['video', 'audio'].includes(formatType)) {
57+
throw new BadRequestException('formatType must be "video" or "audio"');
58+
}
59+
if (!quality) {
60+
throw new BadRequestException('quality is required (e.g., 720p, 360p for video or bestaudio, 128kbps for audio)');
61+
}
62+
return this.youtubeService.getDirectLink(url, formatType, quality);
63+
}
64+
4265
/**
4366
* Start download with selected format
4467
*/
4568
@Post('download')
46-
async startDownload(@Body() request: DownloadRequest): Promise<DownloadResult> {
69+
async startDownload(
70+
@Body() request: DownloadRequest,
71+
): Promise<DownloadResult> {
4772
if (!request.url) {
4873
throw new BadRequestException('URL is required');
4974
}
@@ -53,9 +78,71 @@ export class YouTubeController {
5378
if (!request.quality) {
5479
throw new BadRequestException('quality is required');
5580
}
81+
5682
return this.youtubeService.startDownload(request);
5783
}
5884

85+
/**
86+
* Proxy download endpoint - streams YouTube content with proper download headers
87+
* This ensures the browser downloads the file instead of opening in a new tab
88+
* MUST be placed before :id route to avoid route conflict
89+
*/
90+
@Get('proxy-download')
91+
async proxyDownload(
92+
@Query('url') url: string,
93+
@Query('filename') filename: string,
94+
@Res() res: Response,
95+
): Promise<void> {
96+
if (!url) {
97+
throw new BadRequestException('URL is required');
98+
}
99+
if (!filename) {
100+
throw new BadRequestException('Filename is required');
101+
}
102+
103+
try {
104+
// Fetch the video/audio from YouTube CDN
105+
const response = await fetch(url, {
106+
headers: {
107+
'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',
108+
},
109+
});
110+
111+
if (!response.ok) {
112+
throw new Error(`Failed to fetch: ${response.status}`);
113+
}
114+
115+
// Set proper headers for download
116+
const contentType = response.headers.get('content-type') || 'application/octet-stream';
117+
const contentLength = response.headers.get('content-length');
118+
119+
res.setHeader('Content-Type', contentType);
120+
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
121+
if (contentLength) {
122+
res.setHeader('Content-Length', contentLength);
123+
}
124+
125+
// Pipe the stream directly to response (no storage)
126+
const reader = response.body?.getReader();
127+
if (!reader) {
128+
throw new Error('Failed to get reader');
129+
}
130+
131+
const pump = async () => {
132+
while (true) {
133+
const { done, value } = await reader.read();
134+
if (done) break;
135+
res.write(value);
136+
}
137+
res.end();
138+
};
139+
140+
pump().catch(() => res.end());
141+
} catch (error) {
142+
res.status(500).json({ error: 'Failed to download' });
143+
}
144+
}
145+
59146
/**
60147
* Get download status
61148
*/

0 commit comments

Comments
 (0)