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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
class YouTubeInnerTube {
|
|
4
|
+
constructor() {
|
|
5
|
+
// Verschiedene Client-Konfigurationen zum Testen
|
|
6
|
+
this.clients = {
|
|
7
|
+
// Android Client - BESTE Option, keine N-Parameter!
|
|
8
|
+
android: {
|
|
9
|
+
clientName: 'ANDROID',
|
|
10
|
+
clientVersion: '19.09.37',
|
|
11
|
+
androidSdkVersion: 34,
|
|
12
|
+
apiKey: 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w'
|
|
13
|
+
},
|
|
14
|
+
// iOS Client
|
|
15
|
+
ios: {
|
|
16
|
+
clientName: 'IOS',
|
|
17
|
+
clientVersion: '19.09.3',
|
|
18
|
+
deviceModel: 'iPhone16,2',
|
|
19
|
+
apiKey: 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc'
|
|
20
|
+
},
|
|
21
|
+
// Web Client
|
|
22
|
+
web: {
|
|
23
|
+
clientName: 'WEB',
|
|
24
|
+
clientVersion: '2.20240304.00.00',
|
|
25
|
+
apiKey: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
|
|
26
|
+
},
|
|
27
|
+
// TV Embedded - oft weniger geschützt
|
|
28
|
+
tvEmbedded: {
|
|
29
|
+
clientName: 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
|
|
30
|
+
clientVersion: '2.0',
|
|
31
|
+
apiKey: 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.currentClient = 'android'; // Android als Standard
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getContext(clientType = this.currentClient) {
|
|
39
|
+
const client = this.clients[clientType];
|
|
40
|
+
|
|
41
|
+
const context = {
|
|
42
|
+
client: {
|
|
43
|
+
clientName: client.clientName,
|
|
44
|
+
clientVersion: client.clientVersion,
|
|
45
|
+
hl: 'en',
|
|
46
|
+
gl: 'US'
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (client.androidSdkVersion) {
|
|
51
|
+
context.client.androidSdkVersion = client.androidSdkVersion;
|
|
52
|
+
context.client.osName = 'Android';
|
|
53
|
+
context.client.osVersion = '14';
|
|
54
|
+
context.client.platform = 'MOBILE';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (client.deviceModel) {
|
|
58
|
+
context.client.deviceModel = client.deviceModel;
|
|
59
|
+
context.client.platform = 'MOBILE';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Für TV/Embedded Clients
|
|
63
|
+
if (clientType === 'tvEmbedded') {
|
|
64
|
+
context.thirdParty = {
|
|
65
|
+
embedUrl: 'https://www.youtube.com/'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return context;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getVideoInfo(videoId, clientType = this.currentClient) {
|
|
73
|
+
const client = this.clients[clientType];
|
|
74
|
+
const url = `https://www.youtube.com/youtubei/v1/player?key=${client.apiKey}&prettyPrint=false`;
|
|
75
|
+
|
|
76
|
+
const payload = {
|
|
77
|
+
videoId: videoId,
|
|
78
|
+
context: this.getContext(clientType),
|
|
79
|
+
playbackContext: {
|
|
80
|
+
contentPlaybackContext: {
|
|
81
|
+
html5Preference: 'HTML5_PREF_WANTS',
|
|
82
|
+
signatureTimestamp: Math.floor(Date.now() / 1000)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
contentCheckOk: true,
|
|
86
|
+
racyCheckOk: true
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const headers = {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
'User-Agent': this.getUserAgent(clientType),
|
|
92
|
+
'Accept': '*/*',
|
|
93
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
94
|
+
'Origin': 'https://www.youtube.com',
|
|
95
|
+
'Referer': 'https://www.youtube.com/'
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (clientType === 'android' || clientType === 'androidTv') {
|
|
99
|
+
headers['X-YouTube-Client-Name'] = '3';
|
|
100
|
+
headers['X-YouTube-Client-Version'] = client.clientVersion;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response = await axios.post(url, payload, {
|
|
105
|
+
headers: headers,
|
|
106
|
+
httpsAgent: new (require('https').Agent)({
|
|
107
|
+
rejectUnauthorized: false
|
|
108
|
+
})
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return response.data;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Versuche andere Clients in dieser Reihenfolge
|
|
114
|
+
if (clientType === this.currentClient) {
|
|
115
|
+
const alternatives = ['tv', 'webEmbedded', 'androidTv', 'ios', 'android', 'web'];
|
|
116
|
+
|
|
117
|
+
for (const alt of alternatives) {
|
|
118
|
+
if (alt === clientType) continue;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
console.log(`Trying ${alt} client...`);
|
|
122
|
+
return await this.getVideoInfo(videoId, alt);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(`All InnerTube clients failed: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getUserAgent(clientType) {
|
|
134
|
+
const agents = {
|
|
135
|
+
web: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
136
|
+
android: 'com.google.android.youtube/19.09.37 (Linux; U; Android 14; en_US) gzip',
|
|
137
|
+
ios: 'com.google.ios.youtube/19.09.3 (iPhone16,2; U; CPU iOS 17_4 like Mac OS X; en_US)',
|
|
138
|
+
tvEmbedded: 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version'
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return agents[clientType] || agents.android;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
selectBestFormat(formats) {
|
|
145
|
+
if (!formats || formats.length === 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Bevorzuge Formate mit Audio + Video
|
|
150
|
+
const withBoth = formats.filter(f =>
|
|
151
|
+
f.mimeType &&
|
|
152
|
+
f.mimeType.includes('video') &&
|
|
153
|
+
f.audioQuality &&
|
|
154
|
+
f.url
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (withBoth.length > 0) {
|
|
158
|
+
withBoth.sort((a, b) => {
|
|
159
|
+
const heightA = a.height || 0;
|
|
160
|
+
const heightB = b.height || 0;
|
|
161
|
+
return heightB - heightA;
|
|
162
|
+
});
|
|
163
|
+
return withBoth[0];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback: Nur Video
|
|
167
|
+
const videoOnly = formats.filter(f =>
|
|
168
|
+
f.mimeType &&
|
|
169
|
+
f.mimeType.includes('video') &&
|
|
170
|
+
f.url
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (videoOnly.length > 0) {
|
|
174
|
+
videoOnly.sort((a, b) => (b.height || 0) - (a.height || 0));
|
|
175
|
+
return videoOnly[0];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return formats.find(f => f.url);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
selectBestAudio(formats) {
|
|
182
|
+
if (!formats || formats.length === 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const audioFormats = formats.filter(f =>
|
|
187
|
+
f.mimeType &&
|
|
188
|
+
f.mimeType.includes('audio') &&
|
|
189
|
+
f.url
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
if (audioFormats.length === 0) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
audioFormats.sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0));
|
|
197
|
+
return audioFormats[0];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = YouTubeInnerTube;
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const vm = require('vm');
|
|
3
|
+
|
|
4
|
+
class YouTubeParser {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.cache = {
|
|
7
|
+
playerCode: null,
|
|
8
|
+
playerUrl: null,
|
|
9
|
+
timestamp: null
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getVideoInfo(videoId, userAgent) {
|
|
14
|
+
// Methode 1: Embedded Player API (oft ohne Signatur)
|
|
15
|
+
try {
|
|
16
|
+
const embedUrl = `https://www.youtube.com/embed/${videoId}`;
|
|
17
|
+
const embedResponse = await axios.get(embedUrl, {
|
|
18
|
+
headers: {
|
|
19
|
+
'User-Agent': userAgent,
|
|
20
|
+
'Accept-Language': 'en-US,en;q=0.9'
|
|
21
|
+
},
|
|
22
|
+
httpsAgent: new (require('https').Agent)({
|
|
23
|
+
rejectUnauthorized: false
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const embedHtml = embedResponse.data;
|
|
28
|
+
|
|
29
|
+
// Extrahiere Player Response aus Embed
|
|
30
|
+
let playerResponse = this.extractPlayerResponse(embedHtml);
|
|
31
|
+
|
|
32
|
+
if (playerResponse) {
|
|
33
|
+
return playerResponse;
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.log('Embed method failed, trying watch page...');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Methode 2: Watch Page
|
|
40
|
+
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
41
|
+
const response = await axios.get(watchUrl, {
|
|
42
|
+
headers: {
|
|
43
|
+
'User-Agent': userAgent,
|
|
44
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
45
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
|
46
|
+
},
|
|
47
|
+
httpsAgent: new (require('https').Agent)({
|
|
48
|
+
rejectUnauthorized: false
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const html = response.data;
|
|
53
|
+
return this.extractPlayerResponse(html);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
extractPlayerResponse(html) {
|
|
57
|
+
// Mehrere Patterns versuchen
|
|
58
|
+
const patterns = [
|
|
59
|
+
/var ytInitialPlayerResponse = ({.+?});/,
|
|
60
|
+
/ytInitialPlayerResponse\s*=\s*({.+?});/,
|
|
61
|
+
/"playerResponse":"({.+?})"/,
|
|
62
|
+
/ytInitialPlayerResponse\s*=\s*({.+?});<\/script>/
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const pattern of patterns) {
|
|
66
|
+
const match = html.match(pattern);
|
|
67
|
+
if (match) {
|
|
68
|
+
try {
|
|
69
|
+
let jsonStr = match[1];
|
|
70
|
+
|
|
71
|
+
// Wenn escaped JSON
|
|
72
|
+
if (jsonStr.startsWith('"')) {
|
|
73
|
+
jsonStr = JSON.parse(jsonStr);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return JSON.parse(jsonStr);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw new Error('Could not extract player response');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getPlayerCode(videoId, userAgent) {
|
|
87
|
+
// Cache prüfen (5 Minuten gültig)
|
|
88
|
+
if (this.cache.playerCode && this.cache.timestamp) {
|
|
89
|
+
const age = Date.now() - this.cache.timestamp;
|
|
90
|
+
if (age < 5 * 60 * 1000) {
|
|
91
|
+
return { code: this.cache.playerCode, url: this.cache.playerUrl };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const watchUrl = `https://www.youtube.com/watch?v=${videoId}`;
|
|
96
|
+
const response = await axios.get(watchUrl, {
|
|
97
|
+
headers: {
|
|
98
|
+
'User-Agent': userAgent
|
|
99
|
+
},
|
|
100
|
+
httpsAgent: new (require('https').Agent)({
|
|
101
|
+
rejectUnauthorized: false
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const html = response.data;
|
|
106
|
+
|
|
107
|
+
// Player URL finden
|
|
108
|
+
const patterns = [
|
|
109
|
+
/"jsUrl":"([^"]+)"/,
|
|
110
|
+
/"PLAYER_JS_URL":"([^"]+)"/,
|
|
111
|
+
/jsUrl":"([^"]+)"/
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
let playerUrl = null;
|
|
115
|
+
for (const pattern of patterns) {
|
|
116
|
+
const match = html.match(pattern);
|
|
117
|
+
if (match) {
|
|
118
|
+
playerUrl = match[1].replace(/\\\//g, '/');
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!playerUrl) {
|
|
124
|
+
throw new Error('Could not find player URL');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (playerUrl.startsWith('//')) {
|
|
128
|
+
playerUrl = 'https:' + playerUrl;
|
|
129
|
+
} else if (playerUrl.startsWith('/')) {
|
|
130
|
+
playerUrl = 'https://www.youtube.com' + playerUrl;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Player Code laden
|
|
134
|
+
const playerResponse = await axios.get(playerUrl, {
|
|
135
|
+
headers: {
|
|
136
|
+
'User-Agent': userAgent
|
|
137
|
+
},
|
|
138
|
+
httpsAgent: new (require('https').Agent)({
|
|
139
|
+
rejectUnauthorized: false
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const playerCode = playerResponse.data;
|
|
144
|
+
|
|
145
|
+
// Cache speichern
|
|
146
|
+
this.cache = {
|
|
147
|
+
playerCode: playerCode,
|
|
148
|
+
playerUrl: playerUrl,
|
|
149
|
+
timestamp: Date.now()
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return { code: playerCode, url: playerUrl };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
decipherSignature(signature, playerCode) {
|
|
156
|
+
try {
|
|
157
|
+
// N-Parameter Transform finden (wichtig für 403 Vermeidung)
|
|
158
|
+
const nTransform = this.extractNTransform(playerCode);
|
|
159
|
+
|
|
160
|
+
// Signature Decipher finden
|
|
161
|
+
const decipherFunc = this.extractDecipherFunc(playerCode);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
signature: decipherFunc ? decipherFunc(signature) : signature,
|
|
165
|
+
nTransform: nTransform
|
|
166
|
+
};
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn('Decipher failed:', error.message);
|
|
169
|
+
return { signature: signature, nTransform: null };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
extractNTransform(playerCode) {
|
|
174
|
+
// N-Parameter ist kritisch für 403-Vermeidung
|
|
175
|
+
try {
|
|
176
|
+
const patterns = [
|
|
177
|
+
/&&\(b=([a-zA-Z0-9$]+)\(decodeURIComponent\(b\)\)/,
|
|
178
|
+
/\(b=([a-zA-Z0-9$]+)\(decodeURIComponent\(b\)\)\)/,
|
|
179
|
+
/\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\([a-zA-Z]\)/
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const pattern of patterns) {
|
|
183
|
+
const match = playerCode.match(pattern);
|
|
184
|
+
if (match) {
|
|
185
|
+
const funcName = match[1];
|
|
186
|
+
|
|
187
|
+
// Funktion extrahieren
|
|
188
|
+
const funcPattern = new RegExp(`${funcName.replace(/\$/g, '\\$')}=function\\([^)]+\\)\\{[^}]+\\}`, 'g');
|
|
189
|
+
const funcMatch = playerCode.match(funcPattern);
|
|
190
|
+
|
|
191
|
+
if (funcMatch) {
|
|
192
|
+
const code = funcMatch[0];
|
|
193
|
+
const context = {};
|
|
194
|
+
vm.createContext(context);
|
|
195
|
+
vm.runInContext(code, context);
|
|
196
|
+
return context[funcName];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn('N-transform extraction failed:', error.message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
extractDecipherFunc(playerCode) {
|
|
208
|
+
// Vereinfachter Ansatz: Suche nach bekannten Patterns
|
|
209
|
+
try {
|
|
210
|
+
// Pattern 1: Standard decipher
|
|
211
|
+
const patterns = [
|
|
212
|
+
/([a-zA-Z0-9$]+)=function\([a-zA-Z]\)\{[a-zA-Z]=\1\.split\(""\)/,
|
|
213
|
+
/\b([a-zA-Z0-9$]{2,})\s*=\s*function\(\s*a\s*\)\s*\{\s*a\s*=\s*a\.split\(\s*""\s*\)/
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const pattern of patterns) {
|
|
217
|
+
const match = playerCode.match(pattern);
|
|
218
|
+
if (match) {
|
|
219
|
+
const funcName = match[1];
|
|
220
|
+
|
|
221
|
+
// Extrahiere Funktion und Helper
|
|
222
|
+
const funcPattern = new RegExp(`${funcName.replace(/\$/g, '\\$')}=function\\([a-zA-Z]\\)\\{[^}]+\\}`, 'g');
|
|
223
|
+
const funcMatch = playerCode.match(funcPattern);
|
|
224
|
+
|
|
225
|
+
if (!funcMatch) continue;
|
|
226
|
+
|
|
227
|
+
const funcBody = funcMatch[0];
|
|
228
|
+
const helperMatch = funcBody.match(/;([a-zA-Z0-9$]+)\./);
|
|
229
|
+
|
|
230
|
+
if (!helperMatch) continue;
|
|
231
|
+
|
|
232
|
+
const helperName = helperMatch[1];
|
|
233
|
+
const helperPattern = new RegExp(`var ${helperName.replace(/\$/g, '\\$')}=\\{[\\s\\S]+?\\}\\};`);
|
|
234
|
+
const helperMatch2 = playerCode.match(helperPattern);
|
|
235
|
+
|
|
236
|
+
if (!helperMatch2) continue;
|
|
237
|
+
|
|
238
|
+
const code = `${helperMatch2[0]}\n${funcBody}\n${funcName};`;
|
|
239
|
+
|
|
240
|
+
const context = {};
|
|
241
|
+
vm.createContext(context);
|
|
242
|
+
return vm.runInContext(code, context);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.warn('Decipher extraction failed:', error.message);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
parseSignatureCipher(cipher) {
|
|
253
|
+
const params = new URLSearchParams(cipher);
|
|
254
|
+
return {
|
|
255
|
+
url: params.get('url'),
|
|
256
|
+
s: params.get('s'),
|
|
257
|
+
sp: params.get('sp') || 'signature'
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
buildUrl(baseUrl, signature, sp, nParam, nTransform) {
|
|
262
|
+
let url = baseUrl;
|
|
263
|
+
|
|
264
|
+
// Signatur hinzufügen
|
|
265
|
+
if (signature && sp) {
|
|
266
|
+
url += `&${sp}=${encodeURIComponent(signature)}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// N-Parameter transformieren (wichtig!)
|
|
270
|
+
if (nParam && nTransform) {
|
|
271
|
+
try {
|
|
272
|
+
const transformedN = nTransform(nParam);
|
|
273
|
+
url = url.replace(`&n=${nParam}`, `&n=${transformedN}`);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.warn('N-transform failed:', error.message);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return url;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = YouTubeParser;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
|
|
3
|
+
class YouTubeSearch {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.baseUrl = 'https://www.youtube.com/results';
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async search(query, maxResults = 5) {
|
|
9
|
+
try {
|
|
10
|
+
const response = await axios.get(this.baseUrl, {
|
|
11
|
+
params: { search_query: query },
|
|
12
|
+
headers: {
|
|
13
|
+
'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'
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const html = response.data;
|
|
18
|
+
|
|
19
|
+
// Extrahiere ytInitialData
|
|
20
|
+
const match = html.match(/var ytInitialData = ({.+?});/);
|
|
21
|
+
if (!match) {
|
|
22
|
+
throw new Error('Could not extract search results');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const data = JSON.parse(match[1]);
|
|
26
|
+
|
|
27
|
+
// Navigiere durch die Datenstruktur
|
|
28
|
+
const contents = data?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents;
|
|
29
|
+
|
|
30
|
+
if (!contents) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const results = [];
|
|
35
|
+
|
|
36
|
+
for (const section of contents) {
|
|
37
|
+
const items = section?.itemSectionRenderer?.contents || [];
|
|
38
|
+
|
|
39
|
+
for (const item of items) {
|
|
40
|
+
if (item.videoRenderer && results.length < maxResults) {
|
|
41
|
+
const video = item.videoRenderer;
|
|
42
|
+
|
|
43
|
+
results.push({
|
|
44
|
+
videoId: video.videoId,
|
|
45
|
+
title: video.title?.runs?.[0]?.text || 'Unknown',
|
|
46
|
+
url: `https://www.youtube.com/watch?v=${video.videoId}`,
|
|
47
|
+
thumbnail: video.thumbnail?.thumbnails?.[0]?.url || '',
|
|
48
|
+
duration: this.parseDuration(video.lengthText?.simpleText),
|
|
49
|
+
author: video.ownerText?.runs?.[0]?.text || 'Unknown',
|
|
50
|
+
views: video.viewCountText?.simpleText || '0 views',
|
|
51
|
+
publishedTime: video.publishedTimeText?.simpleText || 'Unknown'
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return results;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new Error(`YouTube search failed: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
parseDuration(durationText) {
|
|
64
|
+
if (!durationText) return 0;
|
|
65
|
+
|
|
66
|
+
const parts = durationText.split(':').reverse();
|
|
67
|
+
let seconds = 0;
|
|
68
|
+
|
|
69
|
+
if (parts[0]) seconds += parseInt(parts[0]);
|
|
70
|
+
if (parts[1]) seconds += parseInt(parts[1]) * 60;
|
|
71
|
+
if (parts[2]) seconds += parseInt(parts[2]) * 3600;
|
|
72
|
+
|
|
73
|
+
return seconds;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async searchAndDownload(query, downloader, options = {}) {
|
|
77
|
+
const results = await this.search(query, 1);
|
|
78
|
+
|
|
79
|
+
if (results.length === 0) {
|
|
80
|
+
throw new Error('No results found');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const video = results[0];
|
|
84
|
+
console.log(`Found: ${video.title} by ${video.author}`);
|
|
85
|
+
|
|
86
|
+
if (options.audioOnly) {
|
|
87
|
+
return await downloader.downloadAudio(video.url, options.outputPath);
|
|
88
|
+
} else {
|
|
89
|
+
return await downloader.download(video.url, options.outputPath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = YouTubeSearch;
|