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.
- package/README.md +17 -6
- package/dist/persistence/PersistenceManager.d.ts +50 -16
- package/dist/persistence/PersistenceManager.d.ts.map +1 -1
- package/dist/persistence/PersistenceManager.js +476 -59
- package/dist/persistence/PersistenceManager.js.map +1 -1
- package/dist/structures/Player.d.ts +1 -0
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +4 -0
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +3 -1
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +49 -31
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +24 -5
- package/dist/types/persistence.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/persistence/PersistenceManager.ts +563 -62
- package/src/structures/Player.ts +6 -0
- package/src/structures/PlayerManager.ts +66 -32
- package/src/types/index.ts +9 -0
- package/src/types/persistence.ts +35 -17
|
@@ -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 =
|
|
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,
|
|
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 =
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|