ziplayer 0.2.7-dev.1 → 0.2.7-dev.3

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.
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.PersistenceManager = exports.FileProvider = void 0;
36
+ exports.PersistenceManager = void 0;
37
37
  const events_1 = require("events");
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
@@ -41,11 +41,13 @@ const zlib = __importStar(require("zlib"));
41
41
  const util_1 = require("util");
42
42
  const gzip = (0, util_1.promisify)(zlib.gzip);
43
43
  const gunzip = (0, util_1.promisify)(zlib.gunzip);
44
- // File provider implementation
44
+ // File provider implementation with enhanced backup management
45
45
  class FileProvider {
46
- constructor(basePath, maxBackups = 5) {
46
+ constructor(basePath, options = {}) {
47
47
  this.basePath = basePath;
48
- this.maxBackups = maxBackups;
48
+ this.maxBackups = options.maxBackups ?? 5;
49
+ this.maxTotalBackups = options.maxTotalBackups ?? 50;
50
+ this.backupRetentionDays = options.backupRetentionDays ?? 7;
49
51
  if (!fs.existsSync(basePath)) {
50
52
  fs.mkdirSync(basePath, { recursive: true });
51
53
  }
@@ -56,17 +58,142 @@ class FileProvider {
56
58
  getBackupPath(key, timestamp) {
57
59
  return path.join(this.basePath, `${key}_backup_${timestamp}.json`);
58
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
+ }
59
86
  cleanOldBackups(key) {
60
- const backups = fs
61
- .readdirSync(this.basePath)
62
- .filter((f) => f.startsWith(key) && f.includes("backup"))
63
- .sort()
64
- .reverse();
65
- // Keep only maxBackups most recent
87
+ const backups = this.getBackupsByKey(key);
88
+ // Delete old backups exceeding maxBackups per player
66
89
  for (let i = this.maxBackups; i < backups.length; i++) {
67
- const backupPath = path.join(this.basePath, backups[i]);
68
- fs.unlinkSync(backupPath);
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);
69
189
  }
190
+ return {
191
+ totalBackups: backups.length,
192
+ totalSize,
193
+ oldestBackup,
194
+ newestBackup,
195
+ backupsByPlayer,
196
+ };
70
197
  }
71
198
  async save(key, data, compress = false) {
72
199
  const filePath = this.getFilePath(key);
@@ -82,6 +209,8 @@ class FileProvider {
82
209
  const backupPath = this.getBackupPath(key, Date.now());
83
210
  fs.copyFileSync(filePath, backupPath);
84
211
  this.cleanOldBackups(key);
212
+ this.cleanTotalBackupsLimit();
213
+ this.cleanOldBackupsByAge();
85
214
  }
86
215
  fs.writeFileSync(filePath, content);
87
216
  }
@@ -107,10 +236,19 @@ class FileProvider {
107
236
  fs.unlinkSync(filePath);
108
237
  if (fs.existsSync(gzPath))
109
238
  fs.unlinkSync(gzPath);
239
+ // Also delete backups for this player
240
+ await this.cleanAllBackupsForPlayer(key);
110
241
  }
111
242
  async list() {
112
243
  const files = fs.readdirSync(this.basePath);
113
- return files.filter((f) => f.endsWith(".json") || f.endsWith(".json.gz")).map((f) => f.replace(/\.json(\.gz)?$/, ""));
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)?$/, ""));
114
252
  }
