flux-dl 1.1.3 → 1.1.6
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 +181 -1
- package/src/index.js +2 -0
- package/src/platforms/youtube.js +77 -9
- package/src/utils/browserEmulation.js +1 -2
- package/src/utils/qualityManager.js +270 -0
package/package.json
CHANGED
package/src/VideoDownloader.js
CHANGED
|
@@ -5,6 +5,7 @@ const platforms = require('./platforms');
|
|
|
5
5
|
const VideoEncryption = require('./utils/encryption');
|
|
6
6
|
const RequestSpoofing = require('./utils/requestSpoofing');
|
|
7
7
|
const BrowserEmulation = require('./utils/browserEmulation');
|
|
8
|
+
const QualityManager = require('./utils/qualityManager');
|
|
8
9
|
|
|
9
10
|
class VideoDownloader {
|
|
10
11
|
constructor(options = {}) {
|
|
@@ -13,17 +14,24 @@ class VideoDownloader {
|
|
|
13
14
|
timeout: 30000,
|
|
14
15
|
encryptionKey: options.encryptionKey || 'default-key',
|
|
15
16
|
cookiesFile: options.cookiesFile,
|
|
17
|
+
quality: options.quality || 192, // Standard: Medium (192 kbps)
|
|
16
18
|
...options
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
this.encryption = new VideoEncryption(this.options.encryptionKey);
|
|
20
22
|
this.spoofing = new RequestSpoofing();
|
|
21
23
|
this.browser = new BrowserEmulation();
|
|
24
|
+
this.qualityManager = new QualityManager();
|
|
22
25
|
|
|
23
26
|
// Load cookies if provided
|
|
24
27
|
if (this.options.cookiesFile) {
|
|
25
28
|
this.browser.cookieManager.loadFromFile(this.options.cookiesFile);
|
|
26
29
|
}
|
|
30
|
+
|
|
31
|
+
// Set default quality if provided
|
|
32
|
+
if (options.quality) {
|
|
33
|
+
this.qualityManager.setDefaultQuality(options.quality);
|
|
34
|
+
}
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
async getVideoInfo(url) {
|
|
@@ -158,8 +166,17 @@ class VideoDownloader {
|
|
|
158
166
|
// Nutze Browser Emulation mit Cookies
|
|
159
167
|
const referer = `https://www.youtube.com/watch?v=${info.videoId}`;
|
|
160
168
|
|
|
169
|
+
// Wrap onProgress to catch errors
|
|
170
|
+
const safeOnProgress = onProgress ? (percent, downloaded, total) => {
|
|
171
|
+
try {
|
|
172
|
+
onProgress(percent, downloaded, total);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
// Silently ignore progress callback errors
|
|
175
|
+
}
|
|
176
|
+
} : null;
|
|
177
|
+
|
|
161
178
|
try {
|
|
162
|
-
const rawStream = await this.browser.downloadStream(info.videoUrl, referer,
|
|
179
|
+
const rawStream = await this.browser.downloadStream(info.videoUrl, referer, safeOnProgress);
|
|
163
180
|
|
|
164
181
|
// Create ULTRA-RESILIENT wrapper stream
|
|
165
182
|
const { PassThrough } = require('stream');
|
|
@@ -347,6 +364,169 @@ class VideoDownloader {
|
|
|
347
364
|
signUrl(url, expiresIn = 3600) {
|
|
348
365
|
return this.encryption.signUrl(url, expiresIn);
|
|
349
366
|
}
|
|
367
|
+
|
|
368
|
+
// ========== QUALITY MANAGEMENT API ==========
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Setzt die Qualität für einen bestimmten User
|
|
372
|
+
* @param {string} userId - User ID
|
|
373
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
374
|
+
* @returns {number} Gesetzte Qualität
|
|
375
|
+
*/
|
|
376
|
+
setUserQuality(userId, quality) {
|
|
377
|
+
return this.qualityManager.setUserQuality(userId, quality);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Gibt die erlaubte Qualität für einen User zurück
|
|
382
|
+
* @param {string} userId - User ID
|
|
383
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
384
|
+
* @returns {number} Erlaubte Bitrate
|
|
385
|
+
*/
|
|
386
|
+
getUserQuality(userId, isPremium = false) {
|
|
387
|
+
return this.qualityManager.getUserQuality(userId, isPremium);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Setzt globale Standard-Qualität
|
|
392
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
393
|
+
*/
|
|
394
|
+
setDefaultQuality(quality) {
|
|
395
|
+
this.qualityManager.setDefaultQuality(quality);
|
|
396
|
+
this.options.quality = quality;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Setzt maximale Qualität für Free Users
|
|
401
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
402
|
+
*/
|
|
403
|
+
setMaxQualityForFreeUsers(quality) {
|
|
404
|
+
this.qualityManager.setMaxQualityForFreeUsers(quality);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Setzt maximale Qualität für Premium Users
|
|
409
|
+
* @param {number} quality - Bitrate (120-360 kbps)
|
|
410
|
+
*/
|
|
411
|
+
setMaxQualityForPremiumUsers(quality) {
|
|
412
|
+
this.qualityManager.setMaxQualityForPremiumUsers(quality);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Gibt verfügbare Qualitätsstufen für User zurück
|
|
417
|
+
* @param {string} userId - User ID
|
|
418
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
419
|
+
* @returns {Array} Verfügbare Stufen
|
|
420
|
+
*/
|
|
421
|
+
getAvailableQualities(userId, isPremium = false) {
|
|
422
|
+
return this.qualityManager.getAvailableQualities(userId, isPremium);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Prüft ob User bestimmte Qualität nutzen darf
|
|
427
|
+
* @param {string} userId - User ID
|
|
428
|
+
* @param {number} requestedQuality - Gewünschte Qualität
|
|
429
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
430
|
+
* @returns {boolean} Erlaubt?
|
|
431
|
+
*/
|
|
432
|
+
canUserUseQuality(userId, requestedQuality, isPremium = false) {
|
|
433
|
+
return this.qualityManager.canUserUseQuality(userId, requestedQuality, isPremium);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Download mit User-spezifischer Qualität
|
|
438
|
+
* @param {string} url - Video URL
|
|
439
|
+
* @param {string} outputPath - Output Pfad
|
|
440
|
+
* @param {string} userId - User ID
|
|
441
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
442
|
+
* @param {function} onProgress - Progress Callback
|
|
443
|
+
* @returns {Promise<Object>} Download Result
|
|
444
|
+
*/
|
|
445
|
+
async downloadAudioWithQuality(url, outputPath, userId, isPremium = false, onProgress = null) {
|
|
446
|
+
const userQuality = this.getUserQuality(userId, isPremium);
|
|
447
|
+
|
|
448
|
+
console.log(`🎵 Downloading for user ${userId}`);
|
|
449
|
+
console.log(`📊 Quality: ${this.qualityManager.getQualityLabel(userQuality)}`);
|
|
450
|
+
|
|
451
|
+
// Temporär Qualität setzen
|
|
452
|
+
const originalQuality = this.options.quality;
|
|
453
|
+
this.options.quality = userQuality;
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const result = await this.downloadAudio(url, outputPath, onProgress);
|
|
457
|
+
return {
|
|
458
|
+
...result,
|
|
459
|
+
quality: userQuality,
|
|
460
|
+
qualityLabel: this.qualityManager.getQualityLabel(userQuality)
|
|
461
|
+
};
|
|
462
|
+
} finally {
|
|
463
|
+
// Qualität zurücksetzen
|
|
464
|
+
this.options.quality = originalQuality;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Stream mit User-spezifischer Qualität
|
|
470
|
+
* @param {string} url - Video URL
|
|
471
|
+
* @param {string} userId - User ID
|
|
472
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
473
|
+
* @param {function} onProgress - Progress Callback
|
|
474
|
+
* @returns {Promise<Object>} Stream Result
|
|
475
|
+
*/
|
|
476
|
+
async getAudioStreamWithQuality(url, userId, isPremium = false, onProgress = null) {
|
|
477
|
+
const userQuality = this.getUserQuality(userId, isPremium);
|
|
478
|
+
|
|
479
|
+
console.log(`🎵 Streaming for user ${userId}`);
|
|
480
|
+
console.log(`📊 Quality: ${this.qualityManager.getQualityLabel(userQuality)}`);
|
|
481
|
+
|
|
482
|
+
// Temporär Qualität setzen
|
|
483
|
+
const originalQuality = this.options.quality;
|
|
484
|
+
this.options.quality = userQuality;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const result = await this.getAudioStream(url, onProgress);
|
|
488
|
+
return {
|
|
489
|
+
...result,
|
|
490
|
+
quality: userQuality,
|
|
491
|
+
qualityLabel: this.qualityManager.getQualityLabel(userQuality)
|
|
492
|
+
};
|
|
493
|
+
} finally {
|
|
494
|
+
// Qualität zurücksetzen
|
|
495
|
+
this.options.quality = originalQuality;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Entfernt User-Permission
|
|
501
|
+
* @param {string} userId - User ID
|
|
502
|
+
*/
|
|
503
|
+
removeUserQuality(userId) {
|
|
504
|
+
this.qualityManager.removeUserQuality(userId);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Gibt alle User-Permissions zurück
|
|
509
|
+
* @returns {Object} User-Permissions
|
|
510
|
+
*/
|
|
511
|
+
getAllUserPermissions() {
|
|
512
|
+
return this.qualityManager.getAllUserPermissions();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Exportiert Quality-Konfiguration
|
|
517
|
+
* @returns {Object} Konfiguration
|
|
518
|
+
*/
|
|
519
|
+
exportQualityConfig() {
|
|
520
|
+
return this.qualityManager.exportConfig();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Importiert Quality-Konfiguration
|
|
525
|
+
* @param {Object} config - Konfiguration
|
|
526
|
+
*/
|
|
527
|
+
importQualityConfig(config) {
|
|
528
|
+
this.qualityManager.importConfig(config);
|
|
529
|
+
}
|
|
350
530
|
}
|
|
351
531
|
|
|
352
532
|
module.exports = VideoDownloader;
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const VideoDownloader = require('./VideoDownloader');
|
|
2
2
|
const VideoEncryption = require('./utils/encryption');
|
|
3
3
|
const YouTubeSearch = require('./utils/youtubeSearch');
|
|
4
|
+
const QualityManager = require('./utils/qualityManager');
|
|
4
5
|
const { supportedPlatforms } = require('./platforms');
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
VideoDownloader,
|
|
8
9
|
VideoEncryption,
|
|
9
10
|
YouTubeSearch,
|
|
11
|
+
QualityManager,
|
|
10
12
|
supportedPlatforms
|
|
11
13
|
};
|
package/src/platforms/youtube.js
CHANGED
|
@@ -18,7 +18,7 @@ module.exports = {
|
|
|
18
18
|
throw new Error('Invalid YouTube URL');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
console.log(`\n=== Extracting YouTube Video: ${videoId} ===`);
|
|
21
|
+
// console.log(`\n=== Extracting YouTube Video: ${videoId} ===`);
|
|
22
22
|
|
|
23
23
|
// Starte Browser-Session (wie echter User)
|
|
24
24
|
await this.browser.startSession();
|
|
@@ -26,9 +26,12 @@ module.exports = {
|
|
|
26
26
|
// Methode 1: InnerTube API mit verschiedenen Clients
|
|
27
27
|
const clientPriority = ['android', 'ios', 'tvEmbedded', 'web'];
|
|
28
28
|
|
|
29
|
+
// Get target quality from options
|
|
30
|
+
const targetQuality = options?.quality || null;
|
|
31
|
+
|
|
29
32
|
for (const clientType of clientPriority) {
|
|
30
33
|
try {
|
|
31
|
-
console.log(`[InnerTube] Trying ${clientType} client...`);
|
|
34
|
+
// console.log(`[InnerTube] Trying ${clientType} client...`);
|
|
32
35
|
const data = await this.innerTube.getVideoInfo(videoId, clientType);
|
|
33
36
|
|
|
34
37
|
if (data.videoDetails && data.streamingData) {
|
|
@@ -41,7 +44,12 @@ module.exports = {
|
|
|
41
44
|
if (formatsWithUrl.length > 0) {
|
|
42
45
|
console.log(`✓ ${clientType} client success! ${formatsWithUrl.length} formats available`);
|
|
43
46
|
|
|
44
|
-
|
|
47
|
+
// Select format with target quality
|
|
48
|
+
const bestFormat = this.selectBestFormat(formatsWithUrl, targetQuality);
|
|
49
|
+
|
|
50
|
+
// Get actual bitrate for quality label
|
|
51
|
+
const actualBitrate = this.getBitrate(bestFormat);
|
|
52
|
+
const qualityLabel = `${actualBitrate}kbps`;
|
|
45
53
|
|
|
46
54
|
// Get highest quality thumbnail (last one in array)
|
|
47
55
|
const thumbnails = data.videoDetails.thumbnail.thumbnails;
|
|
@@ -58,7 +66,8 @@ module.exports = {
|
|
|
58
66
|
likeCount: parseInt(data.videoDetails.likeCount || 0),
|
|
59
67
|
uploadDate: data.videoDetails.uploadDate || data.videoDetails.publishDate || null,
|
|
60
68
|
videoUrl: bestFormat.url,
|
|
61
|
-
quality:
|
|
69
|
+
quality: qualityLabel,
|
|
70
|
+
bitrate: actualBitrate,
|
|
62
71
|
format: bestFormat,
|
|
63
72
|
allFormats: formatsWithUrl,
|
|
64
73
|
platform: this.name,
|
|
@@ -128,8 +137,14 @@ module.exports = {
|
|
|
128
137
|
throw new Error('No valid formats found');
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
// Select format with target quality (already declared above)
|
|
141
|
+
const bestFormat = this.selectBestFormat(validFormats, targetQuality);
|
|
142
|
+
|
|
143
|
+
// Get actual bitrate for quality label
|
|
144
|
+
const actualBitrate = this.getBitrate(bestFormat);
|
|
145
|
+
const qualityLabel = `${actualBitrate}kbps`;
|
|
146
|
+
|
|
147
|
+
console.log(`Selected: ${qualityLabel} quality\n`);
|
|
133
148
|
|
|
134
149
|
// Get highest quality thumbnail (last one in array)
|
|
135
150
|
const thumbnails = videoDetails.thumbnail.thumbnails;
|
|
@@ -146,7 +161,8 @@ module.exports = {
|
|
|
146
161
|
likeCount: parseInt(videoDetails.likeCount || 0),
|
|
147
162
|
uploadDate: videoDetails.uploadDate || videoDetails.publishDate || null,
|
|
148
163
|
videoUrl: bestFormat.url,
|
|
149
|
-
quality:
|
|
164
|
+
quality: qualityLabel,
|
|
165
|
+
bitrate: actualBitrate,
|
|
150
166
|
format: bestFormat,
|
|
151
167
|
allFormats: validFormats,
|
|
152
168
|
platform: this.name,
|
|
@@ -429,8 +445,35 @@ module.exports = {
|
|
|
429
445
|
return res.data;
|
|
430
446
|
},
|
|
431
447
|
|
|
432
|
-
selectBestFormat(formats) {
|
|
433
|
-
//
|
|
448
|
+
selectBestFormat(formats, targetQuality = null) {
|
|
449
|
+
// Wenn targetQuality angegeben, nutze Audio-only Format mit passender Bitrate
|
|
450
|
+
if (targetQuality) {
|
|
451
|
+
const audioFormats = formats.filter(f =>
|
|
452
|
+
f.url && f.mimeType && f.mimeType.includes('audio')
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (audioFormats.length > 0) {
|
|
456
|
+
// Sortiere nach Bitrate-Nähe zur Ziel-Qualität
|
|
457
|
+
audioFormats.sort((a, b) => {
|
|
458
|
+
const bitrateA = this.getBitrate(a);
|
|
459
|
+
const bitrateB = this.getBitrate(b);
|
|
460
|
+
return Math.abs(bitrateA - targetQuality) - Math.abs(bitrateB - targetQuality);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// Finde Format das unter oder gleich der Ziel-Qualität ist
|
|
464
|
+
for (const format of audioFormats) {
|
|
465
|
+
const bitrate = this.getBitrate(format);
|
|
466
|
+
if (bitrate <= targetQuality) {
|
|
467
|
+
return format;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Fallback: Nehme das kleinste Format
|
|
472
|
+
return audioFormats[audioFormats.length - 1];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Standard: Mit Audio + Video
|
|
434
477
|
const withBoth = formats.filter(f =>
|
|
435
478
|
f.url && f.mimeType && f.mimeType.includes('video') && f.audioQuality
|
|
436
479
|
);
|
|
@@ -451,6 +494,31 @@ module.exports = {
|
|
|
451
494
|
return formats[0];
|
|
452
495
|
},
|
|
453
496
|
|
|
497
|
+
getBitrate(format) {
|
|
498
|
+
if (format.bitrate) {
|
|
499
|
+
return Math.round(format.bitrate / 1000); // Bits zu kbps
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (format.audioBitrate) {
|
|
503
|
+
return format.audioBitrate;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (format.averageBitrate) {
|
|
507
|
+
return Math.round(format.averageBitrate / 1000);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Fallback: Schätze aus Quality-Label
|
|
511
|
+
if (format.qualityLabel) {
|
|
512
|
+
const match = format.qualityLabel.match(/(\d+)kbps/i);
|
|
513
|
+
if (match) {
|
|
514
|
+
return parseInt(match[1]);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Default: Mittel
|
|
519
|
+
return 192;
|
|
520
|
+
},
|
|
521
|
+
|
|
454
522
|
extractVideoId(url) {
|
|
455
523
|
let m = url.match(/[?&]v=([^&]+)/);
|
|
456
524
|
if (m) return m[1];
|
|
@@ -280,8 +280,7 @@ class BrowserEmulation {
|
|
|
280
280
|
onProgress(percent, downloadedLength, totalLength);
|
|
281
281
|
}
|
|
282
282
|
} catch (err) {
|
|
283
|
-
//
|
|
284
|
-
console.warn('⚠️ Progress callback error (handled)');
|
|
283
|
+
// Silently ignore progress callback errors
|
|
285
284
|
}
|
|
286
285
|
});
|
|
287
286
|
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUALITY MANAGER - Audio Bitrate Control System
|
|
3
|
+
* Verwaltet Audio-Qualität mit User-Permissions (120-360 kbps)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class QualityManager {
|
|
7
|
+
constructor() {
|
|
8
|
+
// Qualitätsstufen (in kbps für Audio)
|
|
9
|
+
this.QUALITY_LEVELS = {
|
|
10
|
+
MINIMUM: 120, // Niedrigste Qualität
|
|
11
|
+
LOW: 128, // Niedrig
|
|
12
|
+
MEDIUM: 192, // Mittel (Standard)
|
|
13
|
+
HIGH: 256, // Hoch
|
|
14
|
+
MAXIMUM: 360 // Höchste Qualität
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// User-Permissions (wer darf welche Qualität nutzen)
|
|
18
|
+
this.userPermissions = new Map();
|
|
19
|
+
|
|
20
|
+
// Globale Einstellungen
|
|
21
|
+
this.defaultQuality = this.QUALITY_LEVELS.MEDIUM;
|
|
22
|
+
this.maxQualityForFreeUsers = this.QUALITY_LEVELS.MEDIUM;
|
|
23
|
+
this.maxQualityForPremiumUsers = this.QUALITY_LEVELS.MAXIMUM;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Setzt die erlaubte Qualität für einen User
|
|
28
|
+
* @param {string} userId - User ID
|
|
29
|
+
* @param {number} maxQuality - Maximale Bitrate (120-360)
|
|
30
|
+
*/
|
|
31
|
+
setUserQuality(userId, maxQuality) {
|
|
32
|
+
const quality = this.validateQuality(maxQuality);
|
|
33
|
+
this.userPermissions.set(userId, quality);
|
|
34
|
+
return quality;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Gibt die erlaubte Qualität für einen User zurück
|
|
39
|
+
* @param {string} userId - User ID
|
|
40
|
+
* @param {boolean} isPremium - Ist der User Premium?
|
|
41
|
+
* @returns {number} Erlaubte Bitrate
|
|
42
|
+
*/
|
|
43
|
+
getUserQuality(userId, isPremium = false) {
|
|
44
|
+
// Wenn User spezifische Permission hat
|
|
45
|
+
if (this.userPermissions.has(userId)) {
|
|
46
|
+
return this.userPermissions.get(userId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Sonst nach Premium-Status
|
|
50
|
+
return isPremium ? this.maxQualityForPremiumUsers : this.maxQualityForFreeUsers;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validiert und begrenzt Qualitätswert
|
|
55
|
+
* @param {number} quality - Gewünschte Bitrate
|
|
56
|
+
* @returns {number} Validierte Bitrate (120-360)
|
|
57
|
+
*/
|
|
58
|
+
validateQuality(quality) {
|
|
59
|
+
const q = parseInt(quality);
|
|
60
|
+
|
|
61
|
+
if (isNaN(q)) {
|
|
62
|
+
return this.defaultQuality;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Begrenze auf Min/Max
|
|
66
|
+
if (q < this.QUALITY_LEVELS.MINIMUM) {
|
|
67
|
+
return this.QUALITY_LEVELS.MINIMUM;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (q > this.QUALITY_LEVELS.MAXIMUM) {
|
|
71
|
+
return this.QUALITY_LEVELS.MAXIMUM;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return q;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wählt das beste Audio-Format basierend auf Qualitätseinstellung
|
|
79
|
+
* @param {Array} formats - Verfügbare Formate
|
|
80
|
+
* @param {number} targetQuality - Ziel-Bitrate
|
|
81
|
+
* @returns {Object} Bestes Format
|
|
82
|
+
*/
|
|
83
|
+
selectBestAudioFormat(formats, targetQuality) {
|
|
84
|
+
// Nur Audio-Formate
|
|
85
|
+
const audioFormats = formats.filter(f =>
|
|
86
|
+
f.mimeType && f.mimeType.includes('audio') && f.url
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (audioFormats.length === 0) return null;
|
|
90
|
+
|
|
91
|
+
// Sortiere nach Bitrate
|
|
92
|
+
audioFormats.sort((a, b) => {
|
|
93
|
+
const bitrateA = this.getBitrate(a);
|
|
94
|
+
const bitrateB = this.getBitrate(b);
|
|
95
|
+
return Math.abs(bitrateA - targetQuality) - Math.abs(bitrateB - targetQuality);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Finde Format das am nächsten zur Ziel-Qualität ist
|
|
99
|
+
let bestFormat = audioFormats[0];
|
|
100
|
+
|
|
101
|
+
for (const format of audioFormats) {
|
|
102
|
+
const bitrate = this.getBitrate(format);
|
|
103
|
+
|
|
104
|
+
// Wenn Format unter Ziel-Qualität liegt, nehme es
|
|
105
|
+
if (bitrate <= targetQuality && bitrate > this.getBitrate(bestFormat)) {
|
|
106
|
+
bestFormat = format;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return bestFormat;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Extrahiert Bitrate aus Format
|
|
115
|
+
* @param {Object} format - Format-Objekt
|
|
116
|
+
* @returns {number} Bitrate in kbps
|
|
117
|
+
*/
|
|
118
|
+
getBitrate(format) {
|
|
119
|
+
if (format.bitrate) {
|
|
120
|
+
return Math.round(format.bitrate / 1000); // Bits zu kbps
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (format.audioBitrate) {
|
|
124
|
+
return format.audioBitrate;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback: Schätze aus Quality-Label
|
|
128
|
+
if (format.qualityLabel) {
|
|
129
|
+
const match = format.qualityLabel.match(/(\d+)kbps/i);
|
|
130
|
+
if (match) {
|
|
131
|
+
return parseInt(match[1]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Default: Mittel
|
|
136
|
+
return this.QUALITY_LEVELS.MEDIUM;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gibt Qualitäts-Label zurück
|
|
141
|
+
* @param {number} bitrate - Bitrate in kbps
|
|
142
|
+
* @returns {string} Label
|
|
143
|
+
*/
|
|
144
|
+
getQualityLabel(bitrate) {
|
|
145
|
+
if (bitrate >= this.QUALITY_LEVELS.MAXIMUM) return '🔥 Maximum (360 kbps)';
|
|
146
|
+
if (bitrate >= this.QUALITY_LEVELS.HIGH) return '⭐ High (256 kbps)';
|
|
147
|
+
if (bitrate >= this.QUALITY_LEVELS.MEDIUM) return '✅ Medium (192 kbps)';
|
|
148
|
+
if (bitrate >= this.QUALITY_LEVELS.LOW) return '📊 Low (128 kbps)';
|
|
149
|
+
return '💾 Minimum (120 kbps)';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Setzt globale Standard-Qualität
|
|
154
|
+
* @param {number} quality - Bitrate (120-360)
|
|
155
|
+
*/
|
|
156
|
+
setDefaultQuality(quality) {
|
|
157
|
+
this.defaultQuality = this.validateQuality(quality);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Setzt maximale Qualität für Free Users
|
|
162
|
+
* @param {number} quality - Bitrate (120-360)
|
|
163
|
+
*/
|
|
164
|
+
setMaxQualityForFreeUsers(quality) {
|
|
165
|
+
this.maxQualityForFreeUsers = this.validateQuality(quality);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Setzt maximale Qualität für Premium Users
|
|
170
|
+
* @param {number} quality - Bitrate (120-360)
|
|
171
|
+
*/
|
|
172
|
+
setMaxQualityForPremiumUsers(quality) {
|
|
173
|
+
this.maxQualityForPremiumUsers = this.validateQuality(quality);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Entfernt User-Permission
|
|
178
|
+
* @param {string} userId - User ID
|
|
179
|
+
*/
|
|
180
|
+
removeUserQuality(userId) {
|
|
181
|
+
this.userPermissions.delete(userId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Gibt alle User-Permissions zurück
|
|
186
|
+
* @returns {Object} User-Permissions
|
|
187
|
+
*/
|
|
188
|
+
getAllUserPermissions() {
|
|
189
|
+
const permissions = {};
|
|
190
|
+
for (const [userId, quality] of this.userPermissions.entries()) {
|
|
191
|
+
permissions[userId] = {
|
|
192
|
+
quality,
|
|
193
|
+
label: this.getQualityLabel(quality)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return permissions;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Prüft ob User bestimmte Qualität nutzen darf
|
|
201
|
+
* @param {string} userId - User ID
|
|
202
|
+
* @param {number} requestedQuality - Gewünschte Qualität
|
|
203
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
204
|
+
* @returns {boolean} Erlaubt?
|
|
205
|
+
*/
|
|
206
|
+
canUserUseQuality(userId, requestedQuality, isPremium = false) {
|
|
207
|
+
const maxAllowed = this.getUserQuality(userId, isPremium);
|
|
208
|
+
return requestedQuality <= maxAllowed;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Gibt verfügbare Qualitätsstufen für User zurück
|
|
213
|
+
* @param {string} userId - User ID
|
|
214
|
+
* @param {boolean} isPremium - Ist Premium User?
|
|
215
|
+
* @returns {Array} Verfügbare Stufen
|
|
216
|
+
*/
|
|
217
|
+
getAvailableQualities(userId, isPremium = false) {
|
|
218
|
+
const maxQuality = this.getUserQuality(userId, isPremium);
|
|
219
|
+
const available = [];
|
|
220
|
+
|
|
221
|
+
for (const [name, value] of Object.entries(this.QUALITY_LEVELS)) {
|
|
222
|
+
if (value <= maxQuality) {
|
|
223
|
+
available.push({
|
|
224
|
+
name,
|
|
225
|
+
value,
|
|
226
|
+
label: this.getQualityLabel(value)
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return available;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Exportiert Konfiguration
|
|
236
|
+
* @returns {Object} Konfiguration
|
|
237
|
+
*/
|
|
238
|
+
exportConfig() {
|
|
239
|
+
return {
|
|
240
|
+
defaultQuality: this.defaultQuality,
|
|
241
|
+
maxQualityForFreeUsers: this.maxQualityForFreeUsers,
|
|
242
|
+
maxQualityForPremiumUsers: this.maxQualityForPremiumUsers,
|
|
243
|
+
userPermissions: Object.fromEntries(this.userPermissions)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Importiert Konfiguration
|
|
249
|
+
* @param {Object} config - Konfiguration
|
|
250
|
+
*/
|
|
251
|
+
importConfig(config) {
|
|
252
|
+
if (config.defaultQuality) {
|
|
253
|
+
this.defaultQuality = this.validateQuality(config.defaultQuality);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (config.maxQualityForFreeUsers) {
|
|
257
|
+
this.maxQualityForFreeUsers = this.validateQuality(config.maxQualityForFreeUsers);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (config.maxQualityForPremiumUsers) {
|
|
261
|
+
this.maxQualityForPremiumUsers = this.validateQuality(config.maxQualityForPremiumUsers);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (config.userPermissions) {
|
|
265
|
+
this.userPermissions = new Map(Object.entries(config.userPermissions));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
module.exports = QualityManager;
|