ziplayer 0.2.7-dev.3 → 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.
Files changed (47) hide show
  1. package/AI-Guide.md +624 -607
  2. package/README.md +526 -524
  3. package/dist/plugins/index.d.ts +62 -12
  4. package/dist/plugins/index.d.ts.map +1 -1
  5. package/dist/plugins/index.js +497 -57
  6. package/dist/plugins/index.js.map +1 -1
  7. package/dist/structures/PersistenceManager.d.ts +96 -0
  8. package/dist/structures/PersistenceManager.d.ts.map +1 -0
  9. package/dist/structures/PersistenceManager.js +1008 -0
  10. package/dist/structures/PersistenceManager.js.map +1 -0
  11. package/dist/structures/Player.d.ts +109 -18
  12. package/dist/structures/Player.d.ts.map +1 -1
  13. package/dist/structures/Player.js +902 -182
  14. package/dist/structures/Player.js.map +1 -1
  15. package/dist/structures/PlayerManager.d.ts +1 -22
  16. package/dist/structures/PlayerManager.d.ts.map +1 -1
  17. package/dist/structures/PlayerManager.js +1 -73
  18. package/dist/structures/PlayerManager.js.map +1 -1
  19. package/dist/structures/StreamManager.d.ts +137 -0
  20. package/dist/structures/StreamManager.d.ts.map +1 -0
  21. package/dist/structures/StreamManager.js +420 -0
  22. package/dist/structures/StreamManager.js.map +1 -0
  23. package/dist/types/index.d.ts +149 -16
  24. package/dist/types/index.d.ts.map +1 -1
  25. package/dist/types/index.js +0 -1
  26. package/dist/types/index.js.map +1 -1
  27. package/dist/types/persistence.d.ts +3 -2
  28. package/dist/types/persistence.d.ts.map +1 -1
  29. package/package.json +47 -47
  30. package/src/extensions/BaseExtension.ts +36 -36
  31. package/src/extensions/index.ts +473 -473
  32. package/src/index.ts +16 -16
  33. package/src/plugins/BasePlugin.ts +27 -27
  34. package/src/plugins/index.ts +950 -403
  35. package/src/structures/FilterManager.ts +303 -303
  36. package/src/structures/Player.ts +2797 -1970
  37. package/src/structures/PlayerManager.ts +725 -822
  38. package/src/structures/Queue.ts +599 -599
  39. package/src/structures/StreamManager.ts +524 -0
  40. package/src/types/extension.ts +129 -129
  41. package/src/types/fillter.ts +264 -264
  42. package/src/types/index.ts +548 -415
  43. package/src/types/plugin.ts +59 -59
  44. package/src/utils/timeout.ts +10 -10
  45. package/tsconfig.json +22 -22
  46. package/src/persistence/PersistenceManager.ts +0 -1077
  47. package/src/types/persistence.ts +0 -85
