flux-dl 1.0.3 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/VideoDownloader.js +54 -11
- package/src/platforms/youtube.js +16 -2
package/package.json
CHANGED
package/src/VideoDownloader.js
CHANGED
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const platforms = require('./platforms');
|
|
5
5
|
const VideoEncryption = require('./utils/encryption');
|
|
6
6
|
const RequestSpoofing = require('./utils/requestSpoofing');
|
|
7
|
+
const BrowserEmulation = require('./utils/browserEmulation');
|
|
7
8
|
|
|
8
9
|
class VideoDownloader {
|
|
9
10
|
constructor(options = {}) {
|
|
@@ -11,11 +12,18 @@ class VideoDownloader {
|
|
|
11
12
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
12
13
|
timeout: 30000,
|
|
13
14
|
encryptionKey: options.encryptionKey || 'default-key',
|
|
15
|
+
cookiesFile: options.cookiesFile,
|
|
14
16
|
...options
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
this.encryption = new VideoEncryption(this.options.encryptionKey);
|
|
18
20
|
this.spoofing = new RequestSpoofing();
|
|
21
|
+
this.browser = new BrowserEmulation();
|
|
22
|
+
|
|
23
|
+
// Load cookies if provided
|
|
24
|
+
if (this.options.cookiesFile) {
|
|
25
|
+
this.browser.cookieManager.loadFromFile(this.options.cookiesFile);
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
async getVideoInfo(url) {
|
|
@@ -72,31 +80,36 @@ class VideoDownloader {
|
|
|
72
80
|
async downloadAudio(url, outputPath = './downloads') {
|
|
73
81
|
const info = await this.getVideoInfo(url);
|
|
74
82
|
|
|
75
|
-
// Audio-
|
|
76
|
-
|
|
83
|
+
// WICHTIG: Audio-only Formate bekommen 403!
|
|
84
|
+
// Stattdessen: Nutze Format mit Video+Audio (funktioniert ohne 403)
|
|
85
|
+
// Das ist info.videoUrl - das beste Format mit beiden
|
|
77
86
|
|
|
78
|
-
if (!
|
|
79
|
-
throw new Error('No
|
|
87
|
+
if (!info.videoUrl) {
|
|
88
|
+
throw new Error('No download URL available');
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
if (!fs.existsSync(outputPath)) {
|
|
83
92
|
fs.mkdirSync(outputPath, { recursive: true });
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
const filename = this.sanitizeFilename(info.title) +
|
|
95
|
+
// Immer .mp3 für Audio
|
|
96
|
+
const filename = this.sanitizeFilename(info.title) + '.mp3';
|
|
88
97
|
const filepath = path.join(outputPath, filename);
|
|
89
98
|
|
|
90
99
|
console.log(`Downloading audio: ${info.title}`);
|
|
91
|
-
console.log(`
|
|
100
|
+
console.log(`Quality: ${info.quality}`);
|
|
92
101
|
|
|
93
|
-
// Nutze
|
|
102
|
+
// Nutze Browser Emulation mit Cookies
|
|
94
103
|
const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
|
|
95
104
|
const writer = fs.createWriteStream(filepath);
|
|
96
105
|
|
|
97
106
|
let bytesReceived = 0;
|
|
98
|
-
const stream = await this.
|
|
99
|
-
|
|
107
|
+
const stream = await this.browser.downloadStream(info.videoUrl, referer, (percent, downloaded, total) => {
|
|
108
|
+
if (total) {
|
|
109
|
+
process.stdout.write(`\rProgress: ${percent}% (${Math.round(downloaded / 1024 / 1024)}MB / ${Math.round(total / 1024 / 1024)}MB)`);
|
|
110
|
+
} else {
|
|
111
|
+
process.stdout.write(`\rProgress: ${percent}%`);
|
|
112
|
+
}
|
|
100
113
|
});
|
|
101
114
|
|
|
102
115
|
// Track bytes to detect 403 with empty response
|
|
@@ -121,7 +134,37 @@ class VideoDownloader {
|
|
|
121
134
|
|
|
122
135
|
console.log('\n');
|
|
123
136
|
|
|
124
|
-
return { filepath, info
|
|
137
|
+
return { filepath, info };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get audio stream without saving to file
|
|
142
|
+
* Returns a readable stream that can be piped or consumed directly
|
|
143
|
+
* @param {string} url - Video URL
|
|
144
|
+
* @param {function} onProgress - Optional progress callback (percent, downloaded, total)
|
|
145
|
+
* @returns {Promise<{stream: ReadableStream, info: Object}>}
|
|
146
|
+
*/
|
|
147
|
+
async getAudioStream(url, onProgress = null) {
|
|
148
|
+
const info = await this.getVideoInfo(url);
|
|
149
|
+
|
|
150
|
+
if (!info.videoUrl) {
|
|
151
|
+
throw new Error('No download URL available');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(`Streaming audio: ${info.title}`);
|
|
155
|
+
console.log(`Quality: ${info.quality}`);
|
|
156
|
+
|
|
157
|
+
// Nutze Browser Emulation mit Cookies
|
|
158
|
+
const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
|
|
159
|
+
|
|
160
|
+
const stream = await this.browser.downloadStream(info.videoUrl, referer, onProgress);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
stream,
|
|
164
|
+
info,
|
|
165
|
+
contentType: 'audio/mpeg',
|
|
166
|
+
filename: this.sanitizeFilename(info.title) + '.mp3'
|
|
167
|
+
};
|
|
125
168
|
}
|
|
126
169
|
|
|
127
170
|
selectAudioFormat(formats) {
|
package/src/platforms/youtube.js
CHANGED
|
@@ -43,13 +43,20 @@ module.exports = {
|
|
|
43
43
|
|
|
44
44
|
const bestFormat = this.selectBestFormat(formatsWithUrl);
|
|
45
45
|
|
|
46
|
+
// Get highest quality thumbnail (last one in array)
|
|
47
|
+
const thumbnails = data.videoDetails.thumbnail.thumbnails;
|
|
48
|
+
const bestThumbnail = thumbnails[thumbnails.length - 1].url;
|
|
49
|
+
|
|
46
50
|
return {
|
|
47
51
|
title: data.videoDetails.title,
|
|
48
52
|
videoId: videoId,
|
|
49
53
|
duration: parseInt(data.videoDetails.lengthSeconds),
|
|
50
|
-
thumbnail:
|
|
54
|
+
thumbnail: bestThumbnail,
|
|
51
55
|
author: data.videoDetails.author,
|
|
56
|
+
description: data.videoDetails.shortDescription || data.videoDetails.description || '',
|
|
52
57
|
viewCount: parseInt(data.videoDetails.viewCount || 0),
|
|
58
|
+
likeCount: parseInt(data.videoDetails.likeCount || 0),
|
|
59
|
+
uploadDate: data.videoDetails.uploadDate || data.videoDetails.publishDate || null,
|
|
53
60
|
videoUrl: bestFormat.url,
|
|
54
61
|
quality: bestFormat.qualityLabel || bestFormat.quality || 'unknown',
|
|
55
62
|
format: bestFormat,
|
|
@@ -124,13 +131,20 @@ module.exports = {
|
|
|
124
131
|
const bestFormat = this.selectBestFormat(validFormats);
|
|
125
132
|
console.log(`Selected: ${bestFormat.qualityLabel || 'unknown'} quality\n`);
|
|
126
133
|
|
|
134
|
+
// Get highest quality thumbnail (last one in array)
|
|
135
|
+
const thumbnails = videoDetails.thumbnail.thumbnails;
|
|
136
|
+
const bestThumbnail = thumbnails[thumbnails.length - 1].url;
|
|
137
|
+
|
|
127
138
|
return {
|
|
128
139
|
title: videoDetails.title,
|
|
129
140
|
videoId: videoId,
|
|
130
141
|
duration: parseInt(videoDetails.lengthSeconds),
|
|
131
|
-
thumbnail:
|
|
142
|
+
thumbnail: bestThumbnail,
|
|
132
143
|
author: videoDetails.author,
|
|
144
|
+
description: videoDetails.shortDescription || videoDetails.description || '',
|
|
133
145
|
viewCount: parseInt(videoDetails.viewCount || 0),
|
|
146
|
+
likeCount: parseInt(videoDetails.likeCount || 0),
|
|
147
|
+
uploadDate: videoDetails.uploadDate || videoDetails.publishDate || null,
|
|
134
148
|
videoUrl: bestFormat.url,
|
|
135
149
|
quality: bestFormat.qualityLabel || 'unknown',
|
|
136
150
|
format: bestFormat,
|