115
253
  async restoreBackup(key, backupTimestamp) {
116
254
  let backupFile = null;
@@ -122,13 +260,9 @@ class FileProvider {
122
260
  }
123
261
  else {
124
262
  // Get latest backup
125
- const backups = fs
126
- .readdirSync(this.basePath)
127
- .filter((f) => f.startsWith(key) && f.includes("backup"))
128
- .sort()
129
- .reverse();
263
+ const backups = this.getBackupsByKey(key);
130
264
  if (backups.length > 0) {
131
- backupFile = path.join(this.basePath, backups[0]);
265
+ backupFile = backups[0].path;
132
266
  }
133
267
  }
134
268
  if (backupFile && fs.existsSync(backupFile)) {
@@ -140,8 +274,7 @@ class FileProvider {
140
274
  return false;
141
275
  }
142
276
  }
143
- exports.FileProvider = FileProvider;
144
- // Custom provider for database integration
277
+ // Custom provider for database integration (giữ nguyên)
145
278
  class CustomProvider {
146
279
  constructor(saveFn, loadFn, deleteFn, listFn) {
147
280
  this.saveFn = saveFn;
@@ -172,18 +305,27 @@ class PersistenceManager extends events_1.EventEmitter {
172
305
  super();
173
306
  this.saveInterval = null;
174
307
  this.isSaving = false;
308
+ this.isRestoring = false;
309
+ this.destroyedPlayers = new Map();
310
+ this.restoredPlayers = new Set();
311
+ this.backupCleanupDone = false;
175
312
  this.manager = manager;
176
- // Fix: Don't use spread that causes duplicate 'enabled'
313
+ // Default options
177
314
  this.options = {
178
315
  enabled: true,
179
316
  provider: "file",
180
317
  saveInterval: 60000,
181
318
  autoLoad: true,
319
+ autoRestoreOnRestart: true,
320
+ restoreDelay: 5000,
182
321
  maxBackups: 5,
322
+ maxTotalBackups: 10,
323
+ autoCleanupBackupsOnStart: true,
324
+ backupRetentionDays: 2,
183
325
  compress: false,
184
326
  filePath: "./players_data",
185
327
  };
186
- // Merge options manually to avoid spread duplication
328
+ // Merge manually
187
329
  if (options.enabled !== undefined)
188
330
  this.options.enabled = options.enabled;
189
331
  if (options.provider !== undefined)
@@ -192,8 +334,18 @@ class PersistenceManager extends events_1.EventEmitter {
192
334
  this.options.saveInterval = options.saveInterval;
193
335
  if (options.autoLoad !== undefined)
194
336
  this.options.autoLoad = options.autoLoad;
337
+ if (options.autoRestoreOnRestart !== undefined)
338
+ this.options.autoRestoreOnRestart = options.autoRestoreOnRestart;
339
+ if (options.restoreDelay !== undefined)
340
+ this.options.restoreDelay = options.restoreDelay;
195
341
  if (options.maxBackups !== undefined)
196
342
  this.options.maxBackups = options.maxBackups;
343
+ if (options.maxTotalBackups !== undefined)
344
+ this.options.maxTotalBackups = options.maxTotalBackups;
345
+ if (options.autoCleanupBackupsOnStart !== undefined)
346
+ this.options.autoCleanupBackupsOnStart = options.autoCleanupBackupsOnStart;
347
+ if (options.backupRetentionDays !== undefined)
348
+ this.options.backupRetentionDays = options.backupRetentionDays;
197
349
  if (options.compress !== undefined)
198
350
  this.options.compress = options.compress;
199
351
  if (options.filePath !== undefined)
@@ -210,7 +362,17 @@ class PersistenceManager extends events_1.EventEmitter {
210
362
  this.options.delete = options.delete;
211
363
  if (options.list !== undefined)
212
364
  this.options.list = options.list;
365
+ if (options.autoConnect !== undefined)
366
+ this.options.autoConnect = options.autoConnect;
213
367
  this.provider = this.createProvider();
368
+ // Hook into player destroy events
369
+ this.setupDestroyTracking();
370
+ // Clean up old backups on start
371
+ if (this.options.enabled && this.options.autoCleanupBackupsOnStart) {
372
+ this.cleanupBackupsOnStart().catch((err) => {
373
+ this.debug("Backup cleanup on start error:", err);
374
+ });
375
+ }
214
376
  if (this.options.enabled) {
215
377
  this.startAutoSave();
216
378
  if (this.options.autoLoad) {
@@ -220,28 +382,32 @@ class PersistenceManager extends events_1.EventEmitter {
220
382
  }
221
383
  }
222
384
  }
385
+ setupDestroyTracking() {
386
+ this.manager.on("playerDestroy", (player) => {
387
+ this.markAsDestroyed(player.guildId);
388
+ });
389
+ }
223
390
  createProvider() {
224
391
  switch (this.options.provider) {
225
392
  case "file":
226
- return new FileProvider(this.options.filePath, this.options.maxBackups);
393
+ return new FileProvider(this.options.filePath, {
394
+ maxBackups: this.options.maxBackups,
395
+ maxTotalBackups: this.options.maxTotalBackups,
396
+ backupRetentionDays: this.options.backupRetentionDays,
397
+ });
227
398
  case "redis":
228
- // Implement Redis provider if needed
229
399
  throw new Error("Redis provider not implemented yet");
230
400
  case "database":
231
401
  if (!this.options.save || !this.options.load) {
232
402
  throw new Error("Database provider requires save/load functions");
233
403
  }
234
- // Fix: Pass the save and load functions with correct signatures
235
404
  return new CustomProvider(async (key, data) => {
236
405
  if (this.options.save) {
237
- // Call with single object argument if that's expected
238
406
  const saveFn = this.options.save;
239
407
  if (saveFn.length === 1) {
240
- // Save function expects { key, data }
241
408
  await saveFn({ key, data });
242
409
  }
243
410
  else {
244
- // Save function expects (key, data)
245
411
  await saveFn(key, data);
246
412
  }
247
413
  }
@@ -249,19 +415,21 @@ class PersistenceManager extends events_1.EventEmitter {
249
415
  if (this.options.load) {
250
416
  const loadFn = this.options.load;
251
417
  if (loadFn.length === 0) {
252
- // Load function expects no args, returns all data
253
418
  const allData = await loadFn();
254
419
  return allData?.get?.(key) || allData?.[key] || null;
255
420
  }
256
421
  else {
257
- // Load function expects key
258
422
  return await loadFn(key);
259
423
  }
260
424
  }
261
425
  return null;
262
426
  }, this.options.delete, this.options.list);
263
427
  default:
264
- return new FileProvider(this.options.filePath, this.options.maxBackups);
428
+ return new FileProvider(this.options.filePath, {
429
+ maxBackups: this.options.maxBackups,
430
+ maxTotalBackups: this.options.maxTotalBackups,
431
+ backupRetentionDays: this.options.backupRetentionDays,
432
+ });
265
433
  }
266
434
  }
267
435
  debug(message, ...params) {
@@ -281,8 +449,137 @@ class PersistenceManager extends events_1.EventEmitter {
281
449
  }, this.options.saveInterval);
282
450
  this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
283
451
  }
452
+ // NEW: Cleanup backups on startup
453
+ async cleanupBackupsOnStart() {
454
+ if (this.backupCleanupDone)
455
+ return;
456
+ this.debug("Starting backup cleanup on startup...");
457
+ // Only works for file provider
458
+ if (this.provider instanceof FileProvider) {
459
+ try {
460
+ // Clean old backups by age
461
+ // This is already handled in FileProvider, but we can log stats
462
+ const stats = this.provider.getBackupStats();
463
+ this.debug(`Backup stats before cleanup:`, {
464
+ totalBackups: stats.totalBackups,
465
+ totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2),
466
+ oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup).toISOString() : null,
467
+ newestBackup: stats.newestBackup ? new Date(stats.newestBackup).toISOString() : null,
468
+ backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
469
+ });
470
+ // Emit stats event
471
+ this.emit("backupStats", stats);
472
+ // The cleanup is already happening in FileProvider.save()
473
+ // But we can do a one-time deep cleanup on start
474
+ if (this.options.backupRetentionDays && this.options.backupRetentionDays > 0) {
475
+ // Force a cleanup pass
476
+ const deletedCount = await this.cleanOldBackupsByAge();
477
+ if (deletedCount > 0) {
478
+ this.debug(`Cleaned up ${deletedCount} old backups on startup`);
479
+ }
480
+ }
481
+ // Enforce total backup limit
482
+ const totalLimitDeleted = await this.enforceTotalBackupLimit();
483
+ if (totalLimitDeleted > 0) {
484
+ this.debug(`Enforced backup limit: deleted ${totalLimitDeleted} backups`);
485
+ }
486
+ this.backupCleanupDone = true;
487
+ this.emit("backupCleanupDone");
488
+ }
489
+ catch (error) {
490
+ this.debug("Backup cleanup error:", error);
491
+ }
492
+ }
493
+ else {
494
+ this.debug("Backup cleanup only supported for file provider");
495
+ }
496
+ }
497
+ // NEW: Clean old backups by age
498
+ async cleanOldBackupsByAge() {
499
+ if (!(this.provider instanceof FileProvider))
500
+ return 0;
501
+ const retentionMs = (this.options.backupRetentionDays ?? 2) * 24 * 60 * 60 * 1000;
502
+ const now = Date.now();
503
+ const backups = this.provider.getAllBackups();
504
+ let deletedCount = 0;
505
+ for (const backup of backups) {
506
+ if (now - backup.timestamp > retentionMs) {
507
+ try {
508
+ fs.unlinkSync(backup.path);
509
+ deletedCount++;
510
+ }
511
+ catch (err) {
512
+ this.debug(`Failed to delete old backup: ${backup.path}`, err);
513
+ }
514
+ }
515
+ }
516
+ if (deletedCount > 0) {
517
+ this.debug(`Deleted ${deletedCount} backups older than ${this.options.backupRetentionDays} days`);
518
+ }
519
+ return deletedCount;
520
+ }
521
+ // NEW: Enforce total backup limit
522
+ async enforceTotalBackupLimit() {
523
+ if (!(this.provider instanceof FileProvider))
524
+ return 0;
525
+ const backups = this.provider.getAllBackups();
526
+ const maxTotal = this.options.maxTotalBackups ?? 50;
527
+ if (backups.length <= maxTotal)
528
+ return 0;
529
+ const toDelete = backups.slice(maxTotal);
530
+ let deletedCount = 0;
531
+ for (const backup of toDelete) {
532
+ try {
533
+ fs.unlinkSync(backup.path);
534
+ deletedCount++;
535
+ }
536
+ catch (err) {
537
+ this.debug(`Failed to delete backup: ${backup.path}`, err);
538
+ }
539
+ }
540
+ if (deletedCount > 0) {
541
+ this.debug(`Deleted ${deletedCount} backups (exceeded limit ${maxTotal})`);
542
+ }
543
+ return deletedCount;
544
+ }
545
+ markAsDestroyed(guildId) {
546
+ this.destroyedPlayers.set(guildId, {
547
+ guildId,
548
+ destroyedAt: Date.now(),
549
+ reason: "player_destroy",
550
+ });
551
+ this.debug(`Marked player as destroyed: ${guildId}`);
552
+ this.saveDestroyedStatus().catch((err) => {
553
+ this.debug("Failed to save destroyed status:", err);
554
+ });
555
+ }
556
+ isDestroyed(guildId) {
557
+ return this.destroyedPlayers.has(guildId);
558
+ }
559
+ async saveDestroyedStatus() {
560
+ const destroyedData = Array.from(this.destroyedPlayers.values());
561
+ await this.provider.save("__destroyed_players__", destroyedData);
562
+ }
563
+ async loadDestroyedStatus() {
564
+ try {
565
+ const data = await this.provider.load("__destroyed_players__");
566
+ if (data && Array.isArray(data)) {
567
+ this.destroyedPlayers.clear();
568
+ for (const record of data) {
569
+ this.destroyedPlayers.set(record.guildId, record);
570
+ }
571
+ this.debug(`Loaded ${this.destroyedPlayers.size} destroyed player records`);
572
+ }
573
+ }
574
+ catch (error) {
575
+ this.debug("Failed to load destroyed status:", error);
576
+ }
577
+ }
578
+ async clearDestroyedStatus(guildId) {
579
+ this.destroyedPlayers.delete(guildId);
580
+ await this.saveDestroyedStatus();
581
+ }
284
582
  serializeTrack(track) {
285
- // Create base object with required fields only (avoid duplication)
286
583
  const serialized = {
287
584
  id: track.id,
288
585
  title: track.title,
@@ -293,13 +590,11 @@ class PersistenceManager extends events_1.EventEmitter {
293
590
  requestedBy: track.requestedBy,
294
591
  isLive: track.isLive || false,
295
592
  };
296
- // Add optional fields if they exist on the track
297
593
  const trackAny = track;
298
594
  if (trackAny.author)
299
595
  serialized.author = trackAny.author;
300
596
  if (trackAny.artwork)
301
597
  serialized.artwork = trackAny.artwork;
302
- // Add any extra metadata (excluding fields we already set)
303
598
  const excludedFields = new Set([
304
599
  "id",
305
600
  "title",
@@ -330,7 +625,6 @@ class PersistenceManager extends events_1.EventEmitter {
330
625
  };
331
626
  }
332
627
  serializePlayer(player) {
333
- // Get filters safely - access through public method
334
628
  let filters = [];
335
629
  try {
336
630
  const filterString = player.filter?.getFilterString();
@@ -338,9 +632,7 @@ class PersistenceManager extends events_1.EventEmitter {
338
632
  filters = filterString.split(",").filter(Boolean);
339
633
  }
340
634
  }
341
- catch (e) {
342
- // Filter may not be accessible
343
- }
635
+ catch (e) { }
344
636
  return {
345
637
  guildId: player.guildId,
346
638
  queue: this.serializeQueue(player),
@@ -351,10 +643,11 @@ class PersistenceManager extends events_1.EventEmitter {
351
643
  filters: filters.length > 0 ? filters : undefined,
352
644
  lastUpdate: Date.now(),
353
645
  version: "1.0.0",
646
+ wasDestroyed: false,
647
+ channelConnection: player.channelConnection,
354
648
  };
355
649
  }
356
650
  deserializeTrack(data) {
357
- // Create base track object
358
651
  const track = {
359
652
  id: data.id,
360
653
  title: data.title,
@@ -365,12 +658,10 @@ class PersistenceManager extends events_1.EventEmitter {
365
658
  requestedBy: data.requestedBy,
366
659
  isLive: data.isLive || false,
367
660
  };
368
- // Add optional fields if they exist
369
661
  if (data.author)
370
662
  track.author = data.author;
371
663
  if (data.artwork)
372
664
  track.artwork = data.artwork;
373
- // Add any extra metadata from serialized data
374
665
  for (const key of Object.keys(data)) {
375
666
  if (!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)) {
376
667
  track[key] = data[key];
@@ -384,9 +675,13 @@ class PersistenceManager extends events_1.EventEmitter {
384
675
  async savePlayer(player) {
385
676
  if (!this.options.enabled)
386
677
  return false;
678
+ if (this.isDestroyed(player.guildId)) {
679
+ this.debug(`Skipping save for destroyed player: ${player.guildId}`);
680
+ return false;
681
+ }
387
682
  try {
388
683
  const data = this.serializePlayer(player);
389
- await this.provider.save(player.guildId, data);
684
+ await this.provider.save(player.guildId, data, this.options.compress);
390
685
  this.debug(`Saved player: ${player.guildId}`);
391
686
  this.emit("playerSaved", player.guildId);
392
687
  return true;
@@ -408,7 +703,6 @@ class PersistenceManager extends events_1.EventEmitter {
408
703
  try {
409
704
  const players = this.manager.getAll();
410
705
  this.debug(`Saving ${players.length} players...`);
411
- // Save in parallel with limit
412
706
  const batchSize = 5;
413
707
  for (let i = 0; i < players.length; i += batchSize) {
414
708
  const batch = players.slice(i, i + batchSize);
@@ -427,34 +721,51 @@ class PersistenceManager extends events_1.EventEmitter {
427
721
  /**
428
722
  * Load a single player
429
723
  */
430
- async loadPlayer(guildId, restorePosition = true) {
724
+ async loadPlayer(guildId, restorePosition = true, skipIfDestroyed = true) {
431
725
  if (!this.options.enabled)
432
726
  return false;
727
+ if (skipIfDestroyed && this.isDestroyed(guildId)) {
728
+ this.debug(`Skipping load for destroyed player: ${guildId}`);
729
+ this.emit("playerSkipped", guildId, "destroyed");
730
+ return false;
731
+ }
732
+ if (this.restoredPlayers.has(guildId)) {
733
+ this.debug(`Skipping already restored player: ${guildId}`);
734
+ return true;
735
+ }
433
736
  try {
434
737
  const data = await this.provider.load(guildId);
435
738
  if (!data)
436
739
  return false;
437
- // Check if player already exists
740
+ if (data.wasDestroyed === true) {
741
+ this.debug(`Skipping load for player marked as destroyed: ${guildId}`);
742
+ return false;
743
+ }
438
744
  let player = this.manager.get(guildId);
439
745
  if (!player) {
440
746
  player = await this.manager.create(guildId, data.options);
441
747
  }
442
- // Restore queue
748
+ // try connecting to the voice channel if we have one saved and autoConnect is enabled
749
+ if (data.options?.autoConnect && player.connection === null) {
750
+ try {
751
+ const voicechannel = await data.channelConnection;
752
+ await player.connect(voicechannel);
753
+ }
754
+ catch (err) {
755
+ this.debug(`Failed to auto-connect player ${guildId} on load:`, err);
756
+ }
757
+ }
443
758
  const queue = data.queue;
444
- // Clear current queue
445
759
  player.queue.clear();
446
760
  player.queue.loop(queue.loopMode);
447
761
  player.queue.autoPlay(queue.autoPlay);
448
- // Restore tracks
449
762
  if (queue.tracks.length > 0) {
450
763
  const tracks = queue.tracks.map((t) => this.deserializeTrack(t));
451
764
  player.queue.addMultiple(tracks);
452
765
  }
453
- // Restore current track if exists
454
766
  if (queue.current && player.connection) {
455
767
  const currentTrack = this.deserializeTrack(queue.current);
456
768
  player.queue.willNextTrack(currentTrack);
457
- // Restore playback position if requested
458
769
  if (restorePosition && queue.position && queue.position > 0) {
459
770
  await player.refreshPlayerResource(true, queue.position);
460
771
  }
@@ -462,9 +773,7 @@ class PersistenceManager extends events_1.EventEmitter {
462
773
  await player.play(currentTrack);
463
774
  }
464
775
  }
465
- // Restore volume
466
776
  player.setVolume(data.volume);
467
- // Restore filters - safely access through public method
468
777
  if (data.filters && data.filters.length > 0) {
469
778
  try {
470
779
  const filterManager = player.filter;
@@ -476,6 +785,8 @@ class PersistenceManager extends events_1.EventEmitter {
476
785
  this.debug(`Failed to restore filters for ${guildId}:`, e);
477
786
  }
478
787
  }
788
+ this.restoredPlayers.add(guildId);
789
+ await this.clearDestroyedStatus(guildId);
479
790
  this.debug(`Loaded player: ${guildId}`);
480
791
  this.emit("playerLoaded", guildId, data);
481
792
  return true;
@@ -487,20 +798,28 @@ class PersistenceManager extends events_1.EventEmitter {
487
798
  }
488
799
  }
489
800
  /**
490
- * Load all saved players
801
+ * Load all saved players with auto-restore logic
491
802
  */
492
803
  async loadAll(restorePosition = true) {
493
804
  if (!this.options.enabled)
494
805
  return new Map();
806
+ await this.loadDestroyedStatus();
495
807
  const results = new Map();
496
808
  try {
497
809
  const keys = await this.provider.list();
498
- this.debug(`Found ${keys.length} saved players`);
499
- for (const guildId of keys) {
500
- const success = await this.loadPlayer(guildId, restorePosition);
501
- results.set(guildId, success);
810
+ const playerKeys = keys.filter((k) => k !== "__destroyed_players__");
811
+ this.debug(`Found ${playerKeys.length} saved players`);
812
+ if (this.options.autoRestoreOnRestart) {
813
+ this.debug(`Auto-restore enabled, restoring players after ${this.options.restoreDelay}ms delay...`);
814
+ if (this.options.restoreDelay && this.options.restoreDelay > 0) {
815
+ await new Promise((resolve) => setTimeout(resolve, this.options.restoreDelay));
816
+ }
817
+ for (const guildId of playerKeys) {
818
+ const success = await this.loadPlayer(guildId, restorePosition, true);
819
+ results.set(guildId, success);
820
+ }
821
+ this.emit("loadedAll", results);
502
822
  }
503
- this.emit("loadedAll", results);
504
823
  }
505
824
  catch (error) {
506
825
  this.debug("Failed to load players:", error);
@@ -508,6 +827,49 @@ class PersistenceManager extends events_1.EventEmitter {
508
827
  }
509
828
  return results;
510
829
  }
830
+ /**
831
+ * Mark a player as destroyed
832
+ */
833
+ async markPlayerDestroyed(guildId, reason) {
834
+ this.destroyedPlayers.set(guildId, {
835
+ guildId,
836
+ destroyedAt: Date.now(),
837
+ reason: reason || "manual_destroy",
838
+ });
839
+ try {
840
+ const data = await this.provider.load(guildId);
841
+ if (data) {
842
+ data.wasDestroyed = true;
843
+ data.destroyedAt = Date.now();
844
+ await this.provider.save(guildId, data, this.options.compress);
845
+ }
846
+ }
847
+ catch (error) {
848
+ this.debug(`Failed to mark player data as destroyed: ${guildId}`, error);
849
+ }
850
+ await this.saveDestroyedStatus();
851
+ this.debug(`Marked player as destroyed (won't restore on restart): ${guildId}`);
852
+ this.emit("playerMarkedDestroyed", guildId);
853
+ }
854
+ /**
855
+ * Clear destroyed status for a player
856
+ */
857
+ async clearDestroyed(guildId) {
858
+ await this.clearDestroyedStatus(guildId);
859
+ try {
860
+ const data = await this.provider.load(guildId);
861
+ if (data) {
862
+ data.wasDestroyed = false;
863
+ delete data.destroyedAt;
864
+ await this.provider.save(guildId, data, this.options.compress);
865
+ }
866
+ }
867
+ catch (error) {
868
+ this.debug(`Failed to clear destroyed flag in data: ${guildId}`, error);
869
+ }
870
+ this.debug(`Cleared destroyed status for: ${guildId}`);
871
+ this.emit("playerDestroyedCleared", guildId);
872
+ }
511
873
  /**
512
874
  * Delete a player's saved data
513
875
  */
@@ -516,6 +878,8 @@ class PersistenceManager extends events_1.EventEmitter {
516
878
  return false;
517
879
  try {
518
880
  await this.provider.delete(guildId);
881
+ await this.clearDestroyedStatus(guildId);
882
+ this.restoredPlayers.delete(guildId);
519
883
  this.debug(`Deleted saved data for: ${guildId}`);
520
884
  this.emit("playerDeleted", guildId);
521
885
  return true;
@@ -533,7 +897,54 @@ class PersistenceManager extends events_1.EventEmitter {
533
897
  this.debug("Restore from backup only supported for file provider");
534
898
  return false;
535
899
  }
536
- return await this.provider.restoreBackup(guildId, timestamp);
900
+ const success = await this.provider.restoreBackup(guildId, timestamp);
901
+ if (success) {
902
+ await this.clearDestroyed(guildId);
903
+ }
904
+ return success;
905
+ }
906
+ /**
907
+ * Clean all backups for a specific player
908
+ */
909
+ async cleanBackupsForPlayer(guildId) {
910
+ if (!(this.provider instanceof FileProvider)) {
911
+ this.debug("Backup cleanup only supported for file provider");
912
+ return 0;
913
+ }
914
+ const deleted = await this.provider.cleanAllBackupsForPlayer(guildId);
915
+ this.debug(`Cleaned ${deleted} backups for player: ${guildId}`);
916
+ this.emit("backupsCleaned", guildId, deleted);
917
+ return deleted;
918
+ }
919
+ /**
920
+ * Clean all backups
921
+ */
922
+ async cleanAllBackups() {
923
+ if (!(this.provider instanceof FileProvider)) {
924
+ this.debug("Backup cleanup only supported for file provider");
925
+ return 0;
926
+ }
927
+ const deleted = await this.provider.cleanAllBackups();
928
+ this.debug(`Cleaned all ${deleted} backups`);
929
+ this.emit("allBackupsCleaned", deleted);
930
+ return deleted;
931
+ }
932
+ /**
933
+ * Get backup statistics
934
+ */
935
+ getBackupStats() {
936
+ if (!(this.provider instanceof FileProvider)) {
937
+ this.debug("Backup stats only supported for file provider");
938
+ return null;
939
+ }
940
+ const stats = this.provider.getBackupStats();
941
+ return {
942
+ totalBackups: stats.totalBackups,
943
+ totalSizeMB: stats.totalSize / 1024 / 1024,
944
+ oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup) : null,
945
+ newestBackup: stats.newestBackup ? new Date(stats.newestBackup) : null,
946
+ backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
947
+ };
537
948
  }
538
949
  /**
539
950
  * Stop auto-save and clean up
@@ -544,8 +955,21 @@ class PersistenceManager extends events_1.EventEmitter {
544
955
  this.saveInterval = null;
545
956
  }
546
957
  await this.saveAll();
958
+ await this.saveDestroyedStatus();
547
959
  this.debug("Persistence manager shut down");
548
960
  }
961
+ /**
962
+ * Get list of destroyed players
963
+ */
964
+ getDestroyedPlayers() {
965
+ return Array.from(this.destroyedPlayers.values());
966
+ }
967
+ /**
968
+ * Check if auto-restore is enabled
969
+ */
970
+ isAutoRestoreEnabled() {
971
+ return this.options.autoRestoreOnRestart === true;
972
+ }
549
973
  }
550
974
  exports.PersistenceManager = PersistenceManager;
551
975
  //# sourceMappingURL=PersistenceManager.js.map