@@ -0,0 +1,1008 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PersistenceManager = void 0;
37
+ const events_1 = require("events");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const zlib = __importStar(require("zlib"));
41
+ const util_1 = require("util");
42
+ const gzip = (0, util_1.promisify)(zlib.gzip);
43
+ const gunzip = (0, util_1.promisify)(zlib.gunzip);
44
+ // File provider implementation with enhanced backup management
45
+ class FileProvider {
46
+ constructor(basePath, options = {}) {
47
+ this.basePath = basePath;
48
+ this.maxBackups = options.maxBackups ?? 5;
49
+ this.maxTotalBackups = options.maxTotalBackups ?? 50;
50
+ this.backupRetentionDays = options.backupRetentionDays ?? 7;
51
+ if (!fs.existsSync(basePath)) {
52
+ fs.mkdirSync(basePath, { recursive: true });
53
+ }
54
+ }
55
+ getFilePath(key) {
56
+ return path.join(this.basePath, `${key}.json`);
57
+ }
58
+ getBackupPath(key, timestamp) {
59
+ return path.join(this.basePath, `${key}_backup_${timestamp}.json`);
60
+ }
61
+ getAllBackups() {
62
+ const files = fs.readdirSync(this.basePath);
63
+ const backups = [];
64
+ for (const file of files) {
65
+ const match = file.match(/^(.+)_backup_(\d+)\.json(\.gz)?$/);
66
+ if (match) {
67
+ const [, key, timestampStr] = match;
68
+ const timestamp = parseInt(timestampStr, 10);
69
+ const filePath = path.join(this.basePath, file);
70
+ const stats = fs.statSync(filePath);
71
+ const isCompressed = file.endsWith(".gz");
72
+ backups.push({
73
+ key,
74
+ path: filePath,
75
+ timestamp,
76
+ size: stats.size,
77
+ isCompressed,
78
+ });
79
+ }
80
+ }
81
+ return backups.sort((a, b) => b.timestamp - a.timestamp);
82
+ }
83
+ getBackupsByKey(key) {
84
+ return this.getAllBackups().filter((b) => b.key === key);
85
+ }
86
+ cleanOldBackups(key) {
87
+ const backups = this.getBackupsByKey(key);
88
+ // Delete old backups exceeding maxBackups per player
89
+ for (let i = this.maxBackups; i < backups.length; i++) {
90
+ try {
91
+ fs.unlinkSync(backups[i].path);
92
+ console.log(`[Persistence] Deleted old backup: ${path.basename(backups[i].path)}`);
93
+ }
94
+ catch (err) {
95
+ console.error(`[Persistence] Failed to delete backup: ${backups[i].path}`, err);
96
+ }
97
+ }
98
+ }
99
+ cleanOldBackupsByAge() {
100
+ const now = Date.now();
101
+ const retentionMs = this.backupRetentionDays * 24 * 60 * 60 * 1000;
102
+ const backups = this.getAllBackups();
103
+ let deletedCount = 0;
104
+ for (const backup of backups) {
105
+ if (now - backup.timestamp > retentionMs) {
106
+ try {
107
+ fs.unlinkSync(backup.path);
108
+ deletedCount++;
109
+ }
110
+ catch (err) {
111
+ console.error(`[Persistence] Failed to delete old backup: ${backup.path}`, err);
112
+ }
113
+ }
114
+ }
115
+ if (deletedCount > 0) {
116
+ console.log(`[Persistence] Deleted ${deletedCount} backups older than ${this.backupRetentionDays} days`);
117
+ }
118
+ }
119
+ cleanTotalBackupsLimit() {
120
+ let backups = this.getAllBackups();
121
+ if (backups.length <= this.maxTotalBackups)
122
+ return;
123
+ // Delete oldest backups
124
+ const toDelete = backups.slice(this.maxTotalBackups);
125
+ let deletedCount = 0;
126
+ for (const backup of toDelete) {
127
+ try {
128
+ fs.unlinkSync(backup.path);
129
+ deletedCount++;
130
+ }
131
+ catch (err) {
132
+ console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
133
+ }
134
+ }
135
+ if (deletedCount > 0) {
136
+ console.log(`[Persistence] Deleted ${deletedCount} backups (exceeded limit ${this.maxTotalBackups})`);
137
+ }
138
+ }
139
+ // NEW: Clean all backups for a specific player
140
+ async cleanAllBackupsForPlayer(key) {
141
+ const backups = this.getBackupsByKey(key);
142
+ let deletedCount = 0;
143
+ for (const backup of backups) {
144
+ try {
145
+ fs.unlinkSync(backup.path);
146
+ deletedCount++;
147
+ }
148
+ catch (err) {
149
+ console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
150
+ }
151
+ }
152
+ if (deletedCount > 0) {
153
+ console.log(`[Persistence] Deleted ${deletedCount} backups for player: ${key}`);
154
+ }
155
+ return deletedCount;
156
+ }
157
+ // NEW: Clean all backups
158
+ async cleanAllBackups() {
159
+ const backups = this.getAllBackups();
160
+ let deletedCount = 0;
161
+ for (const backup of backups) {
162
+ try {
163
+ fs.unlinkSync(backup.path);
164
+ deletedCount++;
165
+ }
166
+ catch (err) {
167
+ console.error(`[Persistence] Failed to delete backup: ${backup.path}`, err);
168
+ }
169
+ }
170
+ if (deletedCount > 0) {
171
+ console.log(`[Persistence] Deleted all ${deletedCount} backups`);
172
+ }
173
+ return deletedCount;
174
+ }
175
+ // NEW: Get backup statistics
176
+ getBackupStats() {
177
+ const backups = this.getAllBackups();
178
+ let totalSize = 0;
179
+ let oldestBackup = null;
180
+ let newestBackup = null;
181
+ const backupsByPlayer = new Map();
182
+ for (const backup of backups) {
183
+ totalSize += backup.size;
184
+ if (oldestBackup === null || backup.timestamp < oldestBackup)
185
+ oldestBackup = backup.timestamp;
186
+ if (newestBackup === null || backup.timestamp > newestBackup)
187
+ newestBackup = backup.timestamp;
188
+ backupsByPlayer.set(backup.key, (backupsByPlayer.get(backup.key) || 0) + 1);
189
+ }
190
+ return {
191
+ totalBackups: backups.length,
192
+ totalSize,
193
+ oldestBackup,
194
+ newestBackup,
195
+ backupsByPlayer,
196
+ };
197
+ }
198
+ async save(key, data, compress = false) {
199
+ const filePath = this.getFilePath(key);
200
+ let content = JSON.stringify(data, null, 2);
201
+ if (compress) {
202
+ const compressed = await gzip(content);
203
+ content = compressed.toString("base64");
204
+ fs.writeFileSync(filePath + ".gz", content);
205
+ return;
206
+ }
207
+ // Create backup before overwriting
208
+ if (fs.existsSync(filePath)) {
209
+ const backupPath = this.getBackupPath(key, Date.now());
210
+ fs.copyFileSync(filePath, backupPath);
211
+ this.cleanOldBackups(key);
212
+ this.cleanTotalBackupsLimit();
213
+ this.cleanOldBackupsByAge();
214
+ }
215
+ fs.writeFileSync(filePath, content);
216
+ }
217
+ async load(key) {
218
+ const filePath = this.getFilePath(key);
219
+ const gzPath = filePath + ".gz";
220
+ if (fs.existsSync(gzPath)) {
221
+ const compressed = fs.readFileSync(gzPath, "utf8");
222
+ const buffer = Buffer.from(compressed, "base64");
223
+ const decompressed = await gunzip(buffer);
224
+ return JSON.parse(decompressed.toString());
225
+ }
226
+ if (fs.existsSync(filePath)) {
227
+ const content = fs.readFileSync(filePath, "utf8");
228
+ return JSON.parse(content);
229
+ }
230
+ return null;
231
+ }
232
+ async delete(key) {
233
+ const filePath = this.getFilePath(key);
234
+ const gzPath = filePath + ".gz";
235
+ if (fs.existsSync(filePath))
236
+ fs.unlinkSync(filePath);
237
+ if (fs.existsSync(gzPath))
238
+ fs.unlinkSync(gzPath);
239
+ // Also delete backups for this player
240
+ await this.cleanAllBackupsForPlayer(key);
241
+ }
242
+ async list() {
243
+ const files = fs.readdirSync(this.basePath);
244
+ return files
245
+ .filter((f) => {
246
+ // Exclude backup files
247
+ if (f.includes("_backup_"))
248
+ return false;
249
+ return f.endsWith(".json") || f.endsWith(".json.gz");
250
+ })
251
+ .map((f) => f.replace(/\.json(\.gz)?$/, ""));
252
+ }
253
+ async restoreBackup(key, backupTimestamp) {
254
+ let backupFile = null;
255
+ if (backupTimestamp) {
256
+ const specific = this.getBackupPath(key, backupTimestamp);
257
+ if (fs.existsSync(specific)) {
258
+ backupFile = specific;
259
+ }
260
+ }
261
+ else {
262
+ // Get latest backup
263
+ const backups = this.getBackupsByKey(key);
264
+ if (backups.length > 0) {
265
+ backupFile = backups[0].path;
266
+ }
267
+ }
268
+ if (backupFile && fs.existsSync(backupFile)) {
269
+ const content = fs.readFileSync(backupFile, "utf8");
270
+ const data = JSON.parse(content);
271
+ await this.save(key, data);
272
+ return true;
273
+ }
274
+ return false;
275
+ }
276
+ }
277
+ // Custom provider for database integration (giữ nguyên)
278
+ class CustomProvider {
279
+ constructor(saveFn, loadFn, deleteFn, listFn) {
280
+ this.saveFn = saveFn;
281
+ this.loadFn = loadFn;
282
+ this.deleteFn = deleteFn;
283
+ this.listFn = listFn;
284
+ }
285
+ async save(key, data) {
286
+ await this.saveFn(key, data);
287
+ }
288
+ async load(key) {
289
+ return await this.loadFn(key);
290
+ }
291
+ async delete(key) {
292
+ if (this.deleteFn) {
293
+ await this.deleteFn(key);
294
+ }
295
+ }
296
+ async list() {
297
+ if (this.listFn) {
298
+ return await this.listFn();
299
+ }
300
+ return [];
301
+ }
302
+ }
303
+ class PersistenceManager extends events_1.EventEmitter {
304
+ constructor(manager, options) {
305
+ super();
306
+ this.saveInterval = null;
307
+ this.isSaving = false;
308
+ this.isRestoring = false;
309
+ this.destroyedPlayers = new Map();
310
+ this.restoredPlayers = new Set();
311
+ this.backupCleanupDone = false;
312
+ this.manager = manager;
313
+ // Default options
314
+ this.options = {
315
+ enabled: true,
316
+ provider: "file",
317
+ saveInterval: 60000,
318
+ autoLoad: true,
319
+ autoRestoreOnRestart: true,
320
+ restoreDelay: 5000,
321
+ maxBackups: 5,
322
+ maxTotalBackups: 10,
323
+ autoCleanupBackupsOnStart: true,
324
+ backupRetentionDays: 2,
325
+ compress: false,
326
+ filePath: "./players_data",
327
+ };
328
+ this.client = options.client;
329
+ // Merge manually
330
+ if (options.enabled !== undefined)
331
+ this.options.enabled = options.enabled;
332
+ if (options.provider !== undefined)
333
+ this.options.provider = options.provider;
334
+ if (options.saveInterval !== undefined)
335
+ this.options.saveInterval = options.saveInterval;
336
+ if (options.autoLoad !== undefined)
337
+ this.options.autoLoad = options.autoLoad;
338
+ if (options.autoRestoreOnRestart !== undefined)
339
+ this.options.autoRestoreOnRestart = options.autoRestoreOnRestart;
340
+ if (options.restoreDelay !== undefined)
341
+ this.options.restoreDelay = options.restoreDelay;
342
+ if (options.maxBackups !== undefined)
343
+ this.options.maxBackups = options.maxBackups;
344
+ if (options.maxTotalBackups !== undefined)
345
+ this.options.maxTotalBackups = options.maxTotalBackups;
346
+ if (options.autoCleanupBackupsOnStart !== undefined)
347
+ this.options.autoCleanupBackupsOnStart = options.autoCleanupBackupsOnStart;
348
+ if (options.backupRetentionDays !== undefined)
349
+ this.options.backupRetentionDays = options.backupRetentionDays;
350
+ if (options.compress !== undefined)
351
+ this.options.compress = options.compress;
352
+ if (options.filePath !== undefined)
353
+ this.options.filePath = options.filePath;
354
+ if (options.redisUrl !== undefined)
355
+ this.options.redisUrl = options.redisUrl;
356
+ if (options.redisPrefix !== undefined)
357
+ this.options.redisPrefix = options.redisPrefix;
358
+ if (options.save !== undefined)
359
+ this.options.save = options.save;
360
+ if (options.load !== undefined)
361
+ this.options.load = options.load;
362
+ if (options.delete !== undefined)
363
+ this.options.delete = options.delete;
364
+ if (options.list !== undefined)
365
+ this.options.list = options.list;
366
+ if (options.autoConnect !== undefined)
367
+ this.options.autoConnect = options.autoConnect;
368
+ this.provider = this.createProvider();
369
+ // Hook into player destroy events
370
+ this.setupDestroyTracking();
371
+ // Clean up old backups on start
372
+ if (this.options.enabled && this.options.autoCleanupBackupsOnStart) {
373
+ this.cleanupBackupsOnStart().catch((err) => {
374
+ this.debug("Backup cleanup on start error:", err);
375
+ });
376
+ }
377
+ if (this.options.enabled) {
378
+ this.startAutoSave();
379
+ if (this.options.autoLoad) {
380
+ this.loadAll().catch((err) => {
381
+ this.debug("Auto-load error:", err);
382
+ });
383
+ }
384
+ }
385
+ }
386
+ setupDestroyTracking() {
387
+ this.manager.on("playerDestroy", (player) => {
388
+ this.markAsDestroyed(player.guildId);
389
+ });
390
+ }
391
+ createProvider() {
392
+ switch (this.options.provider) {
393
+ case "file":
394
+ return new FileProvider(this.options.filePath, {
395
+ maxBackups: this.options.maxBackups,
396
+ maxTotalBackups: this.options.maxTotalBackups,
397
+ backupRetentionDays: this.options.backupRetentionDays,
398
+ });
399
+ case "redis":
400
+ throw new Error("Redis provider not implemented yet");
401
+ case "database":
402
+ if (!this.options.save || !this.options.load) {
403
+ throw new Error("Database provider requires save/load functions");
404
+ }
405
+ return new CustomProvider(async (key, data) => {
406
+ if (this.options.save) {
407
+ const saveFn = this.options.save;
408
+ if (saveFn.length === 1) {
409
+ await saveFn({ key, data });
410
+ }
411
+ else {
412
+ await saveFn(key, data);
413
+ }
414
+ }
415
+ }, async (key) => {
416
+ if (this.options.load) {
417
+ const loadFn = this.options.load;
418
+ if (loadFn.length === 0) {
419
+ const allData = await loadFn();
420
+ return allData?.get?.(key) || allData?.[key] || null;
421
+ }
422
+ else {
423
+ return await loadFn(key);
424
+ }
425
+ }
426
+ return null;
427
+ }, this.options.delete, this.options.list);
428
+ default:
429
+ return new FileProvider(this.options.filePath, {
430
+ maxBackups: this.options.maxBackups,
431
+ maxTotalBackups: this.options.maxTotalBackups,
432
+ backupRetentionDays: this.options.backupRetentionDays,
433
+ });
434
+ }
435
+ }
436
+ debug(message, ...params) {
437
+ if (this.manager.debugEnabled) {
438
+ this.manager.emit("debug", `[Persistence] ${message}`, ...params);
439
+ }
440
+ }
441
+ startAutoSave() {
442
+ if (this.saveInterval) {
443
+ clearInterval(this.saveInterval);
444
+ }
445
+ this.saveInterval = setInterval(() => {
446
+ this.saveAll().catch((err) => {
447
+ this.debug("Auto-save error:", err);
448
+ this.emit("error", err);
449
+ });
450
+ }, this.options.saveInterval);
451
+ this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
452
+ }
453
+ // NEW: Cleanup backups on startup
454
+ async cleanupBackupsOnStart() {
455
+ if (this.backupCleanupDone)
456
+ return;
457
+ this.debug("Starting backup cleanup on startup...");
458
+ // Only works for file provider
459
+ if (this.provider instanceof FileProvider) {
460
+ try {
461
+ // Clean old backups by age
462
+ // This is already handled in FileProvider, but we can log stats
463
+ const stats = this.provider.getBackupStats();
464
+ this.debug(`Backup stats before cleanup:`, {
465
+ totalBackups: stats.totalBackups,
466
+ totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2),
467
+ oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup).toISOString() : null,
468
+ newestBackup: stats.newestBackup ? new Date(stats.newestBackup).toISOString() : null,
469
+ backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
470
+ });
471
+ // Emit stats event
472
+ this.emit("backupStats", stats);
473
+ // The cleanup is already happening in FileProvider.save()
474
+ // But we can do a one-time deep cleanup on start
475
+ if (this.options.backupRetentionDays && this.options.backupRetentionDays > 0) {
476
+ // Force a cleanup pass
477
+ const deletedCount = await this.cleanOldBackupsByAge();
478
+ if (deletedCount > 0) {
479
+ this.debug(`Cleaned up ${deletedCount} old backups on startup`);
480
+ }
481
+ }
482
+ // Enforce total backup limit
483
+ const totalLimitDeleted = await this.enforceTotalBackupLimit();
484
+ if (totalLimitDeleted > 0) {
485
+ this.debug(`Enforced backup limit: deleted ${totalLimitDeleted} backups`);
486
+ }
487
+ this.backupCleanupDone = true;
488
+ this.emit("backupCleanupDone");
489
+ }
490
+ catch (error) {
491
+ this.debug("Backup cleanup error:", error);
492
+ }
493
+ }
494
+ else {
495
+ this.debug("Backup cleanup only supported for file provider");
496
+ }
497
+ }
498
+ // NEW: Clean old backups by age
499
+ async cleanOldBackupsByAge() {
500
+ if (!(this.provider instanceof FileProvider))
501
+ return 0;
502
+ const retentionMs = (this.options.backupRetentionDays ?? 2) * 24 * 60 * 60 * 1000;
503
+ const now = Date.now();
504
+ const backups = this.provider.getAllBackups();
505
+ let deletedCount = 0;
506
+ for (const backup of backups) {
507
+ if (now - backup.timestamp > retentionMs) {
508
+ try {
509
+ fs.unlinkSync(backup.path);
510
+ deletedCount++;
511
+ }
512
+ catch (err) {
513
+ this.debug(`Failed to delete old backup: ${backup.path}`, err);
514
+ }
515
+ }
516
+ }
517
+ if (deletedCount > 0) {
518
+ this.debug(`Deleted ${deletedCount} backups older than ${this.options.backupRetentionDays} days`);
519
+ }
520
+ return deletedCount;
521
+ }
522
+ // NEW: Enforce total backup limit
523
+ async enforceTotalBackupLimit() {
524
+ if (!(this.provider instanceof FileProvider))
525
+ return 0;
526
+ const backups = this.provider.getAllBackups();
527
+ const maxTotal = this.options.maxTotalBackups ?? 50;
528
+ if (backups.length <= maxTotal)
529
+ return 0;
530
+ const toDelete = backups.slice(maxTotal);
531
+ let deletedCount = 0;
532
+ for (const backup of toDelete) {
533
+ try {
534
+ fs.unlinkSync(backup.path);
535
+ deletedCount++;
536
+ }
537
+ catch (err) {
538
+ this.debug(`Failed to delete backup: ${backup.path}`, err);
539
+ }
540
+ }
541
+ if (deletedCount > 0) {
542
+ this.debug(`Deleted ${deletedCount} backups (exceeded limit ${maxTotal})`);
543
+ }
544
+ return deletedCount;
545
+ }
546
+ markAsDestroyed(guildId) {
547
+ this.destroyedPlayers.set(guildId, {
548
+ guildId,
549
+ destroyedAt: Date.now(),
550
+ reason: "player_destroy",
551
+ });
552
+ this.debug(`Marked player as destroyed: ${guildId}`);
553
+ this.saveDestroyedStatus().catch((err) => {
554
+ this.debug("Failed to save destroyed status:", err);
555
+ });
556
+ }
557
+ isDestroyed(guildId) {
558
+ return this.destroyedPlayers.has(guildId);
559
+ }
560
+ async saveDestroyedStatus() {
561
+ const destroyedData = Array.from(this.destroyedPlayers.values());
562
+ await this.provider.save("__destroyed_players__", destroyedData);
563
+ }
564
+ async loadDestroyedStatus() {
565
+ try {
566
+ const data = await this.provider.load("__destroyed_players__");
567
+ if (data && Array.isArray(data)) {
568
+ this.destroyedPlayers.clear();
569
+ for (const record of data) {
570
+ this.destroyedPlayers.set(record.guildId, record);
571
+ }
572
+ this.debug(`Loaded ${this.destroyedPlayers.size} destroyed player records`);
573
+ }
574
+ }
575
+ catch (error) {
576
+ this.debug("Failed to load destroyed status:", error);
577
+ }
578
+ }
579
+ async clearDestroyedStatus(guildId) {
580
+ this.destroyedPlayers.delete(guildId);
581
+ await this.saveDestroyedStatus();
582
+ }
583
+ serializeTrack(track) {
584
+ const serialized = {
585
+ id: track.id,
586
+ title: track.title,
587
+ url: track.url,
588
+ source: track.source,
589
+ duration: track.duration,
590
+ thumbnail: track.thumbnail,
591
+ requestedBy: track.requestedBy,
592
+ isLive: track.isLive || false,
593
+ };
594
+ const trackAny = track;
595
+ if (trackAny.author)
596
+ serialized.author = trackAny.author;
597
+ if (trackAny.artwork)
598
+ serialized.artwork = trackAny.artwork;
599
+ const excludedFields = new Set([
600
+ "id",
601
+ "title",
602
+ "url",
603
+ "source",
604
+ "duration",
605
+ "thumbnail",
606
+ "requestedBy",
607
+ "isLive",
608
+ "author",
609
+ "artwork",
610
+ ]);
611
+ for (const key of Object.keys(trackAny)) {
612
+ if (!excludedFields.has(key) && trackAny[key] !== undefined) {
613
+ serialized[key] = trackAny[key];
614
+ }
615
+ }
616
+ return serialized;
617
+ }
618
+ serializeQueue(player) {
619
+ return {
620
+ tracks: player.upcomingTracks.map((t) => this.serializeTrack(t)),
621
+ current: player.currentTrack ? this.serializeTrack(player.currentTrack) : null,
622
+ history: player.previousTracks.map((t) => this.serializeTrack(t)),
623
+ loopMode: player.queue.loop(),
624
+ autoPlay: player.queue.autoPlay(),
625
+ position: player.getTime().current,
626
+ };
627
+ }
628
+ serializePlayer(player) {
629
+ let filters = [];
630
+ try {
631
+ const filterString = player.filter?.getFilterString();
632
+ if (filterString) {
633
+ filters = filterString.split(",").filter(Boolean);
634
+ }
635
+ }
636
+ catch (e) { }
637
+ return {
638
+ guildId: player.guildId,
639
+ queue: this.serializeQueue(player),
640
+ volume: player.volume,
641
+ isPlaying: player.isPlaying,
642
+ isPaused: player.isPaused,
643
+ options: player.options,
644
+ filters: filters.length > 0 ? filters : undefined,
645
+ lastUpdate: Date.now(),
646
+ version: "1.0.0",
647
+ wasDestroyed: false,
648
+ voiceChannelId: player.channelConnection?.id,
649
+ adapterCreator: player.channelConnection?.guild?.adapterCreator,
650
+ };
651
+ }
652
+ deserializeTrack(data) {
653
+ const track = {
654
+ id: data.id,
655
+ title: data.title,
656
+ url: data.url,
657
+ source: data.source,
658
+ duration: data.duration,
659
+ thumbnail: data.thumbnail,
660
+ requestedBy: data.requestedBy,
661
+ isLive: data.isLive || false,
662
+ };
663
+ if (data.author)
664
+ track.author = data.author;
665
+ if (data.artwork)
666
+ track.artwork = data.artwork;
667
+ for (const key of Object.keys(data)) {
668
+ if (!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)) {
669
+ track[key] = data[key];
670
+ }
671
+ }
672
+ return track;
673
+ }
674
+ /**
675
+ * Save a single player
676
+ */
677
+ async savePlayer(player) {
678
+ if (!this.options.enabled)
679
+ return false;
680
+ if (this.isDestroyed(player.guildId)) {
681
+ this.debug(`Skipping save for destroyed player: ${player.guildId}`);
682
+ return false;
683
+ }
684
+ try {
685
+ const data = this.serializePlayer(player);
686
+ await this.provider.save(player.guildId, data, this.options.compress);
687
+ this.debug(`Saved player: ${player.guildId}`);
688
+ this.emit("playerSaved", player.guildId);
689
+ return true;
690
+ }
691
+ catch (error) {
692
+ this.debug(`Failed to save player ${player.guildId}:`, error);
693
+ this.emit("error", error);
694
+ return false;
695
+ }
696
+ }
697
+ /**
698
+ * Save all players
699
+ */
700
+ async saveAll() {
701
+ if (!this.options.enabled || this.isSaving)
702
+ return new Map();
703
+ this.isSaving = true;
704
+ const results = new Map();
705
+ try {
706
+ const players = this.manager.getAll();
707
+ this.debug(`Saving ${players.length} players...`);
708
+ const batchSize = 5;
709
+ for (let i = 0; i < players.length; i += batchSize) {
710
+ const batch = players.slice(i, i + batchSize);
711
+ const promises = batch.map((p) => this.savePlayer(p));
712
+ const batchResults = await Promise.all(promises);
713
+ batch.forEach((p, idx) => results.set(p.guildId, batchResults[idx]));
714
+ }
715
+ this.debug(`Saved ${results.size} players`);
716
+ this.emit("savedAll", results);
717
+ }
718
+ finally {
719
+ this.isSaving = false;
720
+ }
721
+ return results;
722
+ }
723
+ /**
724
+ * Load a single player
725
+ */
726
+ async loadPlayer(guildId, restorePosition = true, skipIfDestroyed = true) {
727
+ if (!this.options.enabled)
728
+ return false;
729
+ if (skipIfDestroyed && this.isDestroyed(guildId)) {
730
+ this.debug(`Skipping load for destroyed player: ${guildId}`);
731
+ this.emit("playerSkipped", guildId, "destroyed");
732
+ return false;
733
+ }
734
+ if (this.restoredPlayers.has(guildId)) {
735
+ this.debug(`Skipping already restored player: ${guildId}`);
736
+ return true;
737
+ }
738
+ try {
739
+ const data = await this.provider.load(guildId);
740
+ console.log("[Persistence] Loaded data for", guildId, {
741
+ voiceChannelId: !!data.voiceChannelId,
742
+ adapterCreator: data.adapterCreator,
743
+ autoConnect: data.options?.autoConnect,
744
+ optionsAutoConnect: this.options.autoConnect,
745
+ });
746
+ if (!data)
747
+ return false;
748
+ if (data.wasDestroyed === true) {
749
+ this.debug(`Skipping load for player marked as destroyed: ${guildId}`);
750
+ return false;
751
+ }
752
+ let player = this.manager.get(guildId);
753
+ if (!player) {
754
+ player = await this.manager.create(guildId, data.options);
755
+ }
756
+ this.debug(`[Persistence] Attempting to auto-connect to channel: ${data.voiceChannelId}`);
757
+ try {
758
+ const client = this.client;
759
+ if (!client) {
760
+ this.debug(`[Persistence] Cannot auto-connect: No client available`);
761
+ }
762
+ else {
763
+ const guild = client.guilds.cache.get(guildId);
764
+ if (!guild) {
765
+ this.debug(`[Persistence] Cannot auto-connect: Guild ${guildId} not found`);
766
+ }
767
+ else {
768
+ const voiceChannel = guild.channels.cache.get(data.voiceChannelId);
769
+ if (voiceChannel && voiceChannel.isVoiceBased()) {
770
+ await player.connect(voiceChannel);
771
+ this.debug(`[Persistence] Auto-connected player ${guildId} to channel ${voiceChannel.name}`);
772
+ }
773
+ else {
774
+ this.debug(`[Persistence] Voice channel ${data.voiceChannelId} not found or not a voice channel`);
775
+ }
776
+ }
777
+ }
778
+ }
779
+ catch (err) {
780
+ this.debug(`[Persistence] Failed to auto-connect player ${guildId} on load:`, err);
781
+ }
782
+ const queue = data.queue;
783
+ player.queue.clear();
784
+ player.queue.loop(queue.loopMode);
785
+ player.queue.autoPlay(queue.autoPlay);
786
+ if (queue.tracks.length > 0) {
787
+ const tracks = queue.tracks.map((t) => this.deserializeTrack(t));
788
+ player.queue.addMultiple(tracks);
789
+ }
790
+ if (queue.current && player.connection) {
791
+ const currentTrack = this.deserializeTrack(queue.current);
792
+ player.queue.willNextTrack(currentTrack);
793
+ if (restorePosition && queue.position && queue.position > 0) {
794
+ await player.refreshPlayerResource(true, queue.position);
795
+ }
796
+ else {
797
+ await player.play(currentTrack);
798
+ }
799
+ }
800
+ player.setVolume(data.volume);
801
+ if (data.filters && data.filters.length > 0) {
802
+ try {
803
+ const filterManager = player.filter;
804
+ if (filterManager && typeof filterManager.applyFilters === "function") {
805
+ await filterManager.applyFilters(data.filters);
806
+ }
807
+ }
808
+ catch (e) {
809
+ this.debug(`Failed to restore filters for ${guildId}:`, e);
810
+ }
811
+ }
812
+ if (data.isPaused) {
813
+ player.pause();
814
+ }
815
+ else if (!data.isPlaying) {
816
+ player.stop();
817
+ }
818
+ else if (data.isPlaying) {
819
+ player.resume();
820
+ }
821
+ this.restoredPlayers.add(guildId);
822
+ await this.clearDestroyedStatus(guildId);
823
+ this.debug(`Loaded player: ${guildId}`);
824
+ this.emit("playerLoaded", guildId, data);
825
+ return true;
826
+ }
827
+ catch (error) {
828
+ this.debug(`Failed to load player ${guildId}:`, error);
829
+ this.emit("error", error);
830
+ return false;
831
+ }
832
+ }
833
+ /**
834
+ * Load all saved players with auto-restore logic
835
+ */
836
+ async loadAll(restorePosition = true) {
837
+ if (!this.options.enabled)
838
+ return new Map();
839
+ await this.loadDestroyedStatus();
840
+ const results = new Map();
841
+ try {
842
+ const keys = await this.provider.list();
843
+ const playerKeys = keys.filter((k) => k !== "__destroyed_players__");
844
+ this.debug(`Found ${playerKeys.length} saved players`);
845
+ if (this.options.autoRestoreOnRestart) {
846
+ this.debug(`Auto-restore enabled, restoring players after ${this.options.restoreDelay}ms delay...`);
847
+ if (this.options.restoreDelay && this.options.restoreDelay > 0) {
848
+ await new Promise((resolve) => setTimeout(resolve, this.options.restoreDelay));
849
+ }
850
+ for (const guildId of playerKeys) {
851
+ const success = await this.loadPlayer(guildId, restorePosition, true);
852
+ results.set(guildId, success);
853
+ }
854
+ this.emit("loadedAll", results);
855
+ }
856
+ }
857
+ catch (error) {
858
+ this.debug("Failed to load players:", error);
859
+ this.emit("error", error);
860
+ }
861
+ return results;
862
+ }
863
+ /**
864
+ * Mark a player as destroyed
865
+ */
866
+ async markPlayerDestroyed(guildId, reason) {
867
+ this.destroyedPlayers.set(guildId, {
868
+ guildId,
869
+ destroyedAt: Date.now(),
870
+ reason: reason || "manual_destroy",
871
+ });
872
+ try {
873
+ const data = await this.provider.load(guildId);
874
+ if (data) {
875
+ data.wasDestroyed = true;
876
+ data.destroyedAt = Date.now();
877
+ await this.provider.save(guildId, data, this.options.compress);
878
+ }
879
+ }
880
+ catch (error) {
881
+ this.debug(`Failed to mark player data as destroyed: ${guildId}`, error);
882
+ }
883
+ await this.saveDestroyedStatus();
884
+ this.debug(`Marked player as destroyed (won't restore on restart): ${guildId}`);
885
+ this.emit("playerMarkedDestroyed", guildId);
886
+ }
887
+ /**
888
+ * Clear destroyed status for a player
889
+ */
890
+ async clearDestroyed(guildId) {
891
+ await this.clearDestroyedStatus(guildId);
892
+ try {
893
+ const data = await this.provider.load(guildId);
894
+ if (data) {
895
+ data.wasDestroyed = false;
896
+ delete data.destroyedAt;
897
+ await this.provider.save(guildId, data, this.options.compress);
898
+ }
899
+ }
900
+ catch (error) {
901
+ this.debug(`Failed to clear destroyed flag in data: ${guildId}`, error);
902
+ }
903
+ this.debug(`Cleared destroyed status for: ${guildId}`);
904
+ this.emit("playerDestroyedCleared", guildId);
905
+ }
906
+ /**
907
+ * Delete a player's saved data
908
+ */
909
+ async deletePlayer(guildId) {
910
+ if (!this.options.enabled)
911
+ return false;
912
+ try {
913
+ await this.provider.delete(guildId);
914
+ await this.clearDestroyedStatus(guildId);
915
+ this.restoredPlayers.delete(guildId);
916
+ this.debug(`Deleted saved data for: ${guildId}`);
917
+ this.emit("playerDeleted", guildId);
918
+ return true;
919
+ }
920
+ catch (error) {
921
+ this.debug(`Failed to delete player ${guildId}:`, error);
922
+ return false;
923
+ }
924
+ }
925
+ /**
926
+ * Restore from backup
927
+ */
928
+ async restoreBackup(guildId, timestamp) {
929
+ if (!(this.provider instanceof FileProvider)) {
930
+ this.debug("Restore from backup only supported for file provider");
931
+ return false;
932
+ }
933
+ const success = await this.provider.restoreBackup(guildId, timestamp);
934
+ if (success) {
935
+ await this.clearDestroyed(guildId);
936
+ }
937
+ return success;
938
+ }
939
+ /**
940
+ * Clean all backups for a specific player
941
+ */
942
+ async cleanBackupsForPlayer(guildId) {
943
+ if (!(this.provider instanceof FileProvider)) {
944
+ this.debug("Backup cleanup only supported for file provider");
945
+ return 0;
946
+ }
947
+ const deleted = await this.provider.cleanAllBackupsForPlayer(guildId);
948
+ this.debug(`Cleaned ${deleted} backups for player: ${guildId}`);
949
+ this.emit("backupsCleaned", guildId, deleted);
950
+ return deleted;
951
+ }
952
+ /**
953
+ * Clean all backups
954
+ */
955
+ async cleanAllBackups() {
956
+ if (!(this.provider instanceof FileProvider)) {
957
+ this.debug("Backup cleanup only supported for file provider");
958
+ return 0;
959
+ }
960
+ const deleted = await this.provider.cleanAllBackups();
961
+ this.debug(`Cleaned all ${deleted} backups`);
962
+ this.emit("allBackupsCleaned", deleted);
963
+ return deleted;
964
+ }
965
+ /**
966
+ * Get backup statistics
967
+ */
968
+ getBackupStats() {
969
+ if (!(this.provider instanceof FileProvider)) {
970
+ this.debug("Backup stats only supported for file provider");
971
+ return null;
972
+ }
973
+ const stats = this.provider.getBackupStats();
974
+ return {
975
+ totalBackups: stats.totalBackups,
976
+ totalSizeMB: stats.totalSize / 1024 / 1024,
977
+ oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup) : null,
978
+ newestBackup: stats.newestBackup ? new Date(stats.newestBackup) : null,
979
+ backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
980
+ };
981
+ }
982
+ /**
983
+ * Stop auto-save and clean up
984
+ */
985
+ async shutdown() {
986
+ if (this.saveInterval) {
987
+ clearInterval(this.saveInterval);
988
+ this.saveInterval = null;
989
+ }
990
+ await this.saveAll();
991
+ await this.saveDestroyedStatus();
992
+ this.debug("Persistence manager shut down");
993
+ }
994
+ /**
995
+ * Get list of destroyed players
996
+ */
997
+ getDestroyedPlayers() {
998
+ return Array.from(this.destroyedPlayers.values());
999
+ }
1000
+ /**
1001
+ * Check if auto-restore is enabled
1002
+ */
1003
+ isAutoRestoreEnabled() {
1004
+ return this.options.autoRestoreOnRestart === true;
1005
+ }
1006
+ }
1007
+ exports.PersistenceManager = PersistenceManager;
1008
+ //# sourceMappingURL=PersistenceManager.js.map