flux-dl 1.0.0
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/LICENSE +21 -0
- package/README.md +454 -0
- package/package.json +50 -0
- package/src/VideoDownloader.js +197 -0
- package/src/index.js +11 -0
- package/src/platforms/dailymotion.js +71 -0
- package/src/platforms/examplePlatform.js +45 -0
- package/src/platforms/index.js +13 -0
- package/src/platforms/vimeo.js +66 -0
- package/src/platforms/youtube-deno.js +209 -0
- package/src/platforms/youtube.js +449 -0
- package/src/utils/browserEmulation.js +241 -0
- package/src/utils/cookieManager.js +110 -0
- package/src/utils/encryption.js +102 -0
- package/src/utils/requestSpoofing.js +141 -0
- package/src/utils/signatureDecoder.js +164 -0
- package/src/utils/youtubeInnerTube.js +201 -0
- package/src/utils/youtubeParser.js +283 -0
- package/src/utils/youtubeSearch.js +94 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const VideoDownloader = require('./VideoDownloader');
|
|
2
|
+
const VideoEncryption = require('./utils/encryption');
|
|
3
|
+
const YouTubeSearch = require('./utils/youtubeSearch');
|
|
4
|
+
const { supportedPlatforms } = require('./platforms');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
VideoDownloader,
|
|
8
|
+
VideoEncryption,
|
|
9
|
+
YouTubeSearch,
|
|
10
|
+
supportedPlatforms
|
|
11
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'Dailymotion',
|
|
5
|
+
|
|
6
|
+
canHandle(url) {
|
|
7
|
+
return url.includes('dailymotion.com') || url.includes('dai.ly');
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
async extractInfo(url, options) {
|
|
11
|
+
try {
|
|
12
|
+
const videoId = this.extractVideoId(url);
|
|
13
|
+
if (!videoId) {
|
|
14
|
+
throw new Error('Invalid Dailymotion URL');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Dailymotion API
|
|
18
|
+
const apiUrl = `https://api.dailymotion.com/video/${videoId}?fields=title,duration,thumbnail_url,owner.screenname,views_total`;
|
|
19
|
+
|
|
20
|
+
const response = await axios.get(apiUrl, {
|
|
21
|
+
headers: {
|
|
22
|
+
'User-Agent': options.userAgent
|
|
23
|
+
},
|
|
24
|
+
timeout: options.timeout,
|
|
25
|
+
httpsAgent: new (require('https').Agent)({
|
|
26
|
+
rejectUnauthorized: false
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const data = response.data;
|
|
31
|
+
|
|
32
|
+
// Video-Seite für Stream-URL laden
|
|
33
|
+
const pageResponse = await axios.get(`https://www.dailymotion.com/video/${videoId}`, {
|
|
34
|
+
headers: {
|
|
35
|
+
'User-Agent': options.userAgent
|
|
36
|
+
},
|
|
37
|
+
httpsAgent: new (require('https').Agent)({
|
|
38
|
+
rejectUnauthorized: false
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// M3U8 URL extrahieren
|
|
43
|
+
const m3u8Match = pageResponse.data.match(/"(https:\/\/[^"]+\.m3u8[^"]*)"/);
|
|
44
|
+
const videoUrl = m3u8Match ? m3u8Match[1] : null;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
title: data.title,
|
|
48
|
+
videoId: videoId,
|
|
49
|
+
duration: data.duration,
|
|
50
|
+
thumbnail: data.thumbnail_url,
|
|
51
|
+
author: data['owner.screenname'],
|
|
52
|
+
viewCount: data.views_total,
|
|
53
|
+
videoUrl: videoUrl,
|
|
54
|
+
quality: 'auto',
|
|
55
|
+
allFormats: [],
|
|
56
|
+
platform: this.name
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Failed to extract Dailymotion info: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
extractVideoId(url) {
|
|
65
|
+
let match = url.match(/dailymotion\.com\/video\/([a-zA-Z0-9]+)/);
|
|
66
|
+
if (match) return match[1];
|
|
67
|
+
|
|
68
|
+
match = url.match(/dai\.ly\/([a-zA-Z0-9]+)/);
|
|
69
|
+
return match ? match[1] : null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const cheerio = require('cheerio');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
name: 'Example Platform',
|
|
6
|
+
|
|
7
|
+
canHandle(url) {
|
|
8
|
+
// Prüfe ob URL zu dieser Plattform gehört
|
|
9
|
+
return url.includes('example.com');
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
async extractInfo(url, options) {
|
|
13
|
+
try {
|
|
14
|
+
const response = await axios.get(url, {
|
|
15
|
+
headers: {
|
|
16
|
+
'User-Agent': options.userAgent
|
|
17
|
+
},
|
|
18
|
+
timeout: options.timeout,
|
|
19
|
+
httpsAgent: new (require('https').Agent)({
|
|
20
|
+
rejectUnauthorized: false
|
|
21
|
+
})
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const $ = cheerio.load(response.data);
|
|
25
|
+
|
|
26
|
+
// Beispiel: Video-Infos extrahieren
|
|
27
|
+
const title = $('title').text();
|
|
28
|
+
const videoUrl = $('video source').attr('src');
|
|
29
|
+
|
|
30
|
+
if (!videoUrl) {
|
|
31
|
+
throw new Error('Video URL not found');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
title,
|
|
36
|
+
videoUrl,
|
|
37
|
+
thumbnail: $('meta[property="og:image"]').attr('content'),
|
|
38
|
+
duration: null,
|
|
39
|
+
platform: this.name
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`Failed to extract info: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const examplePlatform = require('./examplePlatform');
|
|
2
|
+
const youtube = require('./youtube');
|
|
3
|
+
const vimeo = require('./vimeo');
|
|
4
|
+
const dailymotion = require('./dailymotion');
|
|
5
|
+
|
|
6
|
+
const supportedPlatforms = {
|
|
7
|
+
example: examplePlatform,
|
|
8
|
+
youtube: youtube, // Eigene Implementation (deno deaktiviert)
|
|
9
|
+
vimeo: vimeo,
|
|
10
|
+
dailymotion: dailymotion
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = supportedPlatforms;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'Vimeo',
|
|
5
|
+
|
|
6
|
+
canHandle(url) {
|
|
7
|
+
return url.includes('vimeo.com');
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
async extractInfo(url, options) {
|
|
11
|
+
try {
|
|
12
|
+
const videoId = this.extractVideoId(url);
|
|
13
|
+
if (!videoId) {
|
|
14
|
+
throw new Error('Invalid Vimeo URL');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Vimeo API nutzen (öffentliche Videos)
|
|
18
|
+
const apiUrl = `https://player.vimeo.com/video/${videoId}/config`;
|
|
19
|
+
|
|
20
|
+
const response = await axios.get(apiUrl, {
|
|
21
|
+
headers: {
|
|
22
|
+
'User-Agent': options.userAgent
|
|
23
|
+
},
|
|
24
|
+
timeout: options.timeout,
|
|
25
|
+
httpsAgent: new (require('https').Agent)({
|
|
26
|
+
rejectUnauthorized: false
|
|
27
|
+
})
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const data = response.data;
|
|
31
|
+
const video = data.video;
|
|
32
|
+
const files = data.request.files;
|
|
33
|
+
|
|
34
|
+
// Beste Qualität finden
|
|
35
|
+
let videoUrl = null;
|
|
36
|
+
let quality = 'unknown';
|
|
37
|
+
|
|
38
|
+
if (files.progressive && files.progressive.length > 0) {
|
|
39
|
+
const sorted = files.progressive.sort((a, b) => b.height - a.height);
|
|
40
|
+
videoUrl = sorted[0].url;
|
|
41
|
+
quality = sorted[0].quality;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
title: video.title,
|
|
46
|
+
videoId: videoId,
|
|
47
|
+
duration: video.duration,
|
|
48
|
+
thumbnail: video.thumbs['640'],
|
|
49
|
+
author: video.owner.name,
|
|
50
|
+
viewCount: null,
|
|
51
|
+
videoUrl: videoUrl,
|
|
52
|
+
quality: quality,
|
|
53
|
+
allFormats: files.progressive || [],
|
|
54
|
+
platform: this.name
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Failed to extract Vimeo info: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
extractVideoId(url) {
|
|
63
|
+
const match = url.match(/vimeo\.com\/(\d+)/);
|
|
64
|
+
return match ? match[1] : null;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'YouTube (deno)',
|
|
7
|
+
|
|
8
|
+
canHandle(url) {
|
|
9
|
+
return url.includes('youtube.com') || url.includes('youtu.be');
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
async extractInfo(url, options) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
// Prüfe ob cookies.txt existiert
|
|
15
|
+
const cookiesArg = fs.existsSync('cookies.txt') ? '--cookies cookies.txt' : '';
|
|
16
|
+
|
|
17
|
+
const cmd = `deno run --allow-net --allow-read --allow-write https://deno.land/x/youtube_dl/mod.ts --dump-json ${cookiesArg} "${url}"`;
|
|
18
|
+
|
|
19
|
+
console.log('Using deno for YouTube extraction...');
|
|
20
|
+
|
|
21
|
+
exec(cmd, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
22
|
+
if (error) {
|
|
23
|
+
// Fallback: Versuche mit yt-dlp
|
|
24
|
+
const ytdlpCmd = `yt-dlp --dump-json ${cookiesArg} "${url}"`;
|
|
25
|
+
|
|
26
|
+
exec(ytdlpCmd, { maxBuffer: 10 * 1024 * 1024 }, (error2, stdout2, stderr2) => {
|
|
27
|
+
if (error2) {
|
|
28
|
+
reject(new Error(`Neither deno nor yt-dlp found. Install one of them:\n- deno: https://deno.land/\n- yt-dlp: https://github.com/yt-dlp/yt-dlp`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const data = JSON.parse(stdout2);
|
|
34
|
+
resolve(this.parseVideoData(data, url));
|
|
35
|
+
} catch (parseError) {
|
|
36
|
+
reject(new Error(`Failed to parse output: ${parseError.message}`));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(stdout);
|
|
44
|
+
resolve(this.parseVideoData(data, url));
|
|
45
|
+
} catch (parseError) {
|
|
46
|
+
reject(new Error(`Failed to parse deno output: ${parseError.message}`));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
parseVideoData(data, url) {
|
|
53
|
+
return {
|
|
54
|
+
title: data.title,
|
|
55
|
+
videoId: data.id,
|
|
56
|
+
duration: data.duration,
|
|
57
|
+
thumbnail: data.thumbnail,
|
|
58
|
+
author: data.uploader || data.channel,
|
|
59
|
+
viewCount: data.view_count,
|
|
60
|
+
videoUrl: url,
|
|
61
|
+
quality: data.format_note || 'best',
|
|
62
|
+
format: data,
|
|
63
|
+
allFormats: data.formats || [],
|
|
64
|
+
platform: this.name,
|
|
65
|
+
_useCli: true
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async download(url, outputPath, onProgress) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
if (!fs.existsSync(outputPath)) {
|
|
72
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cookiesArg = fs.existsSync('cookies.txt') ? '--cookies cookies.txt' : '';
|
|
76
|
+
const outputTemplate = path.join(outputPath, '%(title)s.%(ext)s');
|
|
77
|
+
|
|
78
|
+
// Versuche zuerst deno
|
|
79
|
+
let cmd = `deno run --allow-net --allow-read --allow-write https://deno.land/x/youtube_dl/mod.ts -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" ${cookiesArg} -o "${outputTemplate}" "${url}"`;
|
|
80
|
+
|
|
81
|
+
console.log('Downloading with deno...');
|
|
82
|
+
|
|
83
|
+
let process = exec(cmd, { maxBuffer: 50 * 1024 * 1024 });
|
|
84
|
+
|
|
85
|
+
let usedDeno = true;
|
|
86
|
+
|
|
87
|
+
process.on('error', (err) => {
|
|
88
|
+
// Fallback zu yt-dlp
|
|
89
|
+
console.log('deno failed, trying yt-dlp...');
|
|
90
|
+
usedDeno = false;
|
|
91
|
+
|
|
92
|
+
cmd = `yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" ${cookiesArg} -o "${outputTemplate}" "${url}"`;
|
|
93
|
+
process = exec(cmd, { maxBuffer: 50 * 1024 * 1024 });
|
|
94
|
+
|
|
95
|
+
process.stdout.on('data', (data) => {
|
|
96
|
+
const output = data.toString();
|
|
97
|
+
console.log(output);
|
|
98
|
+
|
|
99
|
+
const match = output.match(/(\d+\.\d+)%/);
|
|
100
|
+
if (match && onProgress) {
|
|
101
|
+
onProgress(parseFloat(match[1]));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
process.stderr.on('data', (data) => {
|
|
106
|
+
console.error(data.toString());
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
process.on('close', (code) => {
|
|
110
|
+
if (code === 0) {
|
|
111
|
+
resolve({ success: true, outputPath, tool: 'yt-dlp' });
|
|
112
|
+
} else {
|
|
113
|
+
reject(new Error(`yt-dlp exited with code ${code}`));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (usedDeno) {
|
|
119
|
+
process.stdout.on('data', (data) => {
|
|
120
|
+
const output = data.toString();
|
|
121
|
+
console.log(output);
|
|
122
|
+
|
|
123
|
+
const match = output.match(/(\d+\.\d+)%/);
|
|
124
|
+
if (match && onProgress) {
|
|
125
|
+
onProgress(parseFloat(match[1]));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
process.stderr.on('data', (data) => {
|
|
130
|
+
console.error(data.toString());
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
process.on('close', (code) => {
|
|
134
|
+
if (code === 0) {
|
|
135
|
+
resolve({ success: true, outputPath, tool: 'deno' });
|
|
136
|
+
} else {
|
|
137
|
+
// Versuche yt-dlp als Fallback
|
|
138
|
+
console.log('deno failed, trying yt-dlp...');
|
|
139
|
+
|
|
140
|
+
const ytdlpCmd = `yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" ${cookiesArg} -o "${outputTemplate}" "${url}"`;
|
|
141
|
+
const ytdlpProcess = exec(ytdlpCmd, { maxBuffer: 50 * 1024 * 1024 });
|
|
142
|
+
|
|
143
|
+
ytdlpProcess.stdout.on('data', (data) => console.log(data.toString()));
|
|
144
|
+
ytdlpProcess.stderr.on('data', (data) => console.error(data.toString()));
|
|
145
|
+
|
|
146
|
+
ytdlpProcess.on('close', (ytdlpCode) => {
|
|
147
|
+
if (ytdlpCode === 0) {
|
|
148
|
+
resolve({ success: true, outputPath, tool: 'yt-dlp' });
|
|
149
|
+
} else {
|
|
150
|
+
reject(new Error(`Both deno and yt-dlp failed`));
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async downloadAudio(url, outputPath, onProgress) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
if (!fs.existsSync(outputPath)) {
|
|
162
|
+
fs.mkdirSync(outputPath, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const cookiesArg = fs.existsSync('cookies.txt') ? '--cookies cookies.txt' : '';
|
|
166
|
+
const outputTemplate = path.join(outputPath, '%(title)s.%(ext)s');
|
|
167
|
+
|
|
168
|
+
let cmd = `deno run --allow-net --allow-read --allow-write https://deno.land/x/youtube_dl/mod.ts -f "bestaudio[ext=m4a]/bestaudio" ${cookiesArg} -o "${outputTemplate}" "${url}"`;
|
|
169
|
+
|
|
170
|
+
console.log('Downloading audio with deno...');
|
|
171
|
+
|
|
172
|
+
const process = exec(cmd, { maxBuffer: 50 * 1024 * 1024 });
|
|
173
|
+
|
|
174
|
+
process.on('error', () => {
|
|
175
|
+
// Fallback zu yt-dlp
|
|
176
|
+
const ytdlpCmd = `yt-dlp -f "bestaudio[ext=m4a]/bestaudio" ${cookiesArg} -o "${outputTemplate}" "${url}"`;
|
|
177
|
+
exec(ytdlpCmd, { maxBuffer: 50 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
178
|
+
if (err) {
|
|
179
|
+
reject(new Error('Both deno and yt-dlp failed'));
|
|
180
|
+
} else {
|
|
181
|
+
resolve({ success: true, outputPath, tool: 'yt-dlp' });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
process.stdout.on('data', (data) => {
|
|
187
|
+
const output = data.toString();
|
|
188
|
+
console.log(output);
|
|
189
|
+
|
|
190
|
+
const match = output.match(/(\d+\.\d+)%/);
|
|
191
|
+
if (match && onProgress) {
|
|
192
|
+
onProgress(parseFloat(match[1]));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
process.stderr.on('data', (data) => {
|
|
197
|
+
console.error(data.toString());
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
process.on('close', (code) => {
|
|
201
|
+
if (code === 0) {
|
|
202
|
+
resolve({ success: true, outputPath, tool: 'deno' });
|
|
203
|
+
} else {
|
|
204
|
+
reject(new Error(`deno exited with code ${code}`));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|