ziplayer 0.2.7 → 0.3.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/AI-Guide.md +624 -956
- package/README.md +277 -10
- package/dist/extensions/BaseExtension.d.ts +1 -0
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts +38 -3
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +259 -41
- package/dist/extensions/index.js.map +1 -1
- package/dist/persistence/PersistenceManager.d.ts +95 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +975 -0
- package/dist/persistence/PersistenceManager.js.map +1 -0
- package/dist/plugins/BasePlugin.js +1 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +73 -8
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +647 -116
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.js +3 -3
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/PersistenceManager.d.ts +96 -0
- package/dist/structures/PersistenceManager.d.ts.map +1 -0
- package/dist/structures/PersistenceManager.js +1008 -0
- package/dist/structures/PersistenceManager.js.map +1 -0
- package/dist/structures/Player.d.ts +157 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +1163 -188
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +106 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +365 -124
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +136 -31
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +265 -46
- package/dist/structures/Queue.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +137 -0
- package/dist/structures/StreamManager.d.ts.map +1 -0
- package/dist/structures/StreamManager.js +420 -0
- package/dist/structures/StreamManager.js.map +1 -0
- package/dist/types/index.d.ts +181 -8
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +77 -0
- package/dist/types/persistence.d.ts.map +1 -0
- package/dist/types/persistence.js +3 -0
- package/dist/types/persistence.js.map +1 -0
- package/package.json +3 -2
- package/src/extensions/BaseExtension.ts +1 -0
- package/src/extensions/index.ts +320 -37
- package/src/plugins/BasePlugin.ts +1 -1
- package/src/plugins/index.ts +801 -139
- package/src/structures/FilterManager.ts +3 -3
- package/src/structures/Player.ts +2797 -1693
- package/src/structures/PlayerManager.ts +438 -129
- package/src/structures/Queue.ts +300 -55
- package/src/structures/StreamManager.ts +524 -0
- package/src/types/extension.ts +129 -129
- package/src/types/fillter.ts +264 -264
- package/src/types/index.ts +187 -12
- package/src/types/plugin.ts +59 -59
- package/tsconfig.json +0 -1
|
@@ -3,11 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Player = void 0;
|
|
4
4
|
const events_1 = require("events");
|
|
5
5
|
const voice_1 = require("@discordjs/voice");
|
|
6
|
+
const lru_cache_1 = require("lru-cache");
|
|
6
7
|
const Queue_1 = require("./Queue");
|
|
7
8
|
const plugins_1 = require("../plugins");
|
|
8
9
|
const extensions_1 = require("../extensions");
|
|
9
|
-
const timeout_1 = require("../utils/timeout");
|
|
10
10
|
const FilterManager_1 = require("./FilterManager");
|
|
11
|
+
const StreamManager_1 = require("./StreamManager");
|
|
11
12
|
/**
|
|
12
13
|
* Represents a music player for a specific Discord guild.
|
|
13
14
|
*
|
|
@@ -51,15 +52,79 @@ class Player extends events_1.EventEmitter {
|
|
|
51
52
|
this.volume = 100;
|
|
52
53
|
this.isPlaying = false;
|
|
53
54
|
this.isPaused = false;
|
|
55
|
+
this._lastActivity = Date.now();
|
|
54
56
|
this.leaveTimeout = null;
|
|
55
57
|
this.currentResource = null;
|
|
56
58
|
this.volumeInterval = null;
|
|
59
|
+
this.stuckTimer = null;
|
|
57
60
|
this.skipLoop = false;
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
this.refreshLock = false;
|
|
62
|
+
//preloaded resource
|
|
63
|
+
this.preloadState = {
|
|
64
|
+
resource: null,
|
|
65
|
+
track: null,
|
|
66
|
+
abortController: null,
|
|
67
|
+
timeoutId: null,
|
|
68
|
+
isValid: false,
|
|
69
|
+
isBeingUsed: false,
|
|
70
|
+
};
|
|
71
|
+
this.isPreloading = false;
|
|
72
|
+
this.currentSlot = {
|
|
73
|
+
resource: null,
|
|
74
|
+
track: null,
|
|
75
|
+
streamId: null,
|
|
76
|
+
abortController: null,
|
|
77
|
+
isValid: false,
|
|
78
|
+
isLoading: false,
|
|
79
|
+
loadPromise: null,
|
|
80
|
+
};
|
|
81
|
+
this.preloadSlot = {
|
|
82
|
+
resource: null,
|
|
83
|
+
track: null,
|
|
84
|
+
streamId: null,
|
|
85
|
+
abortController: null,
|
|
86
|
+
isValid: false,
|
|
87
|
+
isLoading: false,
|
|
88
|
+
loadPromise: null,
|
|
89
|
+
};
|
|
90
|
+
this.preloadLock = false;
|
|
91
|
+
this.preloadEnabled = true;
|
|
92
|
+
this.crossfadeEnabled = true;
|
|
93
|
+
this.crossfadeDurationMs = 500;
|
|
94
|
+
this.lowPerformanceMode = false;
|
|
95
|
+
this.crossfadeTransitionLock = false;
|
|
96
|
+
this.smartTransitionEnabled = true;
|
|
97
|
+
this.smartTransitionGenreAware = true;
|
|
98
|
+
this.smartTransitionBeatAlign = true;
|
|
99
|
+
this.smartTransitionBaseMs = 800;
|
|
100
|
+
this.smartTransitionMinMs = 120;
|
|
101
|
+
this.smartTransitionMaxMs = 8000;
|
|
102
|
+
this.smartTransitionGenreDurations = {
|
|
103
|
+
chill: 700,
|
|
104
|
+
ambient: 750,
|
|
105
|
+
lofi: 650,
|
|
106
|
+
pop: 450,
|
|
107
|
+
rock: 350,
|
|
108
|
+
edm: 220,
|
|
109
|
+
house: 250,
|
|
110
|
+
techno: 200,
|
|
111
|
+
};
|
|
112
|
+
this.smartTransitionBeatAlignMaxWaitMs = 180;
|
|
113
|
+
this.antiStuckEnabled = true;
|
|
114
|
+
this.antiStuckMaxRetries = 2;
|
|
115
|
+
this.antiStuckRetryDelayMs = 900;
|
|
116
|
+
this.antiStuckReusePreloadFirst = true;
|
|
117
|
+
this.antiStuckReduceQualityOnRetry = true;
|
|
118
|
+
this.antiStuckControlledSkipThreshold = 3;
|
|
119
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
120
|
+
this.loudnessNormalizationEnabled = false;
|
|
121
|
+
this.loudnessTargetLUFS = -14;
|
|
122
|
+
this.loudnessMaxBoostDb = 8;
|
|
123
|
+
this.loudnessMaxCutDb = 10;
|
|
124
|
+
this.loudnessLimiterCeiling = 0.95;
|
|
60
125
|
this.SEARCH_CACHE_TTL = 2 * 60 * 1000; // 2 minutes
|
|
61
|
-
this.searchCacheTimestamps = new Map();
|
|
62
126
|
this.ttsPlayer = null;
|
|
127
|
+
this.lastDuration = 0;
|
|
63
128
|
this.debug(`[Player] Constructor called for guildId: ${guildId}`);
|
|
64
129
|
this.guildId = guildId;
|
|
65
130
|
this.queue = new Queue_1.Queue();
|
|
@@ -84,17 +149,81 @@ class Player extends events_1.EventEmitter {
|
|
|
84
149
|
createPlayer: false,
|
|
85
150
|
interrupt: true,
|
|
86
151
|
volume: 100,
|
|
87
|
-
|
|
152
|
+
maxTimeTts: 60_000,
|
|
88
153
|
...(options?.tts || {}),
|
|
89
154
|
},
|
|
90
155
|
};
|
|
156
|
+
this.lowPerformanceMode = this.options.lowPerformance ?? this.options.quality === "low";
|
|
157
|
+
const preloadOptions = this.options.preload || {};
|
|
158
|
+
const preloadAutoDisable = preloadOptions.autoDisableInLowPerformance ?? true;
|
|
159
|
+
this.preloadEnabled = preloadOptions.enabled ?? true;
|
|
160
|
+
if (this.lowPerformanceMode && preloadAutoDisable) {
|
|
161
|
+
this.preloadEnabled = false;
|
|
162
|
+
}
|
|
163
|
+
const crossfadeOptions = this.options.crossfade || {};
|
|
164
|
+
const crossfadeAutoEnable = crossfadeOptions.autoEnable ?? true;
|
|
165
|
+
const crossfadeAutoDisable = crossfadeOptions.autoDisableInLowPerformance ?? true;
|
|
166
|
+
this.crossfadeDurationMs = Math.max(0, crossfadeOptions.durationMs ?? 5000);
|
|
167
|
+
if (typeof crossfadeOptions.enabled === "boolean") {
|
|
168
|
+
this.crossfadeEnabled = crossfadeOptions.enabled;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.crossfadeEnabled = crossfadeAutoEnable;
|
|
172
|
+
}
|
|
173
|
+
if (this.lowPerformanceMode && crossfadeAutoDisable) {
|
|
174
|
+
this.crossfadeEnabled = false;
|
|
175
|
+
}
|
|
176
|
+
const smartTransitionOptions = this.options.smartTransition || {};
|
|
177
|
+
this.smartTransitionEnabled = smartTransitionOptions.enabled ?? true;
|
|
178
|
+
this.smartTransitionGenreAware = smartTransitionOptions.genreAware ?? true;
|
|
179
|
+
this.smartTransitionBeatAlign = smartTransitionOptions.beatAlign ?? true;
|
|
180
|
+
this.smartTransitionBaseMs = Math.max(0, smartTransitionOptions.baseDurationMs ?? this.crossfadeDurationMs);
|
|
181
|
+
this.smartTransitionMinMs = Math.max(0, smartTransitionOptions.minDurationMs ?? 1200);
|
|
182
|
+
this.smartTransitionMaxMs = Math.max(this.smartTransitionMinMs, smartTransitionOptions.maxDurationMs ?? 8000);
|
|
183
|
+
this.smartTransitionBeatAlignMaxWaitMs = Math.max(0, smartTransitionOptions.beatAlignMaxWaitMs ?? 1200);
|
|
184
|
+
this.smartTransitionGenreDurations = {
|
|
185
|
+
...this.smartTransitionGenreDurations,
|
|
186
|
+
...(smartTransitionOptions.genreDurations || {}),
|
|
187
|
+
};
|
|
188
|
+
const antiStuckOptions = this.options.antiStuck || {};
|
|
189
|
+
this.antiStuckEnabled = antiStuckOptions.enabled ?? true;
|
|
190
|
+
this.antiStuckMaxRetries = Math.max(0, antiStuckOptions.maxRetries ?? 2);
|
|
191
|
+
this.antiStuckRetryDelayMs = Math.max(0, antiStuckOptions.retryDelayMs ?? 900);
|
|
192
|
+
this.antiStuckReusePreloadFirst = antiStuckOptions.reusePreloadFirst ?? true;
|
|
193
|
+
this.antiStuckReduceQualityOnRetry = antiStuckOptions.reduceQualityOnRetry ?? true;
|
|
194
|
+
this.antiStuckControlledSkipThreshold = Math.max(1, antiStuckOptions.controlledSkipThreshold ?? 3);
|
|
195
|
+
const loudnessOptions = this.options.loudnessNormalization || {};
|
|
196
|
+
this.loudnessNormalizationEnabled = loudnessOptions.enabled ?? false;
|
|
197
|
+
this.loudnessTargetLUFS = loudnessOptions.targetLUFS ?? -14;
|
|
198
|
+
this.loudnessMaxBoostDb = Math.max(0, loudnessOptions.maxBoostDb ?? 8);
|
|
199
|
+
this.loudnessMaxCutDb = Math.max(0, loudnessOptions.maxCutDb ?? 10);
|
|
200
|
+
this.loudnessLimiterCeiling = Math.min(1, Math.max(0.1, loudnessOptions.limiterCeiling ?? 0.95));
|
|
201
|
+
this.debug(`[Player] Runtime options resolved: lowPerformance=${this.lowPerformanceMode}, preload=${this.preloadEnabled}, crossfade=${this.crossfadeEnabled} (${this.crossfadeDurationMs}ms), smartTransition=${this.smartTransitionEnabled}, antiStuck=${this.antiStuckEnabled}, loudnessNormalization=${this.loudnessNormalizationEnabled}`);
|
|
91
202
|
this.filter = new FilterManager_1.FilterManager(this, this.manager);
|
|
92
203
|
this.extensionManager = new extensions_1.ExtensionManager(this, this.manager);
|
|
93
204
|
this.pluginManager = new plugins_1.PluginManager(this, this.manager, {
|
|
94
205
|
extractorTimeout: this.options.extractorTimeout,
|
|
95
206
|
});
|
|
207
|
+
this.streamManager = new StreamManager_1.StreamManager({
|
|
208
|
+
maxConcurrentStreams: 20,
|
|
209
|
+
streamTimeout: 5 * 60 * 1000,
|
|
210
|
+
maxListenersPerStream: 15,
|
|
211
|
+
enableMetrics: true,
|
|
212
|
+
autoDestroy: true,
|
|
213
|
+
});
|
|
96
214
|
this.volume = this.options.volume || 100;
|
|
97
215
|
this.userdata = this.options.userdata;
|
|
216
|
+
this.searchCache = new lru_cache_1.LRUCache({
|
|
217
|
+
max: 200,
|
|
218
|
+
ttl: this.SEARCH_CACHE_TTL,
|
|
219
|
+
dispose: (value, key, reason) => {
|
|
220
|
+
if (this.listenerCount("debug") > 0) {
|
|
221
|
+
this.debug(`[SearchCache] Disposed cache entry: ${key}, reason: ${reason}`);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
allowStale: false,
|
|
225
|
+
updateAgeOnGet: true,
|
|
226
|
+
});
|
|
98
227
|
this.setupEventListeners();
|
|
99
228
|
// Initialize filters from options
|
|
100
229
|
if (this.options.filters && this.options.filters.length > 0) {
|
|
@@ -114,11 +243,12 @@ class Player extends events_1.EventEmitter {
|
|
|
114
243
|
* @private
|
|
115
244
|
*/
|
|
116
245
|
destroyCurrentStream() {
|
|
246
|
+
this.audioPlayer.stop(true);
|
|
117
247
|
if (!this.currentResource)
|
|
118
248
|
return;
|
|
119
249
|
const stream = this.currentResource?.metadata?.stream ?? this.currentResource?.stream;
|
|
120
|
-
if (stream
|
|
121
|
-
stream.destroy();
|
|
250
|
+
if (stream && typeof stream.destroy === "function") {
|
|
251
|
+
stream.destroy().catch((e) => this.debug("Stream destroy error:", e));
|
|
122
252
|
}
|
|
123
253
|
this.currentResource = null;
|
|
124
254
|
}
|
|
@@ -135,12 +265,7 @@ class Player extends events_1.EventEmitter {
|
|
|
135
265
|
*/
|
|
136
266
|
async search(query, requestedBy) {
|
|
137
267
|
this.debug(`[Player] Search called with query: ${query}, requestedBy: ${requestedBy}`);
|
|
138
|
-
//
|
|
139
|
-
if (Math.random() < 0.1) {
|
|
140
|
-
// 10% chance to clean cache
|
|
141
|
-
this.clearExpiredSearchCache();
|
|
142
|
-
}
|
|
143
|
-
// Check cache first
|
|
268
|
+
// Check player cache first (LRU)
|
|
144
269
|
const cachedResult = this.getCachedSearchResult(query);
|
|
145
270
|
if (cachedResult) {
|
|
146
271
|
return cachedResult;
|
|
@@ -152,44 +277,18 @@ class Player extends events_1.EventEmitter {
|
|
|
152
277
|
this.cacheSearchResult(query, extensionResult);
|
|
153
278
|
return extensionResult;
|
|
154
279
|
}
|
|
155
|
-
//
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
this.debug(`[Player]
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
return true;
|
|
164
|
-
});
|
|
165
|
-
this.debug(`[Player] Using ${plugins.length} plugins for search (filtered from ${allPlugins.length})`);
|
|
166
|
-
let lastError = null;
|
|
167
|
-
let searchAttempts = 0;
|
|
168
|
-
for (const p of plugins) {
|
|
169
|
-
searchAttempts++;
|
|
170
|
-
try {
|
|
171
|
-
this.debug(`[Player] Trying plugin for search: ${p.name} (attempt ${searchAttempts}/${plugins.length})`);
|
|
172
|
-
const startTime = Date.now();
|
|
173
|
-
const res = await (0, timeout_1.withTimeout)(p.search(query, requestedBy), this.options.extractorTimeout ?? 15000, `Search operation timed out for ${p.name}`);
|
|
174
|
-
const duration = Date.now() - startTime;
|
|
175
|
-
if (res && Array.isArray(res.tracks) && res.tracks.length > 0) {
|
|
176
|
-
this.debug(`[Player] Plugin '${p.name}' returned ${res.tracks.length} tracks in ${duration}ms`);
|
|
177
|
-
this.cacheSearchResult(query, res);
|
|
178
|
-
return res;
|
|
179
|
-
}
|
|
180
|
-
this.debug(`[Player] Plugin '${p.name}' returned no tracks in ${duration}ms`);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
184
|
-
this.debug(`[Player] Search via plugin '${p.name}' failed: ${errorMessage}`);
|
|
185
|
-
lastError = error;
|
|
186
|
-
// Continue to next plugin
|
|
280
|
+
// Use PluginManager for search with deduplication and evaluation
|
|
281
|
+
const pluginResult = await this.pluginManager.search(query, requestedBy);
|
|
282
|
+
if (pluginResult && pluginResult.tracks.length > 0) {
|
|
283
|
+
this.debug(`[Player] Plugin search returned ${pluginResult.tracks.length} tracks (score: ${pluginResult.score?.score}%)`);
|
|
284
|
+
if (pluginResult.score) {
|
|
285
|
+
this.debug(`[Player] Search evaluation - ${pluginResult.score.reason}`);
|
|
187
286
|
}
|
|
287
|
+
this.cacheSearchResult(query, pluginResult);
|
|
288
|
+
return pluginResult;
|
|
188
289
|
}
|
|
189
|
-
this.debug(`[Player] No
|
|
190
|
-
|
|
191
|
-
this.emit("playerError", lastError);
|
|
192
|
-
throw new Error(`No plugin found to handle: ${query}`);
|
|
290
|
+
this.debug(`[Player] No search results for query: ${query}`);
|
|
291
|
+
throw new Error(`No results found for: ${query}`);
|
|
193
292
|
}
|
|
194
293
|
/**
|
|
195
294
|
* Get cached search result or null if not found/expired
|
|
@@ -198,14 +297,10 @@ class Player extends events_1.EventEmitter {
|
|
|
198
297
|
*/
|
|
199
298
|
getCachedSearchResult(query) {
|
|
200
299
|
const cacheKey = query.toLowerCase().trim();
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (cachedResult) {
|
|
206
|
-
this.debug(`[SearchCache] Using cached search result for: ${query}`);
|
|
207
|
-
return cachedResult;
|
|
208
|
-
}
|
|
300
|
+
const cached = this.searchCache.get(cacheKey);
|
|
301
|
+
if (cached) {
|
|
302
|
+
this.debug(`[SearchCache] Using cached search result for: ${query}`);
|
|
303
|
+
return cached;
|
|
209
304
|
}
|
|
210
305
|
return null;
|
|
211
306
|
}
|
|
@@ -216,23 +311,15 @@ class Player extends events_1.EventEmitter {
|
|
|
216
311
|
*/
|
|
217
312
|
cacheSearchResult(query, result) {
|
|
218
313
|
const cacheKey = query.toLowerCase().trim();
|
|
219
|
-
const now = Date.now();
|
|
220
314
|
this.searchCache.set(cacheKey, result);
|
|
221
|
-
this.searchCacheTimestamps.set(cacheKey, now);
|
|
222
315
|
this.debug(`[SearchCache] Cached search result for: ${query} (${result.tracks.length} tracks)`);
|
|
223
316
|
}
|
|
224
317
|
/**
|
|
225
318
|
* Clear expired search cache entries
|
|
226
319
|
*/
|
|
227
320
|
clearExpiredSearchCache() {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (now - timestamp >= this.SEARCH_CACHE_TTL) {
|
|
231
|
-
this.searchCache.delete(key);
|
|
232
|
-
this.searchCacheTimestamps.delete(key);
|
|
233
|
-
this.debug(`[SearchCache] Cleared expired cache entry: ${key}`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
321
|
+
this.searchCache.purgeStale();
|
|
322
|
+
this.debug(`[SearchCache] Purged stale search cache entries`);
|
|
236
323
|
}
|
|
237
324
|
/**
|
|
238
325
|
* Clear all search cache entries
|
|
@@ -242,7 +329,6 @@ class Player extends events_1.EventEmitter {
|
|
|
242
329
|
clearSearchCache() {
|
|
243
330
|
const cacheSize = this.searchCache.size;
|
|
244
331
|
this.searchCache.clear();
|
|
245
|
-
this.searchCacheTimestamps.clear();
|
|
246
332
|
this.debug(`[SearchCache] Cleared all ${cacheSize} search cache entries`);
|
|
247
333
|
}
|
|
248
334
|
/**
|
|
@@ -252,9 +338,8 @@ class Player extends events_1.EventEmitter {
|
|
|
252
338
|
*/
|
|
253
339
|
debugSearchQuery(query) {
|
|
254
340
|
const cacheKey = query.toLowerCase().trim();
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
const isCached = cachedTimestamp && now - cachedTimestamp < this.SEARCH_CACHE_TTL;
|
|
341
|
+
const cached = this.searchCache.get(cacheKey);
|
|
342
|
+
const isCached = !!cached;
|
|
258
343
|
const allPlugins = this.pluginManager.getAll();
|
|
259
344
|
const plugins = allPlugins.filter((p) => {
|
|
260
345
|
if (p.name.toLowerCase() === "tts" && !query.toLowerCase().startsWith("tts:")) {
|
|
@@ -263,8 +348,8 @@ class Player extends events_1.EventEmitter {
|
|
|
263
348
|
return true;
|
|
264
349
|
});
|
|
265
350
|
return {
|
|
266
|
-
isCached
|
|
267
|
-
cacheAge:
|
|
351
|
+
isCached,
|
|
352
|
+
cacheAge: undefined,
|
|
268
353
|
pluginCount: plugins.length,
|
|
269
354
|
ttsFiltered: allPlugins.length > plugins.length,
|
|
270
355
|
};
|
|
@@ -334,7 +419,7 @@ class Player extends events_1.EventEmitter {
|
|
|
334
419
|
}
|
|
335
420
|
else {
|
|
336
421
|
// Handle other types (string, Track)
|
|
337
|
-
const hookOutcome = await this.extensionManager.
|
|
422
|
+
const hookOutcome = await this.extensionManager.beforePlayHooks(effectiveRequest);
|
|
338
423
|
effectiveRequest = hookOutcome.request;
|
|
339
424
|
hookResponse = hookOutcome.response;
|
|
340
425
|
if (effectiveRequest.requestedBy === undefined) {
|
|
@@ -350,7 +435,7 @@ class Player extends events_1.EventEmitter {
|
|
|
350
435
|
isPlaylist: hookResponse.isPlaylist ?? false,
|
|
351
436
|
error: hookResponse.error,
|
|
352
437
|
};
|
|
353
|
-
await this.extensionManager.
|
|
438
|
+
await this.extensionManager.afterPlayHooks(handledPayload);
|
|
354
439
|
if (hookResponse.error) {
|
|
355
440
|
this.emit("playerError", hookResponse.error);
|
|
356
441
|
}
|
|
@@ -393,7 +478,7 @@ class Player extends events_1.EventEmitter {
|
|
|
393
478
|
(isTTS(tracksToAdd[0]) || queryLooksTTS)) {
|
|
394
479
|
this.debug(`[Player] Interrupting with TTS: ${tracksToAdd[0].title}`);
|
|
395
480
|
await this.interruptWithTTSTrack(tracksToAdd[0]);
|
|
396
|
-
await this.extensionManager.
|
|
481
|
+
await this.extensionManager.afterPlayHooks({
|
|
397
482
|
success: true,
|
|
398
483
|
query: effectiveRequest.query,
|
|
399
484
|
requestedBy: effectiveRequest.requestedBy,
|
|
@@ -411,7 +496,7 @@ class Player extends events_1.EventEmitter {
|
|
|
411
496
|
this.emit("queueAdd", tracksToAdd[0]);
|
|
412
497
|
}
|
|
413
498
|
const started = !this.isPlaying ? await this.playNext() : true;
|
|
414
|
-
await this.extensionManager.
|
|
499
|
+
await this.extensionManager.afterPlayHooks({
|
|
415
500
|
success: started,
|
|
416
501
|
query: effectiveRequest.query,
|
|
417
502
|
requestedBy: effectiveRequest.requestedBy,
|
|
@@ -421,7 +506,7 @@ class Player extends events_1.EventEmitter {
|
|
|
421
506
|
return started;
|
|
422
507
|
}
|
|
423
508
|
catch (error) {
|
|
424
|
-
await this.extensionManager.
|
|
509
|
+
await this.extensionManager.afterPlayHooks({
|
|
425
510
|
success: false,
|
|
426
511
|
query: effectiveRequest.query,
|
|
427
512
|
requestedBy: effectiveRequest.requestedBy,
|
|
@@ -434,6 +519,537 @@ class Player extends events_1.EventEmitter {
|
|
|
434
519
|
return false;
|
|
435
520
|
}
|
|
436
521
|
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region Preload
|
|
524
|
+
/**
|
|
525
|
+
* Main preload method - only one at a time
|
|
526
|
+
*/
|
|
527
|
+
async preloadNextTrack() {
|
|
528
|
+
if (!this.preloadEnabled) {
|
|
529
|
+
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// Prevent concurrent preloads
|
|
533
|
+
if (this.preloadLock) {
|
|
534
|
+
this.debug(`[Preload] Already preloading, skipping`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const nextTrack = this.queue.nextTrack;
|
|
538
|
+
if (!nextTrack) {
|
|
539
|
+
this.debug(`[Preload] No next track to preload`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
// Check if already preloaded correctly
|
|
543
|
+
if (this.preloadSlot.isValid && this.preloadSlot.track?.id === nextTrack.id && this.preloadSlot.resource) {
|
|
544
|
+
this.debug(`[Preload] Already have valid preload for: ${nextTrack.title}`);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// Check if currently loading the same track
|
|
548
|
+
if (this.preloadSlot.isLoading && this.preloadSlot.track?.id === nextTrack.id) {
|
|
549
|
+
this.debug(`[Preload] Currently loading same track, waiting...`);
|
|
550
|
+
if (this.preloadSlot.loadPromise) {
|
|
551
|
+
await this.preloadSlot.loadPromise;
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Cancel old preload if different track
|
|
556
|
+
if (this.preloadSlot.isValid && this.preloadSlot.track?.id !== nextTrack.id) {
|
|
557
|
+
this.debug(`[Preload] Cancelling old preload for different track: ${this.preloadSlot.track?.title}`);
|
|
558
|
+
await this.safeCancelPreload();
|
|
559
|
+
}
|
|
560
|
+
this.preloadLock = true;
|
|
561
|
+
// Create new abort controller
|
|
562
|
+
const abortController = new AbortController();
|
|
563
|
+
// Setup preload slot
|
|
564
|
+
this.preloadSlot.track = nextTrack;
|
|
565
|
+
this.preloadSlot.abortController = abortController;
|
|
566
|
+
this.preloadSlot.isLoading = true;
|
|
567
|
+
// Create load promise
|
|
568
|
+
const loadPromise = this.executePreload(nextTrack, abortController);
|
|
569
|
+
this.preloadSlot.loadPromise = loadPromise;
|
|
570
|
+
try {
|
|
571
|
+
await loadPromise;
|
|
572
|
+
}
|
|
573
|
+
catch (err) {
|
|
574
|
+
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
575
|
+
this.debug(`[Preload] Cancelled for ${nextTrack.title}`);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
this.debug(`[Preload] Failed for ${nextTrack.title}:`, err);
|
|
579
|
+
}
|
|
580
|
+
this.clearSlot(this.preloadSlot);
|
|
581
|
+
}
|
|
582
|
+
finally {
|
|
583
|
+
this.preloadLock = false;
|
|
584
|
+
this.preloadSlot.isLoading = false;
|
|
585
|
+
this.preloadSlot.loadPromise = null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Execute actual preload
|
|
590
|
+
*/
|
|
591
|
+
async executePreload(track, abortController) {
|
|
592
|
+
this.debug(`[Preload] Starting preload for: ${track.title}`);
|
|
593
|
+
// Check for cancellation
|
|
594
|
+
if (abortController.signal.aborted) {
|
|
595
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
596
|
+
}
|
|
597
|
+
// Check if track still relevant
|
|
598
|
+
if (this.queue.nextTrack?.id !== track.id) {
|
|
599
|
+
this.debug(`[Preload] Track changed, cancelling`);
|
|
600
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
// Get stream with abort support - NO TIMEOUT
|
|
604
|
+
const streamInfo = await this.getStreamWithCancel(track, abortController.signal);
|
|
605
|
+
// Check cancellation
|
|
606
|
+
if (abortController.signal.aborted) {
|
|
607
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
608
|
+
}
|
|
609
|
+
// Check track relevance again
|
|
610
|
+
if (this.queue.nextTrack?.id !== track.id) {
|
|
611
|
+
this.debug(`[Preload] Track changed after stream fetch`);
|
|
612
|
+
throw new Error("PRELOAD_CANCELLED");
|
|
613
|
+
}
|
|
614
|
+
if (!streamInfo?.stream) {
|
|
615
|
+
throw new Error(`No stream available`);
|
|
616
|
+
}
|
|
617
|
+
// Register with StreamManager as preload
|
|
618
|
+
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
619
|
+
source: track.source || "preload",
|
|
620
|
+
isPreload: true,
|
|
621
|
+
priority: 5,
|
|
622
|
+
});
|
|
623
|
+
// Create resource
|
|
624
|
+
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
625
|
+
inlineVolume: true,
|
|
626
|
+
metadata: { ...track, preloaded: true },
|
|
627
|
+
});
|
|
628
|
+
// Verify resource is valid
|
|
629
|
+
if (!resource.playStream || resource.playStream.readable === false) {
|
|
630
|
+
throw new Error("Resource not readable");
|
|
631
|
+
}
|
|
632
|
+
// Update preload slot
|
|
633
|
+
this.preloadSlot.resource = resource;
|
|
634
|
+
this.preloadSlot.streamId = streamId;
|
|
635
|
+
this.preloadSlot.isValid = true;
|
|
636
|
+
this.preloadSlot.track = track;
|
|
637
|
+
this.debug(`[Preload] Successfully preloaded: ${track.title} (Stream ID: ${streamId})`);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
this.debug(`[Preload] Error during preload:`, err);
|
|
644
|
+
throw err;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Safe cancel preload - doesn't throw
|
|
649
|
+
*/
|
|
650
|
+
async safeCancelPreload() {
|
|
651
|
+
if (!this.preloadSlot.abortController && !this.preloadSlot.resource) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this.debug(`[Preload] Safely cancelling preload for: ${this.preloadSlot.track?.title || "unknown"}`);
|
|
655
|
+
// Abort the operation
|
|
656
|
+
if (this.preloadSlot.abortController) {
|
|
657
|
+
this.preloadSlot.abortController.abort();
|
|
658
|
+
this.preloadSlot.abortController = null;
|
|
659
|
+
}
|
|
660
|
+
// Clean up stream
|
|
661
|
+
if (this.preloadSlot.streamId && this.streamManager) {
|
|
662
|
+
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
663
|
+
}
|
|
664
|
+
// Clean up resource
|
|
665
|
+
if (this.preloadSlot.resource) {
|
|
666
|
+
try {
|
|
667
|
+
const stream = this.preloadSlot.resource.playStream;
|
|
668
|
+
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
669
|
+
stream.destroy();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch (err) {
|
|
673
|
+
// Ignore destroy errors
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// Clear slot
|
|
677
|
+
this.clearSlot(this.preloadSlot);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get stream with proper cancellation
|
|
681
|
+
*/
|
|
682
|
+
async getStreamWithCancel(track, signal) {
|
|
683
|
+
// Create abort promise
|
|
684
|
+
const abortPromise = new Promise((_, reject) => {
|
|
685
|
+
if (signal.aborted) {
|
|
686
|
+
reject(new Error("PRELOAD_CANCELLED"));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const handler = () => {
|
|
690
|
+
signal.removeEventListener("abort", handler);
|
|
691
|
+
reject(new Error("PRELOAD_CANCELLED"));
|
|
692
|
+
};
|
|
693
|
+
signal.addEventListener("abort", handler);
|
|
694
|
+
});
|
|
695
|
+
try {
|
|
696
|
+
// Check if stream already exists and is valid
|
|
697
|
+
const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
|
|
698
|
+
if (existingStream && !existingStream.destroyed && existingStream.readable !== false) {
|
|
699
|
+
this.debug(`[Stream] Using existing stream for preload: ${track.title}`);
|
|
700
|
+
return { stream: existingStream, type: "arbitrary" };
|
|
701
|
+
}
|
|
702
|
+
// Race between stream fetch and abort
|
|
703
|
+
const streamPromise = this.getStream(track);
|
|
704
|
+
const result = await Promise.race([streamPromise, abortPromise]);
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
if (err instanceof Error && err.message === "PRELOAD_CANCELLED") {
|
|
709
|
+
throw err;
|
|
710
|
+
}
|
|
711
|
+
throw err;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Preload next track with proper error handling and cleanup
|
|
716
|
+
*/
|
|
717
|
+
async preloadNext() {
|
|
718
|
+
if (!this.preloadEnabled) {
|
|
719
|
+
this.debug(`[Preload] Disabled by options/runtime profile`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
this.cancelPreload();
|
|
723
|
+
const next = this.queue.nextTrack;
|
|
724
|
+
if (!next || this.isPreloading) {
|
|
725
|
+
this.debug(`[Preload] Skipped - ${!next ? "no next track" : "already preloading"}`);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
this.isPreloading = true;
|
|
729
|
+
// Create new AbortController
|
|
730
|
+
const abortController = new AbortController();
|
|
731
|
+
const timeoutId = setTimeout(() => {
|
|
732
|
+
// this.debug(`[Preload] Timeout for track: ${next.title}`);
|
|
733
|
+
// abortController.abort();
|
|
734
|
+
}, 30000);
|
|
735
|
+
this.preloadState.abortController = abortController;
|
|
736
|
+
this.preloadState.timeoutId = timeoutId;
|
|
737
|
+
try {
|
|
738
|
+
this.debug(`[Preload] Starting preload for: ${next.title}`);
|
|
739
|
+
// Check if already aborted
|
|
740
|
+
if (abortController.signal.aborted) {
|
|
741
|
+
throw new Error("Preload aborted before start");
|
|
742
|
+
}
|
|
743
|
+
// Check if this track is still the next one
|
|
744
|
+
if (this.queue.nextTrack?.id !== next.id) {
|
|
745
|
+
this.debug(`[Preload] Track changed, cancelling preload`);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const streamInfo = await this.getStreamWithCancel(next, abortController.signal);
|
|
749
|
+
// Double check
|
|
750
|
+
if (abortController.signal.aborted) {
|
|
751
|
+
throw new Error("Preload aborted after stream fetch");
|
|
752
|
+
}
|
|
753
|
+
if (this.queue.nextTrack?.id !== next.id) {
|
|
754
|
+
this.debug(`[Preload] Track changed after stream fetch`);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (!streamInfo?.stream) {
|
|
758
|
+
throw new Error(`No stream available`);
|
|
759
|
+
}
|
|
760
|
+
// Register with StreamManager
|
|
761
|
+
const streamId = this.streamManager.registerStream(streamInfo.stream, next, {
|
|
762
|
+
source: next.source || "preload",
|
|
763
|
+
isPreload: true,
|
|
764
|
+
priority: 8,
|
|
765
|
+
});
|
|
766
|
+
// Create resource
|
|
767
|
+
const resource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
768
|
+
inlineVolume: true,
|
|
769
|
+
metadata: { ...next, preloaded: true },
|
|
770
|
+
});
|
|
771
|
+
// Store preload state
|
|
772
|
+
this.preloadState = {
|
|
773
|
+
resource,
|
|
774
|
+
track: next,
|
|
775
|
+
abortController,
|
|
776
|
+
timeoutId,
|
|
777
|
+
isValid: true,
|
|
778
|
+
isBeingUsed: false,
|
|
779
|
+
streamId,
|
|
780
|
+
};
|
|
781
|
+
this.debug(`[Preload] Successfully preloaded: ${next.title} (Stream ID: ${streamId})`);
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
if (err instanceof Error && err.message.includes("aborted")) {
|
|
785
|
+
this.debug(`[Preload] Cancelled for ${next.title}`);
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
this.debug(`[Preload] Failed for ${next?.title}:`, err);
|
|
789
|
+
}
|
|
790
|
+
this.cancelPreload();
|
|
791
|
+
}
|
|
792
|
+
finally {
|
|
793
|
+
this.isPreloading = false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async fadeResourceVolume(resource, from, to, durationMs) {
|
|
797
|
+
if (!resource.volume)
|
|
798
|
+
return;
|
|
799
|
+
const safeDuration = Math.max(0, durationMs);
|
|
800
|
+
if (safeDuration === 0) {
|
|
801
|
+
resource.volume.setVolume(to);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const steps = Math.max(1, Math.floor(safeDuration / 50));
|
|
805
|
+
const stepDuration = Math.max(20, Math.floor(safeDuration / steps));
|
|
806
|
+
const delta = (to - from) / steps;
|
|
807
|
+
resource.volume.setVolume(from);
|
|
808
|
+
for (let i = 1; i <= steps; i++) {
|
|
809
|
+
await new Promise((resolve) => setTimeout(resolve, stepDuration));
|
|
810
|
+
resource.volume.setVolume(from + delta * i);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async applyCrossfadeIn(resource, track) {
|
|
814
|
+
if (!this.crossfadeEnabled || !resource.volume)
|
|
815
|
+
return;
|
|
816
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
817
|
+
const transitionMs = this.resolveSmartTransitionDuration(track);
|
|
818
|
+
await this.fadeResourceVolume(resource, 0, targetVolume, transitionMs);
|
|
819
|
+
}
|
|
820
|
+
async applyCrossfadeOutCurrent() {
|
|
821
|
+
if (!this.crossfadeEnabled)
|
|
822
|
+
return;
|
|
823
|
+
const current = this.currentSlot.resource || this.currentResource;
|
|
824
|
+
if (!current?.volume)
|
|
825
|
+
return;
|
|
826
|
+
const currentVolume = current.volume.volume ?? this.volume / 100;
|
|
827
|
+
const currentTrack = this.queue.currentTrack;
|
|
828
|
+
const transitionMs = currentTrack ? this.resolveSmartTransitionDuration(currentTrack) : this.resolveSmartTransitionDuration({});
|
|
829
|
+
await this.fadeResourceVolume(current, currentVolume, 0, transitionMs);
|
|
830
|
+
}
|
|
831
|
+
async crossfadeSkipAndStop() {
|
|
832
|
+
if (!this.crossfadeEnabled) {
|
|
833
|
+
this.audioPlayer.stop();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (this.crossfadeTransitionLock) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
this.crossfadeTransitionLock = true;
|
|
840
|
+
try {
|
|
841
|
+
await this.applyCrossfadeOutCurrent();
|
|
842
|
+
this.audioPlayer.stop();
|
|
843
|
+
}
|
|
844
|
+
finally {
|
|
845
|
+
this.crossfadeTransitionLock = false;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
getTrackMetadataValue(track, key) {
|
|
849
|
+
const md = track?.metadata;
|
|
850
|
+
if (!md)
|
|
851
|
+
return undefined;
|
|
852
|
+
return md[key];
|
|
853
|
+
}
|
|
854
|
+
resolveSmartTransitionDuration(track) {
|
|
855
|
+
if (!this.smartTransitionEnabled) {
|
|
856
|
+
return this.crossfadeDurationMs;
|
|
857
|
+
}
|
|
858
|
+
let duration = this.smartTransitionBaseMs;
|
|
859
|
+
if (this.smartTransitionGenreAware) {
|
|
860
|
+
const rawGenre = this.getTrackMetadataValue(track, "genre");
|
|
861
|
+
const genre = typeof rawGenre === "string" ? rawGenre.toLowerCase().trim() : "";
|
|
862
|
+
if (genre && this.smartTransitionGenreDurations[genre] !== undefined) {
|
|
863
|
+
duration = this.smartTransitionGenreDurations[genre];
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return Math.min(this.smartTransitionMaxMs, Math.max(this.smartTransitionMinMs, duration));
|
|
867
|
+
}
|
|
868
|
+
async maybeAlignToBeatBoundary() {
|
|
869
|
+
if (!this.smartTransitionEnabled || !this.smartTransitionBeatAlign)
|
|
870
|
+
return;
|
|
871
|
+
const currentTrack = this.queue.currentTrack;
|
|
872
|
+
if (!currentTrack || !this.currentResource)
|
|
873
|
+
return;
|
|
874
|
+
const bpmRaw = this.getTrackMetadataValue(currentTrack, "bpm");
|
|
875
|
+
const bpm = typeof bpmRaw === "number" ? bpmRaw : Number(bpmRaw);
|
|
876
|
+
if (!Number.isFinite(bpm) || bpm <= 0)
|
|
877
|
+
return;
|
|
878
|
+
const beatMs = 60000 / bpm;
|
|
879
|
+
const positionMs = this.currentResource.playbackDuration;
|
|
880
|
+
const remainder = positionMs % beatMs;
|
|
881
|
+
const waitMs = beatMs - remainder;
|
|
882
|
+
if (waitMs > 0 && waitMs <= this.smartTransitionBeatAlignMaxWaitMs) {
|
|
883
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
getTrackTargetVolume(track) {
|
|
887
|
+
const base = this.volume / 100;
|
|
888
|
+
if (!this.loudnessNormalizationEnabled) {
|
|
889
|
+
return base;
|
|
890
|
+
}
|
|
891
|
+
const lufsRaw = this.getTrackMetadataValue(track, "lufs");
|
|
892
|
+
const trackLufs = typeof lufsRaw === "number" ? lufsRaw : Number(lufsRaw);
|
|
893
|
+
if (!Number.isFinite(trackLufs)) {
|
|
894
|
+
return Math.min(base, this.loudnessLimiterCeiling);
|
|
895
|
+
}
|
|
896
|
+
const deltaDbRaw = this.loudnessTargetLUFS - trackLufs;
|
|
897
|
+
const maxBoost = this.loudnessMaxBoostDb;
|
|
898
|
+
const maxCut = this.loudnessMaxCutDb;
|
|
899
|
+
const deltaDb = Math.max(-maxCut, Math.min(maxBoost, deltaDbRaw));
|
|
900
|
+
const multiplier = Math.pow(10, deltaDb / 20);
|
|
901
|
+
const adjusted = base * multiplier;
|
|
902
|
+
return Math.min(this.loudnessLimiterCeiling, Math.max(0, adjusted));
|
|
903
|
+
}
|
|
904
|
+
async attemptTrackRecovery(track, reason) {
|
|
905
|
+
if (!this.antiStuckEnabled)
|
|
906
|
+
return false;
|
|
907
|
+
this.debug(`[AntiStuck] Recovery started for: ${track.title}`, reason);
|
|
908
|
+
const originalQuality = this.options.quality;
|
|
909
|
+
let attempted = 0;
|
|
910
|
+
while (attempted < this.antiStuckMaxRetries) {
|
|
911
|
+
attempted++;
|
|
912
|
+
if (this.antiStuckReduceQualityOnRetry) {
|
|
913
|
+
this.options.quality = "low";
|
|
914
|
+
}
|
|
915
|
+
if (this.antiStuckRetryDelayMs > 0) {
|
|
916
|
+
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
917
|
+
}
|
|
918
|
+
try {
|
|
919
|
+
if (this.antiStuckReusePreloadFirst && this.preloadSlot.isValid && this.preloadSlot.track?.id === track.id) {
|
|
920
|
+
const startedFromPreload = await this.startTrack(track);
|
|
921
|
+
if (startedFromPreload) {
|
|
922
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
923
|
+
this.options.quality = originalQuality;
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const started = await this.loadFreshStream(track);
|
|
928
|
+
if (started) {
|
|
929
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
930
|
+
this.options.quality = originalQuality;
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
this.debug(`[AntiStuck] Retry ${attempted} failed for ${track.title}:`, error);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
this.options.quality = originalQuality;
|
|
939
|
+
this.antiStuckConsecutiveFailures++;
|
|
940
|
+
if (this.antiStuckConsecutiveFailures >= this.antiStuckControlledSkipThreshold) {
|
|
941
|
+
this.debug(`[AntiStuck] Controlled skip threshold reached for ${track.title}`);
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
// Avoid hard skip storm by leaving track for next natural retry window.
|
|
945
|
+
this.debug(`[AntiStuck] Keeping track for controlled retry window: ${track.title}`);
|
|
946
|
+
return false;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Clear preloaded resource with proper cleanup
|
|
950
|
+
*/
|
|
951
|
+
clearPreload() {
|
|
952
|
+
// Abort ongoing preload
|
|
953
|
+
if (this.preloadState.abortController) {
|
|
954
|
+
this.preloadState.abortController.abort();
|
|
955
|
+
this.preloadState.abortController = null;
|
|
956
|
+
}
|
|
957
|
+
// Clean up stream
|
|
958
|
+
const stream = this.preloadState.stream;
|
|
959
|
+
if (stream && typeof stream.destroy === "function") {
|
|
960
|
+
try {
|
|
961
|
+
stream.destroy();
|
|
962
|
+
}
|
|
963
|
+
catch (err) {
|
|
964
|
+
this.debug(`[Preload] Error destroying stream:`, err);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Clean up resource
|
|
968
|
+
if (this.preloadState.resource) {
|
|
969
|
+
try {
|
|
970
|
+
const playStream = this.preloadState.resource.playStream;
|
|
971
|
+
if (playStream && typeof playStream.destroy === "function") {
|
|
972
|
+
playStream.destroy();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
this.debug(`[Preload] Error destroying resource:`, err);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
this.preloadState = {
|
|
980
|
+
resource: null,
|
|
981
|
+
track: null,
|
|
982
|
+
abortController: null,
|
|
983
|
+
timeoutId: null,
|
|
984
|
+
isValid: false,
|
|
985
|
+
isBeingUsed: false,
|
|
986
|
+
streamId: undefined,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Cancel preload (when skipping or stopping)
|
|
991
|
+
*/
|
|
992
|
+
cancelPreload() {
|
|
993
|
+
if (this.preloadSlot.abortController) {
|
|
994
|
+
this.debug(`[Preload] Cancelling preload for: ${this.preloadSlot.track?.title}`);
|
|
995
|
+
this.preloadSlot.abortController.abort();
|
|
996
|
+
}
|
|
997
|
+
if (this.preloadSlot.streamId && this.streamManager) {
|
|
998
|
+
this.streamManager.unregisterStream(this.preloadSlot.streamId, true);
|
|
999
|
+
}
|
|
1000
|
+
this.clearSlot(this.preloadSlot);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Clear a stream slot
|
|
1004
|
+
*/
|
|
1005
|
+
clearSlot(slot) {
|
|
1006
|
+
if (slot.resource) {
|
|
1007
|
+
try {
|
|
1008
|
+
const stream = slot.resource.playStream;
|
|
1009
|
+
if (stream && typeof stream.destroy === "function" && !stream.destroyed) {
|
|
1010
|
+
stream.destroy();
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
// Ignore
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (slot.streamId && this.streamManager) {
|
|
1018
|
+
// Don't wait for unregister
|
|
1019
|
+
this.streamManager.unregisterStream(slot.streamId, true);
|
|
1020
|
+
}
|
|
1021
|
+
slot.resource = null;
|
|
1022
|
+
slot.track = null;
|
|
1023
|
+
slot.streamId = null;
|
|
1024
|
+
slot.abortController = null;
|
|
1025
|
+
slot.isValid = false;
|
|
1026
|
+
slot.isLoading = false;
|
|
1027
|
+
slot.loadPromise = null;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Promote preload slot to current slot without destroying promoted stream.
|
|
1031
|
+
*/
|
|
1032
|
+
promotePreloadToCurrent(track) {
|
|
1033
|
+
const promotedResource = this.preloadSlot.resource;
|
|
1034
|
+
const promotedStreamId = this.preloadSlot.streamId;
|
|
1035
|
+
// Move ownership to current slot.
|
|
1036
|
+
this.currentSlot.resource = promotedResource;
|
|
1037
|
+
this.currentSlot.track = track;
|
|
1038
|
+
this.currentSlot.streamId = promotedStreamId;
|
|
1039
|
+
this.currentSlot.abortController = null;
|
|
1040
|
+
this.currentSlot.isValid = !!promotedResource;
|
|
1041
|
+
this.currentSlot.isLoading = false;
|
|
1042
|
+
this.currentSlot.loadPromise = null;
|
|
1043
|
+
this.currentResource = promotedResource;
|
|
1044
|
+
// Reset preload slot only (do not destroy promoted resource/stream).
|
|
1045
|
+
this.preloadSlot.resource = null;
|
|
1046
|
+
this.preloadSlot.track = null;
|
|
1047
|
+
this.preloadSlot.streamId = null;
|
|
1048
|
+
this.preloadSlot.abortController = null;
|
|
1049
|
+
this.preloadSlot.isValid = false;
|
|
1050
|
+
this.preloadSlot.isLoading = false;
|
|
1051
|
+
this.preloadSlot.loadPromise = null;
|
|
1052
|
+
}
|
|
437
1053
|
/**
|
|
438
1054
|
* Create AudioResource with filters and seek applied
|
|
439
1055
|
*
|
|
@@ -482,12 +1098,40 @@ class Player extends events_1.EventEmitter {
|
|
|
482
1098
|
}
|
|
483
1099
|
}
|
|
484
1100
|
async getStream(track) {
|
|
1101
|
+
const trackId = track.id || track.url || track.title;
|
|
1102
|
+
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
1103
|
+
if (existingStream && !existingStream.destroyed) {
|
|
1104
|
+
this.debug(`[Stream] Using existing stream from manager for: ${track.title}`);
|
|
1105
|
+
return { stream: existingStream, type: "arbitrary" };
|
|
1106
|
+
}
|
|
485
1107
|
let stream = await this.extensionManager.provideStream(track);
|
|
486
|
-
if (stream?.stream)
|
|
1108
|
+
if (stream?.stream) {
|
|
1109
|
+
// Register with StreamManager
|
|
1110
|
+
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1111
|
+
source: "extension",
|
|
1112
|
+
isPreload: false,
|
|
1113
|
+
priority: 10,
|
|
1114
|
+
});
|
|
1115
|
+
this.debug(`[Stream] Extension stream registered with ID: ${streamId}`);
|
|
487
1116
|
return stream;
|
|
1117
|
+
}
|
|
488
1118
|
stream = await this.pluginManager.getStream(track);
|
|
489
|
-
if (stream?.stream)
|
|
1119
|
+
if (stream?.stream) {
|
|
1120
|
+
const existingAgain = this.streamManager.getStreamByTrack(trackId);
|
|
1121
|
+
if (existingAgain && !existingAgain.destroyed) {
|
|
1122
|
+
if (stream.stream.destroy)
|
|
1123
|
+
stream.stream.destroy();
|
|
1124
|
+
return { stream: existingAgain, type: "arbitrary" };
|
|
1125
|
+
}
|
|
1126
|
+
// Register with StreamManager
|
|
1127
|
+
const streamId = this.streamManager.registerStream(stream.stream, track, {
|
|
1128
|
+
source: track.source || "plugin",
|
|
1129
|
+
isPreload: false,
|
|
1130
|
+
priority: 5,
|
|
1131
|
+
});
|
|
1132
|
+
this.debug(`[Stream] Plugin stream registered with ID: ${streamId}`);
|
|
490
1133
|
return stream;
|
|
1134
|
+
}
|
|
491
1135
|
throw new Error(`No stream available for track: ${track.title}`);
|
|
492
1136
|
}
|
|
493
1137
|
/**
|
|
@@ -495,59 +1139,48 @@ class Player extends events_1.EventEmitter {
|
|
|
495
1139
|
*/
|
|
496
1140
|
async startTrack(track) {
|
|
497
1141
|
try {
|
|
498
|
-
|
|
499
|
-
this.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return true;
|
|
1142
|
+
// Try to use preloaded resource
|
|
1143
|
+
if (this.preloadSlot.isValid &&
|
|
1144
|
+
this.preloadSlot.track?.id === track.id &&
|
|
1145
|
+
this.preloadSlot.resource &&
|
|
1146
|
+
this.preloadSlot.resource.playStream?.readable !== false) {
|
|
1147
|
+
this.debug(`[Player] Using preloaded stream for: ${track.title}`);
|
|
1148
|
+
// Stop current playback
|
|
1149
|
+
this.audioPlayer.stop(true);
|
|
1150
|
+
// Clean up old current stream (but delay to be safe)
|
|
1151
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1152
|
+
if (oldStreamId && this.streamManager) {
|
|
1153
|
+
setTimeout(() => {
|
|
1154
|
+
if (this.currentSlot.streamId === oldStreamId) {
|
|
1155
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
1156
|
+
}
|
|
1157
|
+
}, 3000);
|
|
515
1158
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const fallbackResource = (0, voice_1.createAudioResource)(streamInfo.stream, {
|
|
522
|
-
metadata: track,
|
|
523
|
-
inputType: streamInfo.type === "webm/opus" ? voice_1.StreamType.WebmOpus
|
|
524
|
-
: streamInfo.type === "ogg/opus" ? voice_1.StreamType.OggOpus
|
|
525
|
-
: voice_1.StreamType.Arbitrary,
|
|
526
|
-
inlineVolume: true,
|
|
527
|
-
});
|
|
528
|
-
this.currentResource = fallbackResource;
|
|
529
|
-
this.currentResource.volume?.setVolume(this.volume / 100);
|
|
530
|
-
this.audioPlayer.play(this.currentResource);
|
|
531
|
-
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 5_000);
|
|
532
|
-
return true;
|
|
533
|
-
}
|
|
534
|
-
catch (fallbackError) {
|
|
535
|
-
this.debug(`[Player] Fallback also failed:`, fallbackError);
|
|
536
|
-
throw fallbackError;
|
|
537
|
-
}
|
|
1159
|
+
// Set current slot from preload
|
|
1160
|
+
this.promotePreloadToCurrent(track);
|
|
1161
|
+
const currentResource = this.currentSlot.resource;
|
|
1162
|
+
if (!currentResource) {
|
|
1163
|
+
return false;
|
|
538
1164
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
this.
|
|
1165
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1166
|
+
// Apply volume
|
|
1167
|
+
if (currentResource.volume) {
|
|
1168
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1169
|
+
}
|
|
1170
|
+
// Play
|
|
1171
|
+
await this.maybeAlignToBeatBoundary();
|
|
1172
|
+
this.audioPlayer.play(currentResource);
|
|
1173
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
1174
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
1175
|
+
// Start preloading next track (async, don't await)
|
|
1176
|
+
this.preloadNextTrack().catch((err) => {
|
|
1177
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1178
|
+
});
|
|
546
1179
|
return true;
|
|
547
1180
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1181
|
+
// No valid preload, load fresh
|
|
1182
|
+
this.debug(`[Player] No preload available, loading fresh: ${track.title}`);
|
|
1183
|
+
return await this.loadFreshStream(track);
|
|
551
1184
|
}
|
|
552
1185
|
catch (error) {
|
|
553
1186
|
this.debug(`[Player] startTrack error:`, error);
|
|
@@ -555,8 +1188,113 @@ class Player extends events_1.EventEmitter {
|
|
|
555
1188
|
return false;
|
|
556
1189
|
}
|
|
557
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Swap preload slot to current slot
|
|
1193
|
+
*/
|
|
1194
|
+
async swapToCurrent(track) {
|
|
1195
|
+
// Store preload resource
|
|
1196
|
+
const newResource = this.preloadSlot.resource;
|
|
1197
|
+
const oldStreamId = this.currentSlot.streamId;
|
|
1198
|
+
if (!newResource) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
// Stop current playback
|
|
1202
|
+
this.audioPlayer.stop(true);
|
|
1203
|
+
// Clean up old current stream (but keep it for a moment)
|
|
1204
|
+
if (oldStreamId && this.streamManager) {
|
|
1205
|
+
// Delay cleanup to avoid destroying if still needed
|
|
1206
|
+
setTimeout(() => {
|
|
1207
|
+
if (this.currentSlot.streamId === oldStreamId) {
|
|
1208
|
+
this.streamManager.unregisterStream(oldStreamId, true);
|
|
1209
|
+
}
|
|
1210
|
+
}, 5000);
|
|
1211
|
+
}
|
|
1212
|
+
// Set new current
|
|
1213
|
+
this.promotePreloadToCurrent(track);
|
|
1214
|
+
const currentResource = this.currentSlot.resource;
|
|
1215
|
+
if (!currentResource) {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1219
|
+
// Apply volume
|
|
1220
|
+
if (currentResource.volume) {
|
|
1221
|
+
currentResource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1222
|
+
}
|
|
1223
|
+
// Play
|
|
1224
|
+
await this.maybeAlignToBeatBoundary();
|
|
1225
|
+
this.audioPlayer.play(currentResource);
|
|
1226
|
+
try {
|
|
1227
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
1228
|
+
await this.applyCrossfadeIn(currentResource, track);
|
|
1229
|
+
// Start preloading next track
|
|
1230
|
+
this.preloadNextTrack().catch((err) => {
|
|
1231
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1232
|
+
});
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
catch (err) {
|
|
1236
|
+
this.debug(`[Player] Failed to play swapped track:`, err);
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Load fresh stream when no preload available
|
|
1242
|
+
*/
|
|
1243
|
+
async loadFreshStream(track) {
|
|
1244
|
+
// Cancel preload to free resources
|
|
1245
|
+
await this.safeCancelPreload();
|
|
1246
|
+
try {
|
|
1247
|
+
const streamInfo = await this.getStream(track);
|
|
1248
|
+
if (!streamInfo?.stream) {
|
|
1249
|
+
throw new Error(`No stream available`);
|
|
1250
|
+
}
|
|
1251
|
+
// Register with StreamManager
|
|
1252
|
+
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
1253
|
+
source: track.source || "stream",
|
|
1254
|
+
isPreload: false,
|
|
1255
|
+
priority: 10,
|
|
1256
|
+
});
|
|
1257
|
+
// Create resource
|
|
1258
|
+
const resource = await this.createResource(streamInfo, track, 0);
|
|
1259
|
+
// Clean up old current
|
|
1260
|
+
if (this.currentSlot.streamId && this.currentSlot.streamId !== streamId) {
|
|
1261
|
+
this.streamManager.unregisterStream(this.currentSlot.streamId, true);
|
|
1262
|
+
}
|
|
1263
|
+
// Set current slot
|
|
1264
|
+
this.currentSlot.resource = resource;
|
|
1265
|
+
this.currentSlot.track = track;
|
|
1266
|
+
this.currentSlot.streamId = streamId;
|
|
1267
|
+
this.currentSlot.isValid = true;
|
|
1268
|
+
this.currentResource = resource;
|
|
1269
|
+
// Apply volume
|
|
1270
|
+
const targetVolume = this.getTrackTargetVolume(track);
|
|
1271
|
+
if (resource.volume) {
|
|
1272
|
+
resource.volume.setVolume(this.crossfadeEnabled ? 0 : targetVolume);
|
|
1273
|
+
}
|
|
1274
|
+
// Play
|
|
1275
|
+
await this.maybeAlignToBeatBoundary();
|
|
1276
|
+
this.audioPlayer.stop(true);
|
|
1277
|
+
this.audioPlayer.play(resource);
|
|
1278
|
+
await (0, voice_1.entersState)(this.audioPlayer, voice_1.AudioPlayerStatus.Playing, 10_000);
|
|
1279
|
+
await this.applyCrossfadeIn(resource, track);
|
|
1280
|
+
// Preload next (async)
|
|
1281
|
+
this.preloadNextTrack().catch((err) => {
|
|
1282
|
+
this.debug(`[Player] Preload error:`, err);
|
|
1283
|
+
});
|
|
1284
|
+
return true;
|
|
1285
|
+
}
|
|
1286
|
+
catch (error) {
|
|
1287
|
+
this.debug(`[Player] loadFreshStream error:`, error);
|
|
1288
|
+
throw error;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Play the next track in the queue, handling errors and edge cases gracefully
|
|
1293
|
+
*/
|
|
558
1294
|
async playNext() {
|
|
559
|
-
this.debug(
|
|
1295
|
+
this.debug("[Player] playNext called");
|
|
1296
|
+
// Don't cancel preload here unless absolutely necessary
|
|
1297
|
+
// Let startTrack handle it
|
|
560
1298
|
while (true) {
|
|
561
1299
|
const track = this.queue.next(this.skipLoop);
|
|
562
1300
|
this.skipLoop = false;
|
|
@@ -571,20 +1309,47 @@ class Player extends events_1.EventEmitter {
|
|
|
571
1309
|
this.debug(`[Player] No next track in queue`);
|
|
572
1310
|
this.isPlaying = false;
|
|
573
1311
|
this.emit("queueEnd");
|
|
1312
|
+
// Clean up both slots when queue is empty
|
|
1313
|
+
this.clearSlot(this.currentSlot);
|
|
1314
|
+
await this.safeCancelPreload();
|
|
574
1315
|
if (this.options.leaveOnEnd) {
|
|
575
1316
|
this.scheduleLeave();
|
|
576
1317
|
}
|
|
577
1318
|
return false;
|
|
578
1319
|
}
|
|
579
|
-
this.generateWillNext();
|
|
1320
|
+
this.generateWillNext().catch((err) => this.debug("[Player] generateWillNext error:", err));
|
|
580
1321
|
this.clearLeaveTimeout();
|
|
581
1322
|
this.debug(`[Player] playNext called for track: ${track.title}`);
|
|
582
1323
|
try {
|
|
583
|
-
|
|
1324
|
+
const started = await this.startTrack(track);
|
|
1325
|
+
if (started) {
|
|
1326
|
+
this.antiStuckConsecutiveFailures = 0;
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
const recovered = await this.attemptTrackRecovery(track, new Error("TRACK_START_RETURNED_FALSE"));
|
|
1330
|
+
if (recovered) {
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
if (this.antiStuckEnabled && this.antiStuckConsecutiveFailures < this.antiStuckControlledSkipThreshold) {
|
|
1334
|
+
this.queue.insert(track, 0);
|
|
1335
|
+
if (this.antiStuckRetryDelayMs > 0) {
|
|
1336
|
+
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
584
1339
|
}
|
|
585
1340
|
catch (err) {
|
|
586
1341
|
this.debug(`[Player] playNext error:`, err);
|
|
587
1342
|
this.emit("playerError", err, track);
|
|
1343
|
+
const recovered = await this.attemptTrackRecovery(track, err);
|
|
1344
|
+
if (recovered) {
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
if (this.antiStuckEnabled && this.antiStuckConsecutiveFailures < this.antiStuckControlledSkipThreshold) {
|
|
1348
|
+
this.queue.insert(track, 0);
|
|
1349
|
+
if (this.antiStuckRetryDelayMs > 0) {
|
|
1350
|
+
await new Promise((resolve) => setTimeout(resolve, this.antiStuckRetryDelayMs));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
588
1353
|
continue;
|
|
589
1354
|
}
|
|
590
1355
|
}
|
|
@@ -624,12 +1389,12 @@ class Player extends events_1.EventEmitter {
|
|
|
624
1389
|
// Build resource from plugin stream
|
|
625
1390
|
const streamInfo = await this.pluginManager.getStream(track);
|
|
626
1391
|
if (!streamInfo) {
|
|
627
|
-
throw new Error(
|
|
1392
|
+
throw new Error(`No stream available for track: ${track.title}`);
|
|
628
1393
|
}
|
|
629
1394
|
ttsStream = streamInfo.stream;
|
|
630
1395
|
const resource = await this.createResource(streamInfo, track);
|
|
631
1396
|
if (!resource) {
|
|
632
|
-
throw new Error(
|
|
1397
|
+
throw new Error(`No resource available for track: ${track.title}`);
|
|
633
1398
|
}
|
|
634
1399
|
ttsResource = resource;
|
|
635
1400
|
if (resource.volume) {
|
|
@@ -656,7 +1421,7 @@ class Player extends events_1.EventEmitter {
|
|
|
656
1421
|
declared
|
|
657
1422
|
: declared * 1000
|
|
658
1423
|
: undefined;
|
|
659
|
-
const cap = this.options?.tts?.
|
|
1424
|
+
const cap = this.options?.tts?.maxTimeTts ?? 60_000;
|
|
660
1425
|
const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
|
|
661
1426
|
await (0, voice_1.entersState)(ttsPlayer, voice_1.AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
|
|
662
1427
|
// Swap back and resume if needed
|
|
@@ -707,9 +1472,21 @@ class Player extends events_1.EventEmitter {
|
|
|
707
1472
|
});
|
|
708
1473
|
await (0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Ready, 50_000);
|
|
709
1474
|
this.connection = connection;
|
|
710
|
-
connection.on(voice_1.VoiceConnectionStatus.Disconnected, () => {
|
|
711
|
-
|
|
712
|
-
|
|
1475
|
+
connection.on(voice_1.VoiceConnectionStatus.Disconnected, async () => {
|
|
1476
|
+
try {
|
|
1477
|
+
// move channel
|
|
1478
|
+
await Promise.race([
|
|
1479
|
+
(0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Signalling, 5_000),
|
|
1480
|
+
(0, voice_1.entersState)(connection, voice_1.VoiceConnectionStatus.Connecting, 5_000),
|
|
1481
|
+
]);
|
|
1482
|
+
// Signalling/Connecting → reconnect
|
|
1483
|
+
this.debug(`[Player] Reconnecting after channel move...`);
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
// no reconnect in 5 giây → disconnect
|
|
1487
|
+
this.debug(`[Player] Truly disconnected, destroying player`);
|
|
1488
|
+
this.destroy();
|
|
1489
|
+
}
|
|
713
1490
|
});
|
|
714
1491
|
connection.on("error", (error) => {
|
|
715
1492
|
this.debug(`[Player] Voice connection error:`, error);
|
|
@@ -757,7 +1534,7 @@ class Player extends events_1.EventEmitter {
|
|
|
757
1534
|
const track = this.queue.currentTrack;
|
|
758
1535
|
if (track) {
|
|
759
1536
|
this.debug(`[Player] Player resumed on track: ${track.title}`);
|
|
760
|
-
this.emit("playerResume", track);
|
|
1537
|
+
// this.emit("playerResume", track); //đã có trong stateChange
|
|
761
1538
|
}
|
|
762
1539
|
}
|
|
763
1540
|
return result;
|
|
@@ -774,6 +1551,8 @@ class Player extends events_1.EventEmitter {
|
|
|
774
1551
|
*/
|
|
775
1552
|
stop() {
|
|
776
1553
|
this.debug(`[Player] stop called`);
|
|
1554
|
+
// Cancel preload when stopping
|
|
1555
|
+
this.cancelPreload();
|
|
777
1556
|
this.queue.clear();
|
|
778
1557
|
const result = this.audioPlayer.stop();
|
|
779
1558
|
this.destroyCurrentStream();
|
|
@@ -808,12 +1587,7 @@ class Player extends events_1.EventEmitter {
|
|
|
808
1587
|
this.debug(`[Player] Invalid seek position: ${position}ms (track duration: ${totalDuration}ms)`);
|
|
809
1588
|
return false;
|
|
810
1589
|
}
|
|
811
|
-
|
|
812
|
-
if (!streaminfo?.stream) {
|
|
813
|
-
this.debug(`[Player] No stream to seek`);
|
|
814
|
-
return false;
|
|
815
|
-
}
|
|
816
|
-
await this.refeshPlayerResource(true, position);
|
|
1590
|
+
await this.refreshPlayerResource(true, position);
|
|
817
1591
|
return true;
|
|
818
1592
|
}
|
|
819
1593
|
/**
|
|
@@ -830,26 +1604,23 @@ class Player extends events_1.EventEmitter {
|
|
|
830
1604
|
this.debug(`[Player] skip called with index: ${index}`);
|
|
831
1605
|
try {
|
|
832
1606
|
if (typeof index === "number" && index >= 0) {
|
|
833
|
-
// Skip to specific index
|
|
834
1607
|
const targetTrack = this.queue.getTrack(index);
|
|
835
1608
|
if (!targetTrack) {
|
|
836
1609
|
this.debug(`[Player] No track found at index ${index}`);
|
|
837
1610
|
return false;
|
|
838
1611
|
}
|
|
839
|
-
// Remove tracks from 0 to index-1
|
|
840
1612
|
for (let i = 0; i < index; i++) {
|
|
841
1613
|
this.queue.remove(0);
|
|
842
1614
|
}
|
|
843
1615
|
this.debug(`[Player] Skipped to track at index ${index}: ${targetTrack.title}`);
|
|
844
|
-
if (this.isPlaying || this.isPaused) {
|
|
845
|
-
this.skipLoop = true;
|
|
846
|
-
return this.audioPlayer.stop();
|
|
847
|
-
}
|
|
848
|
-
return true;
|
|
849
1616
|
}
|
|
850
1617
|
if (this.isPlaying || this.isPaused) {
|
|
851
1618
|
this.skipLoop = true;
|
|
852
|
-
|
|
1619
|
+
void this.crossfadeSkipAndStop().catch((error) => {
|
|
1620
|
+
this.debug(`[Player] crossfade skip error:`, error);
|
|
1621
|
+
this.audioPlayer.stop();
|
|
1622
|
+
});
|
|
1623
|
+
return true;
|
|
853
1624
|
}
|
|
854
1625
|
return true;
|
|
855
1626
|
}
|
|
@@ -919,7 +1690,7 @@ class Player extends events_1.EventEmitter {
|
|
|
919
1690
|
saveOptions = options;
|
|
920
1691
|
}
|
|
921
1692
|
try {
|
|
922
|
-
//
|
|
1693
|
+
// Skip extension manager for saving - we want the raw stream without filters/seek applied, and extensions may not support this
|
|
923
1694
|
let streamInfo = await this.pluginManager.getStream(track);
|
|
924
1695
|
if (!streamInfo || !streamInfo.stream) {
|
|
925
1696
|
throw new Error(`No save stream available for track: ${track.title}`);
|
|
@@ -1133,21 +1904,96 @@ class Player extends events_1.EventEmitter {
|
|
|
1133
1904
|
* @example
|
|
1134
1905
|
* const progressBar = player.getProgressBar();
|
|
1135
1906
|
* console.log(`Progress bar: ${progressBar}`);
|
|
1907
|
+
*
|
|
1908
|
+
* // Custom options
|
|
1909
|
+
* const customBar = player.getProgressBar({
|
|
1910
|
+
* size: 30,
|
|
1911
|
+
* barChar: "─",
|
|
1912
|
+
* progressChar: "●",
|
|
1913
|
+
* timeFormat: "compact" // "compact" = 1:22:12, "full" = 01:22:12
|
|
1914
|
+
* });
|
|
1136
1915
|
*/
|
|
1137
1916
|
getProgressBar(options = {}) {
|
|
1138
|
-
const { size = 20, barChar = "▬", progressChar = "🔘"
|
|
1917
|
+
const { size = 20, barChar = "▬", progressChar = "🔘", timeFormat = "compact", // "compact" or "full"
|
|
1918
|
+
showPercentage = false, showTime = true, } = options;
|
|
1139
1919
|
const track = this.queue.currentTrack;
|
|
1140
1920
|
const resource = this.currentResource;
|
|
1141
|
-
|
|
1921
|
+
// Handle live stream
|
|
1922
|
+
if (this.isLive || !track || !resource) {
|
|
1923
|
+
if (this.isLive)
|
|
1924
|
+
return "🔴 LIVE";
|
|
1142
1925
|
return "";
|
|
1926
|
+
}
|
|
1143
1927
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
1144
1928
|
if (!total)
|
|
1145
|
-
return this.
|
|
1929
|
+
return this.formatTimeCompact(resource.playbackDuration);
|
|
1146
1930
|
const current = resource.playbackDuration;
|
|
1147
|
-
const ratio = Math.min(current / total, 1);
|
|
1931
|
+
const ratio = Math.min(Math.max(current / total, 0), 1);
|
|
1148
1932
|
const progress = Math.round(ratio * size);
|
|
1149
|
-
|
|
1150
|
-
|
|
1933
|
+
// Build progress bar
|
|
1934
|
+
let bar = "";
|
|
1935
|
+
if (progressChar === "none" || options.hideProgressChar) {
|
|
1936
|
+
// Continuous bar without separator
|
|
1937
|
+
const filled = barChar.repeat(progress);
|
|
1938
|
+
const empty = barChar.repeat(size - progress);
|
|
1939
|
+
bar = filled + empty;
|
|
1940
|
+
}
|
|
1941
|
+
else {
|
|
1942
|
+
// Bar with progress character
|
|
1943
|
+
const filled = barChar.repeat(progress);
|
|
1944
|
+
const empty = barChar.repeat(Math.max(0, size - progress));
|
|
1945
|
+
bar = filled + progressChar + empty;
|
|
1946
|
+
}
|
|
1947
|
+
// Format time based on option
|
|
1948
|
+
const formatTimeFn = timeFormat === "compact" ? this.formatTimeCompact.bind(this) : this.formatTime.bind(this);
|
|
1949
|
+
const currentTimeStr = formatTimeFn(current);
|
|
1950
|
+
const totalTimeStr = formatTimeFn(total);
|
|
1951
|
+
// Build result
|
|
1952
|
+
let result = "";
|
|
1953
|
+
if (showTime) {
|
|
1954
|
+
result = `${currentTimeStr} ${bar} ${totalTimeStr}`;
|
|
1955
|
+
}
|
|
1956
|
+
else {
|
|
1957
|
+
result = bar;
|
|
1958
|
+
}
|
|
1959
|
+
// Add percentage if requested
|
|
1960
|
+
if (showPercentage) {
|
|
1961
|
+
const percent = Math.round(ratio * 100);
|
|
1962
|
+
result += ` (${percent}%)`;
|
|
1963
|
+
}
|
|
1964
|
+
return result;
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Format time with leading zeros (00:00 or 00:00:00)
|
|
1968
|
+
* @param ms - Time in milliseconds
|
|
1969
|
+
* @returns Formatted time string with leading zeros
|
|
1970
|
+
*/
|
|
1971
|
+
formatTime(ms) {
|
|
1972
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
1973
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1974
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1975
|
+
const seconds = totalSeconds % 60;
|
|
1976
|
+
const parts = [];
|
|
1977
|
+
if (hours > 0)
|
|
1978
|
+
parts.push(String(hours).padStart(2, "0"));
|
|
1979
|
+
parts.push(String(minutes).padStart(2, "0"));
|
|
1980
|
+
parts.push(String(seconds).padStart(2, "0"));
|
|
1981
|
+
return parts.join(":");
|
|
1982
|
+
}
|
|
1983
|
+
/**
|
|
1984
|
+
* Format time without leading zeros for hours (1:22:12 or 3:45)
|
|
1985
|
+
* @param ms - Time in milliseconds
|
|
1986
|
+
* @returns Compact formatted time string
|
|
1987
|
+
*/
|
|
1988
|
+
formatTimeCompact(ms) {
|
|
1989
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
1990
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
1991
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1992
|
+
const seconds = totalSeconds % 60;
|
|
1993
|
+
if (hours > 0) {
|
|
1994
|
+
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
1995
|
+
}
|
|
1996
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
1151
1997
|
}
|
|
1152
1998
|
/**
|
|
1153
1999
|
* Get the time of the current track
|
|
@@ -1156,44 +2002,44 @@ class Player extends events_1.EventEmitter {
|
|
|
1156
2002
|
* @example
|
|
1157
2003
|
* const time = player.getTime();
|
|
1158
2004
|
* console.log(`Time: ${time.current}`);
|
|
2005
|
+
* console.log(`Formatted: ${time.formatted.current}`); // "1:22:12" or "3:45"
|
|
1159
2006
|
*/
|
|
1160
2007
|
getTime() {
|
|
2008
|
+
if (this.isLive)
|
|
2009
|
+
return {
|
|
2010
|
+
current: 0,
|
|
2011
|
+
total: 0,
|
|
2012
|
+
format: "LIVE",
|
|
2013
|
+
formatted: {
|
|
2014
|
+
current: "LIVE",
|
|
2015
|
+
total: "LIVE",
|
|
2016
|
+
},
|
|
2017
|
+
};
|
|
1161
2018
|
const resource = this.currentResource;
|
|
1162
2019
|
const track = this.queue.currentTrack;
|
|
1163
|
-
if (!track || !resource)
|
|
2020
|
+
if (!track || !resource) {
|
|
1164
2021
|
return {
|
|
1165
2022
|
current: 0,
|
|
1166
2023
|
total: 0,
|
|
1167
2024
|
format: "00:00",
|
|
2025
|
+
formatted: {
|
|
2026
|
+
current: "00:00",
|
|
2027
|
+
total: "00:00",
|
|
2028
|
+
},
|
|
1168
2029
|
};
|
|
2030
|
+
}
|
|
1169
2031
|
const total = track.duration > 1000 ? track.duration : track.duration * 1000;
|
|
2032
|
+
const current = resource.playbackDuration;
|
|
1170
2033
|
return {
|
|
1171
|
-
current:
|
|
2034
|
+
current: current,
|
|
1172
2035
|
total: total,
|
|
1173
|
-
format: this.formatTime(
|
|
2036
|
+
format: this.formatTime(current),
|
|
2037
|
+
formatted: {
|
|
2038
|
+
current: this.formatTimeCompact(current),
|
|
2039
|
+
total: this.formatTimeCompact(total),
|
|
2040
|
+
},
|
|
1174
2041
|
};
|
|
1175
2042
|
}
|
|
1176
|
-
/**
|
|
1177
|
-
* Format the time in the format of HH:MM:SS
|
|
1178
|
-
*
|
|
1179
|
-
* @param {number} ms - The time in milliseconds
|
|
1180
|
-
* @returns {string} The formatted time
|
|
1181
|
-
* @example
|
|
1182
|
-
* const formattedTime = player.formatTime(1000);
|
|
1183
|
-
* console.log(`Formatted time: ${formattedTime}`);
|
|
1184
|
-
*/
|
|
1185
|
-
formatTime(ms) {
|
|
1186
|
-
const totalSeconds = Math.floor(ms / 1000);
|
|
1187
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
1188
|
-
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
1189
|
-
const seconds = totalSeconds % 60;
|
|
1190
|
-
const parts = [];
|
|
1191
|
-
if (hours > 0)
|
|
1192
|
-
parts.push(String(hours).padStart(2, "0"));
|
|
1193
|
-
parts.push(String(minutes).padStart(2, "0"));
|
|
1194
|
-
parts.push(String(seconds).padStart(2, "0"));
|
|
1195
|
-
return parts.join(":");
|
|
1196
|
-
}
|
|
1197
2043
|
/**
|
|
1198
2044
|
* Destroy the player
|
|
1199
2045
|
*
|
|
@@ -1207,10 +2053,14 @@ class Player extends events_1.EventEmitter {
|
|
|
1207
2053
|
clearTimeout(this.leaveTimeout);
|
|
1208
2054
|
this.leaveTimeout = null;
|
|
1209
2055
|
}
|
|
2056
|
+
this.streamManager.destroyAll(true);
|
|
1210
2057
|
// Destroy current stream before stopping audio
|
|
1211
2058
|
this.destroyCurrentStream();
|
|
2059
|
+
this.clearSlot(this.currentSlot);
|
|
2060
|
+
this.clearSlot(this.preloadSlot);
|
|
1212
2061
|
this.audioPlayer.removeAllListeners();
|
|
1213
2062
|
this.audioPlayer.stop(true);
|
|
2063
|
+
this.clearPreload();
|
|
1214
2064
|
if (this.ttsPlayer) {
|
|
1215
2065
|
try {
|
|
1216
2066
|
this.ttsPlayer.stop(true);
|
|
@@ -1243,7 +2093,7 @@ class Player extends events_1.EventEmitter {
|
|
|
1243
2093
|
if (this.leaveTimeout) {
|
|
1244
2094
|
clearTimeout(this.leaveTimeout);
|
|
1245
2095
|
}
|
|
1246
|
-
if (this.options.
|
|
2096
|
+
if (this.options.leaveOnEnd && this.options.leaveTimeout) {
|
|
1247
2097
|
this.leaveTimeout = setTimeout(() => {
|
|
1248
2098
|
this.debug(`[Player] Leaving voice channel after timeoutMs`);
|
|
1249
2099
|
this.destroy();
|
|
@@ -1257,13 +2107,16 @@ class Player extends events_1.EventEmitter {
|
|
|
1257
2107
|
* @param {number} position - Position to seek to in milliseconds
|
|
1258
2108
|
* @returns {Promise<boolean>}
|
|
1259
2109
|
* @example
|
|
1260
|
-
* const refreshed = await player.
|
|
2110
|
+
* const refreshed = await player.refreshPlayerResource(true, 1000);
|
|
1261
2111
|
* console.log(`Refreshed: ${refreshed}`);
|
|
1262
2112
|
*/
|
|
1263
|
-
async
|
|
2113
|
+
async refreshPlayerResource(applyToCurrent = true, position = -1) {
|
|
1264
2114
|
if (!applyToCurrent || !this.queue.currentTrack || !(this.isPlaying || this.isPaused)) {
|
|
1265
2115
|
return false;
|
|
1266
2116
|
}
|
|
2117
|
+
if (this.refreshLock)
|
|
2118
|
+
return false;
|
|
2119
|
+
this.refreshLock = true;
|
|
1267
2120
|
try {
|
|
1268
2121
|
const track = this.queue.currentTrack;
|
|
1269
2122
|
this.debug(`[Player] Refreshing player resource for track: ${track.title}`);
|
|
@@ -1290,7 +2143,10 @@ class Player extends events_1.EventEmitter {
|
|
|
1290
2143
|
}
|
|
1291
2144
|
}
|
|
1292
2145
|
catch (error) {
|
|
1293
|
-
this.debug(`[Player] Error destroying old stream in
|
|
2146
|
+
this.debug(`[Player] Error destroying old stream in refreshPlayerResource:`, error);
|
|
2147
|
+
}
|
|
2148
|
+
finally {
|
|
2149
|
+
this.refreshLock = false;
|
|
1294
2150
|
}
|
|
1295
2151
|
this.currentResource = resource;
|
|
1296
2152
|
// Subscribe to new resource
|
|
@@ -1407,11 +2263,42 @@ class Player extends events_1.EventEmitter {
|
|
|
1407
2263
|
}
|
|
1408
2264
|
else if (newState.status === voice_1.AudioPlayerStatus.Buffering) {
|
|
1409
2265
|
this.debug(`[Player] AudioPlayerStatus.Buffering`);
|
|
2266
|
+
this.lastDuration = this.currentResource?.playbackDuration || 0;
|
|
2267
|
+
this.stuckTimer = setTimeout(() => {
|
|
2268
|
+
if (this.currentResource?.playbackDuration === this.lastDuration) {
|
|
2269
|
+
this.emit("trackStuck", this.currentTrack);
|
|
2270
|
+
const stuckTrack = this.currentTrack;
|
|
2271
|
+
if (stuckTrack && this.antiStuckEnabled) {
|
|
2272
|
+
void this.attemptTrackRecovery(stuckTrack, new Error("TRACK_STUCK")).then((recovered) => {
|
|
2273
|
+
if (!recovered) {
|
|
2274
|
+
this.skip();
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
this.skip();
|
|
2280
|
+
}
|
|
2281
|
+
}, 10000);
|
|
2282
|
+
}
|
|
2283
|
+
else {
|
|
2284
|
+
if (this.stuckTimer) {
|
|
2285
|
+
clearTimeout(this.stuckTimer);
|
|
2286
|
+
this.stuckTimer = null;
|
|
2287
|
+
}
|
|
1410
2288
|
}
|
|
1411
2289
|
});
|
|
1412
2290
|
this.audioPlayer.on("error", (error) => {
|
|
1413
2291
|
this.debug(`[Player] AudioPlayer error:`, error);
|
|
1414
2292
|
this.emit("playerError", error, this.queue.currentTrack || undefined);
|
|
2293
|
+
const track = this.queue.currentTrack;
|
|
2294
|
+
if (track && this.antiStuckEnabled) {
|
|
2295
|
+
void this.attemptTrackRecovery(track, error).then((recovered) => {
|
|
2296
|
+
if (!recovered) {
|
|
2297
|
+
this.playNext();
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
1415
2302
|
this.playNext();
|
|
1416
2303
|
});
|
|
1417
2304
|
this.audioPlayer.on("debug", (...args) => {
|
|
@@ -1419,6 +2306,20 @@ class Player extends events_1.EventEmitter {
|
|
|
1419
2306
|
this.emit("debug", ...args);
|
|
1420
2307
|
}
|
|
1421
2308
|
});
|
|
2309
|
+
//stream Manager events
|
|
2310
|
+
this.streamManager.on("streamError", ({ streamId, error }) => {
|
|
2311
|
+
this.debug(`[StreamManager] Error for stream ${streamId}:`, error);
|
|
2312
|
+
this.emit("streamError", error, this.queue.currentTrack || null);
|
|
2313
|
+
});
|
|
2314
|
+
this.streamManager.on("streamRegistered", ({ streamId, track, metadata }) => {
|
|
2315
|
+
this.debug(`[StreamManager] Stream registered: ${track.title} (preload: ${metadata.isPreload})`);
|
|
2316
|
+
});
|
|
2317
|
+
this.streamManager.on("streamUnregistered", ({ streamId, track, reason }) => {
|
|
2318
|
+
this.debug(`[StreamManager] Stream unregistered: ${track.title} (reason: ${reason})`);
|
|
2319
|
+
});
|
|
2320
|
+
this.streamManager.on("debug", (...args) => {
|
|
2321
|
+
this.debug(...args);
|
|
2322
|
+
});
|
|
1422
2323
|
}
|
|
1423
2324
|
addPlugin(plugin) {
|
|
1424
2325
|
this.debug(`[Player] Adding plugin: ${plugin.name}`);
|
|
@@ -1428,6 +2329,77 @@ class Player extends events_1.EventEmitter {
|
|
|
1428
2329
|
this.debug(`[Player] Removing plugin: ${name}`);
|
|
1429
2330
|
return this.pluginManager.unregister(name);
|
|
1430
2331
|
}
|
|
2332
|
+
/**
|
|
2333
|
+
* Save the current session of the player, including queue, current track, position, volume, loop mode, auto-play mode, and active extensions/plugins.
|
|
2334
|
+
*
|
|
2335
|
+
* @returns {PlayerSession} The saved session data
|
|
2336
|
+
*/
|
|
2337
|
+
saveSession() {
|
|
2338
|
+
return {
|
|
2339
|
+
guildId: this.guildId,
|
|
2340
|
+
currentTrack: this.currentTrack,
|
|
2341
|
+
position: this.currentResource?.playbackDuration || null,
|
|
2342
|
+
volume: this.volume,
|
|
2343
|
+
queue: this.queue.getTracks(),
|
|
2344
|
+
loopMode: this.queue.loop(),
|
|
2345
|
+
autoPlay: this.queue.autoPlay(),
|
|
2346
|
+
extensions: this.extensionManager.getAll().map((ext) => ext.name),
|
|
2347
|
+
plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Get serializable state (for manual persistence)
|
|
2352
|
+
*/
|
|
2353
|
+
getSerializableState() {
|
|
2354
|
+
return {
|
|
2355
|
+
guildId: this.guildId,
|
|
2356
|
+
queue: this.queue.getTracks(),
|
|
2357
|
+
currentTrack: this.currentTrack,
|
|
2358
|
+
volume: this.volume,
|
|
2359
|
+
isPlaying: this.isPlaying,
|
|
2360
|
+
isPaused: this.isPaused,
|
|
2361
|
+
loopMode: this.queue.loop(),
|
|
2362
|
+
autoPlay: this.queue.autoPlay(),
|
|
2363
|
+
filters: this.filter.getFilterString(),
|
|
2364
|
+
timestamp: Date.now(),
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Restore from saved state
|
|
2369
|
+
*/
|
|
2370
|
+
async restoreState(state) {
|
|
2371
|
+
try {
|
|
2372
|
+
if (state.volume)
|
|
2373
|
+
this.setVolume(state.volume);
|
|
2374
|
+
if (state.loopMode)
|
|
2375
|
+
this.queue.loop(state.loopMode);
|
|
2376
|
+
if (typeof state.autoPlay === "boolean")
|
|
2377
|
+
this.queue.autoPlay(state.autoPlay);
|
|
2378
|
+
if (state.filters)
|
|
2379
|
+
await this.filter.applyFilters(state.filters.split(","));
|
|
2380
|
+
// Restore queue
|
|
2381
|
+
if (state.queue && Array.isArray(state.queue)) {
|
|
2382
|
+
this.queue.clear();
|
|
2383
|
+
this.queue.addMultiple(state.queue);
|
|
2384
|
+
}
|
|
2385
|
+
this.debug("[Player] State restored");
|
|
2386
|
+
return true;
|
|
2387
|
+
}
|
|
2388
|
+
catch (error) {
|
|
2389
|
+
this.debug("[Player] Failed to restore state:", error);
|
|
2390
|
+
return false;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Get stream manager stats
|
|
2395
|
+
*/
|
|
2396
|
+
getStreamManagerStats() {
|
|
2397
|
+
return {
|
|
2398
|
+
metrics: this.streamManager.getMetrics(),
|
|
2399
|
+
stats: this.streamManager.getStats(),
|
|
2400
|
+
totalStreams: this.streamManager.getStreamCount(),
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
1431
2403
|
//#endregion
|
|
1432
2404
|
//#region Getters
|
|
1433
2405
|
/**
|
|
@@ -1507,6 +2479,9 @@ class Player extends events_1.EventEmitter {
|
|
|
1507
2479
|
get relatedTracks() {
|
|
1508
2480
|
return this.queue.relatedTracks();
|
|
1509
2481
|
}
|
|
2482
|
+
get isLive() {
|
|
2483
|
+
return this.currentTrack?.isLive === true;
|
|
2484
|
+
}
|
|
1510
2485
|
}
|
|
1511
2486
|
exports.Player = Player;
|
|
1512
2487
|
//# sourceMappingURL=Player.js.map
|