ziplayer 0.2.7-dev.1 → 0.2.7-dev.2

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,18 +58,143 @@ 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
+ }
69
97
  }
70
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
+ }
71
198
  async save(key, data, compress = false) {
72
199
  const filePath = this.getFilePath(key);
73
200
  let content = JSON.stringify(data, null, 2);
@@ -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)
@@ -211,6 +363,14 @@ class PersistenceManager extends events_1.EventEmitter {
211
363
  if (options.list !== undefined)
212
364
  this.options.list = options.list;
213
365
  this.provider = this.createProvider();
366
+ // Hook into player destroy events
367
+ this.setupDestroyTracking();
368
+ // Clean up old backups on start
369
+ if (this.options.enabled && this.options.autoCleanupBackupsOnStart) {
370
+ this.cleanupBackupsOnStart().catch((err) => {
371
+ this.debug("Backup cleanup on start error:", err);
372
+ });
373
+ }
214
374
  if (this.options.enabled) {
215
375
  this.startAutoSave();
216
376
  if (this.options.autoLoad) {
@@ -220,28 +380,32 @@ class PersistenceManager extends events_1.EventEmitter {
220
380
  }
221
381
  }
222
382
  }
383
+ setupDestroyTracking() {
384
+ this.manager.on("playerDestroy", (player) => {
385
+ this.markAsDestroyed(player.guildId);
386
+ });
387
+ }
223
388
  createProvider() {
224
389
  switch (this.options.provider) {
225
390
  case "file":
226
- return new FileProvider(this.options.filePath, this.options.maxBackups);
391
+ return new FileProvider(this.options.filePath, {
392
+ maxBackups: this.options.maxBackups,
393
+ maxTotalBackups: this.options.maxTotalBackups,
394
+ backupRetentionDays: this.options.backupRetentionDays,
395
+ });
227
396
  case "redis":
228
- // Implement Redis provider if needed
229
397
  throw new Error("Redis provider not implemented yet");
230
398
  case "database":
231
399
  if (!this.options.save || !this.options.load) {
232
400
  throw new Error("Database provider requires save/load functions");
233
401
  }
234
- // Fix: Pass the save and load functions with correct signatures
235
402
  return new CustomProvider(async (key, data) => {
236
403
  if (this.options.save) {
237
- // Call with single object argument if that's expected
238
404
  const saveFn = this.options.save;
239
405
  if (saveFn.length === 1) {
240
- // Save function expects { key, data }
241
406
  await saveFn({ key, data });
242
407
  }
243
408
  else {
244
- // Save function expects (key, data)
245
409
  await saveFn(key, data);
246
410
  }
247
411
  }
@@ -249,19 +413,21 @@ class PersistenceManager extends events_1.EventEmitter {
249
413
  if (this.options.load) {
250
414
  const loadFn = this.options.load;
251
415
  if (loadFn.length === 0) {
252
- // Load function expects no args, returns all data
253
416
  const allData = await loadFn();
254
417
  return allData?.get?.(key) || allData?.[key] || null;
255
418
  }
256
419
  else {
257
- // Load function expects key
258
420
  return await loadFn(key);
259
421
  }
260
422
  }
261
423
  return null;
262
424
  }, this.options.delete, this.options.list);
263
425
  default:
264
- return new FileProvider(this.options.filePath, this.options.maxBackups);
426
+ return new FileProvider(this.options.filePath, {
427
+ maxBackups: this.options.maxBackups,
428
+ maxTotalBackups: this.options.maxTotalBackups,
429
+ backupRetentionDays: this.options.backupRetentionDays,
430
+ });
265
431
  }
266
432
  }
267
433
  debug(message, ...params) {
@@ -281,8 +447,137 @@ class PersistenceManager extends events_1.EventEmitter {
281
447
  }, this.options.saveInterval);
282
448
  this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
283
449
  }
450
+ // NEW: Cleanup backups on startup
451
+ async cleanupBackupsOnStart() {
452
+ if (this.backupCleanupDone)
453
+ return;
454
+ this.debug("Starting backup cleanup on startup...");
455
+ // Only works for file provider
456
+ if (this.provider instanceof FileProvider) {
457
+ try {
458
+ // Clean old backups by age
459
+ // This is already handled in FileProvider, but we can log stats
460
+ const stats = this.provider.getBackupStats();
461
+ this.debug(`Backup stats before cleanup:`, {
462
+ totalBackups: stats.totalBackups,
463
+ totalSizeMB: (stats.totalSize / 1024 / 1024).toFixed(2),
464
+ oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup).toISOString() : null,
465
+ newestBackup: stats.newestBackup ? new Date(stats.newestBackup).toISOString() : null,
466
+ backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
467
+ });
468
+ // Emit stats event
469
+ this.emit("backupStats", stats);
470
+ // The cleanup is already happening in FileProvider.save()
471
+ // But we can do a one-time deep cleanup on start
472
+ if (this.options.backupRetentionDays && this.options.backupRetentionDays > 0) {
473
+ // Force a cleanup pass
474
+ const deletedCount = await this.cleanOldBackupsByAge();
475
+ if (deletedCount > 0) {
476
+ this.debug(`Cleaned up ${deletedCount} old backups on startup`);
477
+ }
478
+ }
479
+ // Enforce total backup limit
480
+ const totalLimitDeleted = await this.enforceTotalBackupLimit();
481
+ if (totalLimitDeleted > 0) {
482
+ this.debug(`Enforced backup limit: deleted ${totalLimitDeleted} backups`);
483
+ }
484
+ this.backupCleanupDone = true;
485
+ this.emit("backupCleanupDone");
486
+ }
487
+ catch (error) {
488
+ this.debug("Backup cleanup error:", error);
489
+ }
490
+ }
491
+ else {
492
+ this.debug("Backup cleanup only supported for file provider");
493
+ }
494
+ }
495
+ // NEW: Clean old backups by age
496
+ async cleanOldBackupsByAge() {
497
+ if (!(this.provider instanceof FileProvider))
498
+ return 0;
499
+ const retentionMs = (this.options.backupRetentionDays ?? 2) * 24 * 60 * 60 * 1000;
500
+ const now = Date.now();
501
+ const backups = this.provider.getAllBackups();
502
+ let deletedCount = 0;
503
+ for (const backup of backups) {
504
+ if (now - backup.timestamp > retentionMs) {
505
+ try {
506
+ fs.unlinkSync(backup.path);
507
+ deletedCount++;
508
+ }
509
+ catch (err) {
510
+ this.debug(`Failed to delete old backup: ${backup.path}`, err);
511
+ }
512
+ }
513
+ }
514
+ if (deletedCount > 0) {
515
+ this.debug(`Deleted ${deletedCount} backups older than ${this.options.backupRetentionDays} days`);
516
+ }
517
+ return deletedCount;
518
+ }
519
+ // NEW: Enforce total backup limit
520
+ async enforceTotalBackupLimit() {
521
+ if (!(this.provider instanceof FileProvider))
522
+ return 0;
523
+ const backups = this.provider.getAllBackups();
524
+ const maxTotal = this.options.maxTotalBackups ?? 50;
525
+ if (backups.length <= maxTotal)
526
+ return 0;
527
+ const toDelete = backups.slice(maxTotal);
528
+ let deletedCount = 0;
529
+ for (const backup of toDelete) {
530
+ try {
531
+ fs.unlinkSync(backup.path);
532
+ deletedCount++;
533
+ }
534
+ catch (err) {
535
+ this.debug(`Failed to delete backup: ${backup.path}`, err);
536
+ }
537
+ }
538
+ if (deletedCount > 0) {
539
+ this.debug(`Deleted ${deletedCount} backups (exceeded limit ${maxTotal})`);
540
+ }
541
+ return deletedCount;
542
+ }
543
+ markAsDestroyed(guildId) {
544
+ this.destroyedPlayers.set(guildId, {
545
+ guildId,
546
+ destroyedAt: Date.now(),
547
+ reason: "player_destroy",
548
+ });
549
+ this.debug(`Marked player as destroyed: ${guildId}`);
550
+ this.saveDestroyedStatus().catch((err) => {
551
+ this.debug("Failed to save destroyed status:", err);
552
+ });
553
+ }
554
+ isDestroyed(guildId) {
555
+ return this.destroyedPlayers.has(guildId);
556
+ }
557
+ async saveDestroyedStatus() {
558
+ const destroyedData = Array.from(this.destroyedPlayers.values());
559
+ await this.provider.save("__destroyed_players__", destroyedData);
560
+ }
561
+ async loadDestroyedStatus() {
562
+ try {
563
+ const data = await this.provider.load("__destroyed_players__");
564
+ if (data && Array.isArray(data)) {
565
+ this.destroyedPlayers.clear();
566
+ for (const record of data) {
567
+ this.destroyedPlayers.set(record.guildId, record);
568
+ }
569
+ this.debug(`Loaded ${this.destroyedPlayers.size} destroyed player records`);
570
+ }
571
+ }
572
+ catch (error) {
573
+ this.debug("Failed to load destroyed status:", error);
574
+ }
575
+ }
576
+ async clearDestroyedStatus(guildId) {
577
+ this.destroyedPlayers.delete(guildId);
578
+ await this.saveDestroyedStatus();
579
+ }
284
580
  serializeTrack(track) {
285
- // Create base object with required fields only (avoid duplication)
286
581
  const serialized = {
287
582
  id: track.id,
288
583
  title: track.title,
@@ -293,13 +588,11 @@ class PersistenceManager extends events_1.EventEmitter {
293
588
  requestedBy: track.requestedBy,
294
589
  isLive: track.isLive || false,
295
590
  };
296
- // Add optional fields if they exist on the track
297
591
  const trackAny = track;
298
592
  if (trackAny.author)
299
593
  serialized.author = trackAny.author;
300
594
  if (trackAny.artwork)
301
595
  serialized.artwork = trackAny.artwork;
302
- // Add any extra metadata (excluding fields we already set)
303
596
  const excludedFields = new Set([
304
597
  "id",
305
598
  "title",
@@ -330,7 +623,6 @@ class PersistenceManager extends events_1.EventEmitter {
330
623
  };
331
624
  }
332
625
  serializePlayer(player) {
333
- // Get filters safely - access through public method
334
626
  let filters = [];
335
627
  try {
336
628
  const filterString = player.filter?.getFilterString();
@@ -338,9 +630,7 @@ class PersistenceManager extends events_1.EventEmitter {
338
630
  filters = filterString.split(",").filter(Boolean);
339
631
  }
340
632
  }
341
- catch (e) {
342
- // Filter may not be accessible
343
- }
633
+ catch (e) { }
344
634
  return {
345
635
  guildId: player.guildId,
346
636
  queue: this.serializeQueue(player),
@@ -351,10 +641,10 @@ class PersistenceManager extends events_1.EventEmitter {
351
641
  filters: filters.length > 0 ? filters : undefined,
352
642
  lastUpdate: Date.now(),
353
643
  version: "1.0.0",
644
+ wasDestroyed: false,
354
645
  };
355
646
  }
356
647
  deserializeTrack(data) {
357
- // Create base track object
358
648
  const track = {
359
649
  id: data.id,
360
650
  title: data.title,
@@ -365,12 +655,10 @@ class PersistenceManager extends events_1.EventEmitter {
365
655
  requestedBy: data.requestedBy,
366
656
  isLive: data.isLive || false,
367
657
  };
368
- // Add optional fields if they exist
369
658
  if (data.author)
370
659
  track.author = data.author;
371
660
  if (data.artwork)
372
661
  track.artwork = data.artwork;
373
- // Add any extra metadata from serialized data
374
662
  for (const key of Object.keys(data)) {
375
663
  if (!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)) {
376
664
  track[key] = data[key];
@@ -384,9 +672,13 @@ class PersistenceManager extends events_1.EventEmitter {
384
672
  async savePlayer(player) {
385
673
  if (!this.options.enabled)
386
674
  return false;
675
+ if (this.isDestroyed(player.guildId)) {
676
+ this.debug(`Skipping save for destroyed player: ${player.guildId}`);
677
+ return false;
678
+ }
387
679
  try {
388
680
  const data = this.serializePlayer(player);
389
- await this.provider.save(player.guildId, data);
681
+ await this.provider.save(player.guildId, data, this.options.compress);
390
682
  this.debug(`Saved player: ${player.guildId}`);
391
683
  this.emit("playerSaved", player.guildId);
392
684
  return true;
@@ -408,7 +700,6 @@ class PersistenceManager extends events_1.EventEmitter {
408
700
  try {
409
701
  const players = this.manager.getAll();
410
702
  this.debug(`Saving ${players.length} players...`);
411
- // Save in parallel with limit
412
703
  const batchSize = 5;
413
704
  for (let i = 0; i < players.length; i += batchSize) {
414
705
  const batch = players.slice(i, i + batchSize);
@@ -427,34 +718,41 @@ class PersistenceManager extends events_1.EventEmitter {
427
718
  /**
428
719
  * Load a single player
429
720
  */
430
- async loadPlayer(guildId, restorePosition = true) {
721
+ async loadPlayer(guildId, restorePosition = true, skipIfDestroyed = true) {
431
722
  if (!this.options.enabled)
432
723
  return false;
724
+ if (skipIfDestroyed && this.isDestroyed(guildId)) {
725
+ this.debug(`Skipping load for destroyed player: ${guildId}`);
726
+ this.emit("playerSkipped", guildId, "destroyed");
727
+ return false;
728
+ }
729
+ if (this.restoredPlayers.has(guildId)) {
730
+ this.debug(`Skipping already restored player: ${guildId}`);
731
+ return true;
732
+ }
433
733
  try {
434
734
  const data = await this.provider.load(guildId);
435
735
  if (!data)
436
736
  return false;
437
- // Check if player already exists
737
+ if (data.wasDestroyed === true) {
738
+ this.debug(`Skipping load for player marked as destroyed: ${guildId}`);
739
+ return false;
740
+ }
438
741
  let player = this.manager.get(guildId);
439
742
  if (!player) {
440
743
  player = await this.manager.create(guildId, data.options);
441
744
  }
442
- // Restore queue
443
745
  const queue = data.queue;
444
- // Clear current queue
445
746
  player.queue.clear();
446
747
  player.queue.loop(queue.loopMode);
447
748
  player.queue.autoPlay(queue.autoPlay);
448
- // Restore tracks
449
749
  if (queue.tracks.length > 0) {
450
750
  const tracks = queue.tracks.map((t) => this.deserializeTrack(t));
451
751
  player.queue.addMultiple(tracks);
452
752
  }
453
- // Restore current track if exists
454
753
  if (queue.current && player.connection) {
455
754
  const currentTrack = this.deserializeTrack(queue.current);
456
755
  player.queue.willNextTrack(currentTrack);
457
- // Restore playback position if requested
458
756
  if (restorePosition && queue.position && queue.position > 0) {
459
757
  await player.refreshPlayerResource(true, queue.position);
460
758
  }
@@ -462,9 +760,7 @@ class PersistenceManager extends events_1.EventEmitter {
462
760
  await player.play(currentTrack);
463
761
  }
464
762
  }
465
- // Restore volume
466
763
  player.setVolume(data.volume);
467
- // Restore filters - safely access through public method
468
764
  if (data.filters && data.filters.length > 0) {
469
765
  try {
470
766
  const filterManager = player.filter;
@@ -476,6 +772,8 @@ class PersistenceManager extends events_1.EventEmitter {
476
772
  this.debug(`Failed to restore filters for ${guildId}:`, e);
477
773
  }
478
774
  }
775
+ this.restoredPlayers.add(guildId);
776
+ await this.clearDestroyedStatus(guildId);
479
777
  this.debug(`Loaded player: ${guildId}`);
480
778
  this.emit("playerLoaded", guildId, data);
481
779
  return true;
@@ -487,18 +785,32 @@ class PersistenceManager extends events_1.EventEmitter {
487
785
  }
488
786
  }
489
787
  /**
490
- * Load all saved players
788
+ * Load all saved players with auto-restore logic
491
789
  */
492
790
  async loadAll(restorePosition = true) {
493
791
  if (!this.options.enabled)
494
792
  return new Map();
793
+ await this.loadDestroyedStatus();
495
794
  const results = new Map();
496
795
  try {
497
796
  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);
797
+ const playerKeys = keys.filter((k) => k !== "__destroyed_players__");
798
+ this.debug(`Found ${playerKeys.length} saved players`);
799
+ if (this.options.autoRestoreOnRestart) {
800
+ this.debug(`Auto-restore enabled, restoring players after ${this.options.restoreDelay}ms delay...`);
801
+ if (this.options.restoreDelay && this.options.restoreDelay > 0) {
802
+ await new Promise((resolve) => setTimeout(resolve, this.options.restoreDelay));
803
+ }
804
+ for (const guildId of playerKeys) {
805
+ const success = await this.loadPlayer(guildId, restorePosition, true);
806
+ results.set(guildId, success);
807
+ }
808
+ }
809
+ else {
810
+ for (const guildId of playerKeys) {
811
+ const success = await this.loadPlayer(guildId, restorePosition, true);
812
+ results.set(guildId, success);
813
+ }
502
814
  }
503
815
  this.emit("loadedAll", results);
504
816
  }
@@ -508,6 +820,49 @@ class PersistenceManager extends events_1.EventEmitter {
508
820
  }
509
821
  return results;
510
822
  }
823
+ /**
824
+ * Mark a player as destroyed
825
+ */
826
+ async markPlayerDestroyed(guildId, reason) {
827
+ this.destroyedPlayers.set(guildId, {
828
+ guildId,
829
+ destroyedAt: Date.now(),
830
+ reason: reason || "manual_destroy",
831
+ });
832
+ try {
833
+ const data = await this.provider.load(guildId);
834
+ if (data) {
835
+ data.wasDestroyed = true;
836
+ data.destroyedAt = Date.now();
837
+ await this.provider.save(guildId, data, this.options.compress);
838
+ }
839
+ }
840
+ catch (error) {
841
+ this.debug(`Failed to mark player data as destroyed: ${guildId}`, error);
842
+ }
843
+ await this.saveDestroyedStatus();
844
+ this.debug(`Marked player as destroyed (won't restore on restart): ${guildId}`);
845
+ this.emit("playerMarkedDestroyed", guildId);
846
+ }
847
+ /**
848
+ * Clear destroyed status for a player
849
+ */
850
+ async clearDestroyed(guildId) {
851
+ await this.clearDestroyedStatus(guildId);
852
+ try {
853
+ const data = await this.provider.load(guildId);
854
+ if (data) {
855
+ data.wasDestroyed = false;
856
+ delete data.destroyedAt;
857
+ await this.provider.save(guildId, data, this.options.compress);
858
+ }
859
+ }
860
+ catch (error) {
861
+ this.debug(`Failed to clear destroyed flag in data: ${guildId}`, error);
862
+ }
863
+ this.debug(`Cleared destroyed status for: ${guildId}`);
864
+ this.emit("playerDestroyedCleared", guildId);
865
+ }
511
866
  /**
512
867
  * Delete a player's saved data
513
868
  */
@@ -516,6 +871,8 @@ class PersistenceManager extends events_1.EventEmitter {
516
871
  return false;
517
872
  try {
518
873
  await this.provider.delete(guildId);
874
+ await this.clearDestroyedStatus(guildId);
875
+ this.restoredPlayers.delete(guildId);
519
876
  this.debug(`Deleted saved data for: ${guildId}`);
520
877
  this.emit("playerDeleted", guildId);
521
878
  return true;
@@ -533,7 +890,54 @@ class PersistenceManager extends events_1.EventEmitter {
533
890
  this.debug("Restore from backup only supported for file provider");
534
891
  return false;
535
892
  }
536
- return await this.provider.restoreBackup(guildId, timestamp);
893
+ const success = await this.provider.restoreBackup(guildId, timestamp);
894
+ if (success) {
895
+ await this.clearDestroyed(guildId);
896
+ }
897
+ return success;
898
+ }
899
+ /**
900
+ * Clean all backups for a specific player
901
+ */
902
+ async cleanBackupsForPlayer(guildId) {
903
+ if (!(this.provider instanceof FileProvider)) {
904
+ this.debug("Backup cleanup only supported for file provider");
905
+ return 0;
906
+ }
907
+ const deleted = await this.provider.cleanAllBackupsForPlayer(guildId);
908
+ this.debug(`Cleaned ${deleted} backups for player: ${guildId}`);
909
+ this.emit("backupsCleaned", guildId, deleted);
910
+ return deleted;
911
+ }
912
+ /**
913
+ * Clean all backups
914
+ */
915
+ async cleanAllBackups() {
916
+ if (!(this.provider instanceof FileProvider)) {
917
+ this.debug("Backup cleanup only supported for file provider");
918
+ return 0;
919
+ }
920
+ const deleted = await this.provider.cleanAllBackups();
921
+ this.debug(`Cleaned all ${deleted} backups`);
922
+ this.emit("allBackupsCleaned", deleted);
923
+ return deleted;
924
+ }
925
+ /**
926
+ * Get backup statistics
927
+ */
928
+ getBackupStats() {
929
+ if (!(this.provider instanceof FileProvider)) {
930
+ this.debug("Backup stats only supported for file provider");
931
+ return null;
932
+ }
933
+ const stats = this.provider.getBackupStats();
934
+ return {
935
+ totalBackups: stats.totalBackups,
936
+ totalSizeMB: stats.totalSize / 1024 / 1024,
937
+ oldestBackup: stats.oldestBackup ? new Date(stats.oldestBackup) : null,
938
+ newestBackup: stats.newestBackup ? new Date(stats.newestBackup) : null,
939
+ backupsByPlayer: Object.fromEntries(stats.backupsByPlayer),
940
+ };
537
941
  }
538
942
  /**
539
943
  * Stop auto-save and clean up
@@ -544,8 +948,21 @@ class PersistenceManager extends events_1.EventEmitter {
544
948
  this.saveInterval = null;
545
949
  }
546
950
  await this.saveAll();
951
+ await this.saveDestroyedStatus();
547
952
  this.debug("Persistence manager shut down");
548
953
  }
954
+ /**
955
+ * Get list of destroyed players
956
+ */
957
+ getDestroyedPlayers() {
958
+ return Array.from(this.destroyedPlayers.values());
959
+ }
960
+ /**
961
+ * Check if auto-restore is enabled
962
+ */
963
+ isAutoRestoreEnabled() {
964
+ return this.options.autoRestoreOnRestart === true;
965
+ }
549
966
  }
550
967
  exports.PersistenceManager = PersistenceManager;
551
968
  //# sourceMappingURL=PersistenceManager.js.map