ziplayer 0.2.6 → 0.2.7-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI-Guide.md +607 -0
- package/README.md +513 -196
- package/dist/extensions/BaseExtension.d.ts +1 -0
- package/dist/extensions/BaseExtension.d.ts.map +1 -1
- package/dist/extensions/BaseExtension.js.map +1 -1
- package/dist/extensions/index.d.ts +38 -3
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +259 -41
- package/dist/extensions/index.js.map +1 -1
- package/dist/persistence/PersistenceManager.d.ts +61 -0
- package/dist/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/persistence/PersistenceManager.js +551 -0
- package/dist/persistence/PersistenceManager.js.map +1 -0
- package/dist/plugins/BasePlugin.js +1 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +19 -4
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +273 -146
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/FilterManager.js +3 -3
- package/dist/structures/FilterManager.js.map +1 -1
- package/dist/structures/Player.d.ts +64 -14
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +344 -91
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/PlayerManager.d.ts +125 -91
- package/dist/structures/PlayerManager.d.ts.map +1 -1
- package/dist/structures/PlayerManager.js +406 -111
- package/dist/structures/PlayerManager.js.map +1 -1
- package/dist/structures/Queue.d.ts +136 -31
- package/dist/structures/Queue.d.ts.map +1 -1
- package/dist/structures/Queue.js +265 -46
- package/dist/structures/Queue.js.map +1 -1
- package/dist/types/index.d.ts +39 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/persistence.d.ts +55 -0
- package/dist/types/persistence.d.ts.map +1 -0
- package/dist/types/persistence.js +3 -0
- package/dist/types/persistence.js.map +1 -0
- package/package.json +47 -46
- package/src/extensions/BaseExtension.ts +36 -35
- package/src/extensions/index.ts +473 -190
- package/src/index.ts +16 -16
- package/src/persistence/PersistenceManager.ts +572 -0
- package/src/plugins/BasePlugin.ts +27 -27
- package/src/plugins/index.ts +403 -236
- package/src/structures/FilterManager.ts +303 -303
- package/src/structures/Player.ts +1962 -1689
- package/src/structures/PlayerManager.ts +788 -416
- package/src/structures/Queue.ts +599 -354
- package/src/types/index.ts +406 -373
- package/src/types/persistence.ts +65 -0
- package/src/types/plugin.ts +1 -1
- package/src/utils/timeout.ts +10 -10
- package/tsconfig.json +22 -23
package/src/index.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { PlayerManager, getGlobalManager } from "./structures/PlayerManager";
|
|
2
|
-
import type { PlayerManagerOptions } from "./types";
|
|
3
|
-
|
|
4
|
-
export { Player } from "./structures/Player";
|
|
5
|
-
export { Queue } from "./structures/Queue";
|
|
6
|
-
export { PlayerManager } from "./structures/PlayerManager";
|
|
7
|
-
export * from "./types";
|
|
8
|
-
export * from "./plugins";
|
|
9
|
-
export * from "./extensions";
|
|
10
|
-
|
|
11
|
-
// Default export
|
|
12
|
-
export default PlayerManager;
|
|
13
|
-
|
|
14
|
-
// Simple shared-instance accessor
|
|
15
|
-
export const getManager = () => getGlobalManager();
|
|
16
|
-
export const getPlayer = (guildOrId: string) => getManager()?.get(guildOrId);
|
|
1
|
+
import { PlayerManager, getGlobalManager } from "./structures/PlayerManager";
|
|
2
|
+
import type { PlayerManagerOptions } from "./types";
|
|
3
|
+
|
|
4
|
+
export { Player } from "./structures/Player";
|
|
5
|
+
export { Queue } from "./structures/Queue";
|
|
6
|
+
export { PlayerManager } from "./structures/PlayerManager";
|
|
7
|
+
export * from "./types";
|
|
8
|
+
export * from "./plugins";
|
|
9
|
+
export * from "./extensions";
|
|
10
|
+
|
|
11
|
+
// Default export
|
|
12
|
+
export default PlayerManager;
|
|
13
|
+
|
|
14
|
+
// Simple shared-instance accessor
|
|
15
|
+
export const getManager = () => getGlobalManager();
|
|
16
|
+
export const getPlayer = (guildOrId: string) => getManager()?.get(guildOrId);
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as zlib from "zlib";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import type { SerializedPlayer, SerializedQueue, SerializedTrack, PersistenceOptions, PersistenceProvider } from "../types";
|
|
7
|
+
import type { Player } from "../structures/Player";
|
|
8
|
+
import type { PlayerManager } from "../structures/PlayerManager";
|
|
9
|
+
import type { Track } from "../types";
|
|
10
|
+
|
|
11
|
+
const gzip = promisify(zlib.gzip);
|
|
12
|
+
const gunzip = promisify(zlib.gunzip);
|
|
13
|
+
|
|
14
|
+
// File provider implementation
|
|
15
|
+
export class FileProvider implements PersistenceProvider {
|
|
16
|
+
private basePath: string;
|
|
17
|
+
private maxBackups: number;
|
|
18
|
+
|
|
19
|
+
constructor(basePath: string, maxBackups: number = 5) {
|
|
20
|
+
this.basePath = basePath;
|
|
21
|
+
this.maxBackups = maxBackups;
|
|
22
|
+
if (!fs.existsSync(basePath)) {
|
|
23
|
+
fs.mkdirSync(basePath, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private getFilePath(key: string): string {
|
|
28
|
+
return path.join(this.basePath, `${key}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private getBackupPath(key: string, timestamp: number): string {
|
|
32
|
+
return path.join(this.basePath, `${key}_backup_${timestamp}.json`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private cleanOldBackups(key: string): void {
|
|
36
|
+
const backups = fs
|
|
37
|
+
.readdirSync(this.basePath)
|
|
38
|
+
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
39
|
+
.sort()
|
|
40
|
+
.reverse();
|
|
41
|
+
|
|
42
|
+
// Keep only maxBackups most recent
|
|
43
|
+
for (let i = this.maxBackups; i < backups.length; i++) {
|
|
44
|
+
const backupPath = path.join(this.basePath, backups[i]);
|
|
45
|
+
fs.unlinkSync(backupPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async save(key: string, data: any, compress: boolean = false): Promise<void> {
|
|
50
|
+
const filePath = this.getFilePath(key);
|
|
51
|
+
let content = JSON.stringify(data, null, 2);
|
|
52
|
+
|
|
53
|
+
if (compress) {
|
|
54
|
+
const compressed = await gzip(content);
|
|
55
|
+
content = compressed.toString("base64");
|
|
56
|
+
fs.writeFileSync(filePath + ".gz", content);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create backup before overwriting
|
|
61
|
+
if (fs.existsSync(filePath)) {
|
|
62
|
+
const backupPath = this.getBackupPath(key, Date.now());
|
|
63
|
+
fs.copyFileSync(filePath, backupPath);
|
|
64
|
+
this.cleanOldBackups(key);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(filePath, content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async load(key: string): Promise<any> {
|
|
71
|
+
const filePath = this.getFilePath(key);
|
|
72
|
+
const gzPath = filePath + ".gz";
|
|
73
|
+
|
|
74
|
+
if (fs.existsSync(gzPath)) {
|
|
75
|
+
const compressed = fs.readFileSync(gzPath, "utf8");
|
|
76
|
+
const buffer = Buffer.from(compressed, "base64");
|
|
77
|
+
const decompressed = await gunzip(buffer);
|
|
78
|
+
return JSON.parse(decompressed.toString());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(filePath)) {
|
|
82
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
83
|
+
return JSON.parse(content);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async delete(key: string): Promise<void> {
|
|
90
|
+
const filePath = this.getFilePath(key);
|
|
91
|
+
const gzPath = filePath + ".gz";
|
|
92
|
+
|
|
93
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
94
|
+
if (fs.existsSync(gzPath)) fs.unlinkSync(gzPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async list(): Promise<string[]> {
|
|
98
|
+
const files = fs.readdirSync(this.basePath);
|
|
99
|
+
return files.filter((f) => f.endsWith(".json") || f.endsWith(".json.gz")).map((f) => f.replace(/\.json(\.gz)?$/, ""));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async restoreBackup(key: string, backupTimestamp?: number): Promise<boolean> {
|
|
103
|
+
let backupFile: string | null = null;
|
|
104
|
+
|
|
105
|
+
if (backupTimestamp) {
|
|
106
|
+
const specific = this.getBackupPath(key, backupTimestamp);
|
|
107
|
+
if (fs.existsSync(specific)) {
|
|
108
|
+
backupFile = specific;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// Get latest backup
|
|
112
|
+
const backups = fs
|
|
113
|
+
.readdirSync(this.basePath)
|
|
114
|
+
.filter((f) => f.startsWith(key) && f.includes("backup"))
|
|
115
|
+
.sort()
|
|
116
|
+
.reverse();
|
|
117
|
+
if (backups.length > 0) {
|
|
118
|
+
backupFile = path.join(this.basePath, backups[0]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (backupFile && fs.existsSync(backupFile)) {
|
|
123
|
+
const content = fs.readFileSync(backupFile, "utf8");
|
|
124
|
+
const data = JSON.parse(content);
|
|
125
|
+
await this.save(key, data);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Custom provider for database integration
|
|
134
|
+
class CustomProvider implements PersistenceProvider {
|
|
135
|
+
constructor(
|
|
136
|
+
private saveFn: (key: string, data: any) => Promise<void>,
|
|
137
|
+
private loadFn: (key: string) => Promise<any>,
|
|
138
|
+
private deleteFn?: (key: string) => Promise<void>,
|
|
139
|
+
private listFn?: () => Promise<string[]>,
|
|
140
|
+
) {}
|
|
141
|
+
|
|
142
|
+
async save(key: string, data: any): Promise<void> {
|
|
143
|
+
await this.saveFn(key, data);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async load(key: string): Promise<any> {
|
|
147
|
+
return await this.loadFn(key);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async delete(key: string): Promise<void> {
|
|
151
|
+
if (this.deleteFn) {
|
|
152
|
+
await this.deleteFn(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async list(): Promise<string[]> {
|
|
157
|
+
if (this.listFn) {
|
|
158
|
+
return await this.listFn();
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export class PersistenceManager extends EventEmitter {
|
|
165
|
+
private manager: PlayerManager;
|
|
166
|
+
private options: PersistenceOptions;
|
|
167
|
+
private provider: PersistenceProvider;
|
|
168
|
+
private saveInterval: NodeJS.Timeout | null = null;
|
|
169
|
+
private isSaving: boolean = false;
|
|
170
|
+
|
|
171
|
+
constructor(manager: PlayerManager, options: PersistenceOptions) {
|
|
172
|
+
super();
|
|
173
|
+
this.manager = manager;
|
|
174
|
+
// Fix: Don't use spread that causes duplicate 'enabled'
|
|
175
|
+
this.options = {
|
|
176
|
+
enabled: true,
|
|
177
|
+
provider: "file",
|
|
178
|
+
saveInterval: 60000,
|
|
179
|
+
autoLoad: true,
|
|
180
|
+
maxBackups: 5,
|
|
181
|
+
compress: false,
|
|
182
|
+
filePath: "./players_data",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Merge options manually to avoid spread duplication
|
|
186
|
+
if (options.enabled !== undefined) this.options.enabled = options.enabled;
|
|
187
|
+
if (options.provider !== undefined) this.options.provider = options.provider;
|
|
188
|
+
if (options.saveInterval !== undefined) this.options.saveInterval = options.saveInterval;
|
|
189
|
+
if (options.autoLoad !== undefined) this.options.autoLoad = options.autoLoad;
|
|
190
|
+
if (options.maxBackups !== undefined) this.options.maxBackups = options.maxBackups;
|
|
191
|
+
if (options.compress !== undefined) this.options.compress = options.compress;
|
|
192
|
+
if (options.filePath !== undefined) this.options.filePath = options.filePath;
|
|
193
|
+
if (options.redisUrl !== undefined) this.options.redisUrl = options.redisUrl;
|
|
194
|
+
if (options.redisPrefix !== undefined) this.options.redisPrefix = options.redisPrefix;
|
|
195
|
+
if (options.save !== undefined) this.options.save = options.save;
|
|
196
|
+
if (options.load !== undefined) this.options.load = options.load;
|
|
197
|
+
if (options.delete !== undefined) this.options.delete = options.delete;
|
|
198
|
+
if (options.list !== undefined) this.options.list = options.list;
|
|
199
|
+
|
|
200
|
+
this.provider = this.createProvider();
|
|
201
|
+
|
|
202
|
+
if (this.options.enabled) {
|
|
203
|
+
this.startAutoSave();
|
|
204
|
+
|
|
205
|
+
if (this.options.autoLoad) {
|
|
206
|
+
this.loadAll().catch((err) => {
|
|
207
|
+
this.debug("Auto-load error:", err);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private createProvider(): PersistenceProvider {
|
|
214
|
+
switch (this.options.provider) {
|
|
215
|
+
case "file":
|
|
216
|
+
return new FileProvider(this.options.filePath!, this.options.maxBackups);
|
|
217
|
+
case "redis":
|
|
218
|
+
// Implement Redis provider if needed
|
|
219
|
+
throw new Error("Redis provider not implemented yet");
|
|
220
|
+
case "database":
|
|
221
|
+
if (!this.options.save || !this.options.load) {
|
|
222
|
+
throw new Error("Database provider requires save/load functions");
|
|
223
|
+
}
|
|
224
|
+
// Fix: Pass the save and load functions with correct signatures
|
|
225
|
+
return new CustomProvider(
|
|
226
|
+
async (key: string, data: any) => {
|
|
227
|
+
if (this.options.save) {
|
|
228
|
+
// Call with single object argument if that's expected
|
|
229
|
+
const saveFn = this.options.save as any;
|
|
230
|
+
if (saveFn.length === 1) {
|
|
231
|
+
// Save function expects { key, data }
|
|
232
|
+
await saveFn({ key, data });
|
|
233
|
+
} else {
|
|
234
|
+
// Save function expects (key, data)
|
|
235
|
+
await saveFn(key, data);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
async (key: string) => {
|
|
240
|
+
if (this.options.load) {
|
|
241
|
+
const loadFn = this.options.load as any;
|
|
242
|
+
if (loadFn.length === 0) {
|
|
243
|
+
// Load function expects no args, returns all data
|
|
244
|
+
const allData = await loadFn();
|
|
245
|
+
return allData?.get?.(key) || allData?.[key] || null;
|
|
246
|
+
} else {
|
|
247
|
+
// Load function expects key
|
|
248
|
+
return await loadFn(key);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
},
|
|
253
|
+
this.options.delete,
|
|
254
|
+
this.options.list,
|
|
255
|
+
);
|
|
256
|
+
default:
|
|
257
|
+
return new FileProvider(this.options.filePath!, this.options.maxBackups);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private debug(message: any, ...params: any[]): void {
|
|
262
|
+
if (this.manager.debugEnabled) {
|
|
263
|
+
this.manager.emit("debug", `[Persistence] ${message}`, ...params);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private startAutoSave(): void {
|
|
268
|
+
if (this.saveInterval) {
|
|
269
|
+
clearInterval(this.saveInterval);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.saveInterval = setInterval(() => {
|
|
273
|
+
this.saveAll().catch((err) => {
|
|
274
|
+
this.debug("Auto-save error:", err);
|
|
275
|
+
this.emit("error", err);
|
|
276
|
+
});
|
|
277
|
+
}, this.options.saveInterval);
|
|
278
|
+
|
|
279
|
+
this.debug(`Auto-save started (interval: ${this.options.saveInterval}ms)`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private serializeTrack(track: Track): SerializedTrack {
|
|
283
|
+
// Create base object with required fields only (avoid duplication)
|
|
284
|
+
const serialized: SerializedTrack = {
|
|
285
|
+
id: track.id,
|
|
286
|
+
title: track.title,
|
|
287
|
+
url: track.url,
|
|
288
|
+
source: track.source,
|
|
289
|
+
duration: track.duration,
|
|
290
|
+
thumbnail: track.thumbnail,
|
|
291
|
+
requestedBy: track.requestedBy,
|
|
292
|
+
isLive: track.isLive || false,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Add optional fields if they exist on the track
|
|
296
|
+
const trackAny = track as any;
|
|
297
|
+
if (trackAny.author) serialized.author = trackAny.author;
|
|
298
|
+
if (trackAny.artwork) serialized.artwork = trackAny.artwork;
|
|
299
|
+
|
|
300
|
+
// Add any extra metadata (excluding fields we already set)
|
|
301
|
+
const excludedFields = new Set([
|
|
302
|
+
"id",
|
|
303
|
+
"title",
|
|
304
|
+
"url",
|
|
305
|
+
"source",
|
|
306
|
+
"duration",
|
|
307
|
+
"thumbnail",
|
|
308
|
+
"requestedBy",
|
|
309
|
+
"isLive",
|
|
310
|
+
"author",
|
|
311
|
+
"artwork",
|
|
312
|
+
]);
|
|
313
|
+
for (const key of Object.keys(trackAny)) {
|
|
314
|
+
if (!excludedFields.has(key) && trackAny[key] !== undefined) {
|
|
315
|
+
(serialized as any)[key] = trackAny[key];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return serialized;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private serializeQueue(player: Player): SerializedQueue {
|
|
323
|
+
return {
|
|
324
|
+
tracks: player.upcomingTracks.map((t) => this.serializeTrack(t)),
|
|
325
|
+
current: player.currentTrack ? this.serializeTrack(player.currentTrack) : null,
|
|
326
|
+
history: player.previousTracks.map((t) => this.serializeTrack(t)),
|
|
327
|
+
loopMode: player.queue.loop(),
|
|
328
|
+
autoPlay: player.queue.autoPlay(),
|
|
329
|
+
position: player.getTime().current,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private serializePlayer(player: Player): SerializedPlayer {
|
|
334
|
+
// Get filters safely - access through public method
|
|
335
|
+
let filters: string[] = [];
|
|
336
|
+
try {
|
|
337
|
+
const filterString = (player as any).filter?.getFilterString();
|
|
338
|
+
if (filterString) {
|
|
339
|
+
filters = filterString.split(",").filter(Boolean);
|
|
340
|
+
}
|
|
341
|
+
} catch (e) {
|
|
342
|
+
// Filter may not be accessible
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
guildId: player.guildId,
|
|
347
|
+
queue: this.serializeQueue(player),
|
|
348
|
+
volume: player.volume,
|
|
349
|
+
isPlaying: player.isPlaying,
|
|
350
|
+
isPaused: player.isPaused,
|
|
351
|
+
options: player.options,
|
|
352
|
+
filters: filters.length > 0 ? filters : undefined,
|
|
353
|
+
lastUpdate: Date.now(),
|
|
354
|
+
version: "1.0.0",
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private deserializeTrack(data: SerializedTrack): Track {
|
|
359
|
+
// Create base track object
|
|
360
|
+
const track: any = {
|
|
361
|
+
id: data.id,
|
|
362
|
+
title: data.title,
|
|
363
|
+
url: data.url,
|
|
364
|
+
source: data.source,
|
|
365
|
+
duration: data.duration,
|
|
366
|
+
thumbnail: data.thumbnail,
|
|
367
|
+
requestedBy: data.requestedBy,
|
|
368
|
+
isLive: data.isLive || false,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Add optional fields if they exist
|
|
372
|
+
if (data.author) track.author = data.author;
|
|
373
|
+
if (data.artwork) track.artwork = data.artwork;
|
|
374
|
+
|
|
375
|
+
// Add any extra metadata from serialized data
|
|
376
|
+
for (const key of Object.keys(data)) {
|
|
377
|
+
if (
|
|
378
|
+
!["id", "title", "url", "source", "duration", "thumbnail", "requestedBy", "isLive", "author", "artwork"].includes(key)
|
|
379
|
+
) {
|
|
380
|
+
track[key] = (data as any)[key];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return track as Track;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Save a single player
|
|
389
|
+
*/
|
|
390
|
+
async savePlayer(player: Player): Promise<boolean> {
|
|
391
|
+
if (!this.options.enabled) return false;
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
const data = this.serializePlayer(player);
|
|
395
|
+
await this.provider.save(player.guildId, data);
|
|
396
|
+
this.debug(`Saved player: ${player.guildId}`);
|
|
397
|
+
this.emit("playerSaved", player.guildId);
|
|
398
|
+
return true;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
this.debug(`Failed to save player ${player.guildId}:`, error);
|
|
401
|
+
this.emit("error", error);
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Save all players
|
|
408
|
+
*/
|
|
409
|
+
async saveAll(): Promise<Map<string, boolean>> {
|
|
410
|
+
if (!this.options.enabled || this.isSaving) return new Map();
|
|
411
|
+
|
|
412
|
+
this.isSaving = true;
|
|
413
|
+
const results = new Map<string, boolean>();
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const players = this.manager.getAll();
|
|
417
|
+
this.debug(`Saving ${players.length} players...`);
|
|
418
|
+
|
|
419
|
+
// Save in parallel with limit
|
|
420
|
+
const batchSize = 5;
|
|
421
|
+
for (let i = 0; i < players.length; i += batchSize) {
|
|
422
|
+
const batch = players.slice(i, i + batchSize);
|
|
423
|
+
const promises = batch.map((p) => this.savePlayer(p));
|
|
424
|
+
const batchResults = await Promise.all(promises);
|
|
425
|
+
batch.forEach((p, idx) => results.set(p.guildId, batchResults[idx]));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.debug(`Saved ${results.size} players`);
|
|
429
|
+
this.emit("savedAll", results);
|
|
430
|
+
} finally {
|
|
431
|
+
this.isSaving = false;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return results;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Load a single player
|
|
439
|
+
*/
|
|
440
|
+
async loadPlayer(guildId: string, restorePosition: boolean = true): Promise<boolean> {
|
|
441
|
+
if (!this.options.enabled) return false;
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const data = await this.provider.load(guildId);
|
|
445
|
+
if (!data) return false;
|
|
446
|
+
|
|
447
|
+
// Check if player already exists
|
|
448
|
+
let player = this.manager.get(guildId);
|
|
449
|
+
if (!player) {
|
|
450
|
+
player = await this.manager.create(guildId, data.options);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Restore queue
|
|
454
|
+
const queue = data.queue as SerializedQueue;
|
|
455
|
+
|
|
456
|
+
// Clear current queue
|
|
457
|
+
player.queue.clear();
|
|
458
|
+
player.queue.loop(queue.loopMode);
|
|
459
|
+
player.queue.autoPlay(queue.autoPlay);
|
|
460
|
+
|
|
461
|
+
// Restore tracks
|
|
462
|
+
if (queue.tracks.length > 0) {
|
|
463
|
+
const tracks = queue.tracks.map((t: SerializedTrack) => this.deserializeTrack(t));
|
|
464
|
+
player.queue.addMultiple(tracks);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Restore current track if exists
|
|
468
|
+
if (queue.current && player.connection) {
|
|
469
|
+
const currentTrack = this.deserializeTrack(queue.current);
|
|
470
|
+
player.queue.willNextTrack(currentTrack);
|
|
471
|
+
|
|
472
|
+
// Restore playback position if requested
|
|
473
|
+
if (restorePosition && queue.position && queue.position > 0) {
|
|
474
|
+
await player.refreshPlayerResource(true, queue.position);
|
|
475
|
+
} else {
|
|
476
|
+
await player.play(currentTrack);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Restore volume
|
|
481
|
+
player.setVolume(data.volume);
|
|
482
|
+
|
|
483
|
+
// Restore filters - safely access through public method
|
|
484
|
+
if (data.filters && data.filters.length > 0) {
|
|
485
|
+
try {
|
|
486
|
+
const filterManager = (player as any).filter;
|
|
487
|
+
if (filterManager && typeof filterManager.applyFilters === "function") {
|
|
488
|
+
await filterManager.applyFilters(data.filters);
|
|
489
|
+
}
|
|
490
|
+
} catch (e) {
|
|
491
|
+
this.debug(`Failed to restore filters for ${guildId}:`, e);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
this.debug(`Loaded player: ${guildId}`);
|
|
496
|
+
this.emit("playerLoaded", guildId, data);
|
|
497
|
+
return true;
|
|
498
|
+
} catch (error) {
|
|
499
|
+
this.debug(`Failed to load player ${guildId}:`, error);
|
|
500
|
+
this.emit("error", error);
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Load all saved players
|
|
507
|
+
*/
|
|
508
|
+
async loadAll(restorePosition: boolean = true): Promise<Map<string, boolean>> {
|
|
509
|
+
if (!this.options.enabled) return new Map();
|
|
510
|
+
|
|
511
|
+
const results = new Map<string, boolean>();
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const keys = await this.provider.list();
|
|
515
|
+
this.debug(`Found ${keys.length} saved players`);
|
|
516
|
+
|
|
517
|
+
for (const guildId of keys) {
|
|
518
|
+
const success = await this.loadPlayer(guildId, restorePosition);
|
|
519
|
+
results.set(guildId, success);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
this.emit("loadedAll", results);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
this.debug("Failed to load players:", error);
|
|
525
|
+
this.emit("error", error);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return results;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Delete a player's saved data
|
|
533
|
+
*/
|
|
534
|
+
async deletePlayer(guildId: string): Promise<boolean> {
|
|
535
|
+
if (!this.options.enabled) return false;
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
await this.provider.delete(guildId);
|
|
539
|
+
this.debug(`Deleted saved data for: ${guildId}`);
|
|
540
|
+
this.emit("playerDeleted", guildId);
|
|
541
|
+
return true;
|
|
542
|
+
} catch (error) {
|
|
543
|
+
this.debug(`Failed to delete player ${guildId}:`, error);
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Restore from backup
|
|
550
|
+
*/
|
|
551
|
+
async restoreBackup(guildId: string, timestamp?: number): Promise<boolean> {
|
|
552
|
+
if (!(this.provider instanceof FileProvider)) {
|
|
553
|
+
this.debug("Restore from backup only supported for file provider");
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return await (this.provider as FileProvider).restoreBackup(guildId, timestamp);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Stop auto-save and clean up
|
|
562
|
+
*/
|
|
563
|
+
async shutdown(): Promise<void> {
|
|
564
|
+
if (this.saveInterval) {
|
|
565
|
+
clearInterval(this.saveInterval);
|
|
566
|
+
this.saveInterval = null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
await this.saveAll();
|
|
570
|
+
this.debug("Persistence manager shut down");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { SourcePlugin, Track, SearchResult, StreamInfo } from "../types";
|
|
2
|
-
|
|
3
|
-
export abstract class BasePlugin implements SourcePlugin {
|
|
4
|
-
abstract name: string;
|
|
5
|
-
abstract version: string;
|
|
6
|
-
priority?: number = 0;
|
|
7
|
-
|
|
8
|
-
abstract canHandle(query: string): boolean;
|
|
9
|
-
abstract search(query: string, requestedBy: string): Promise<SearchResult>;
|
|
10
|
-
abstract getStream(track: Track, signal?: AbortSignal): Promise<StreamInfo>;
|
|
11
|
-
|
|
12
|
-
getFallback?(track: Track, signal?: AbortSignal): Promise<StreamInfo> {
|
|
13
|
-
throw new Error("getFallback not implemented");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
getRelatedTracks?(trackURL: Track, opts?: { limit?: number; offset?: number; history?: Track[] }): Promise<Track[]> {
|
|
17
|
-
return Promise.resolve([]);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
validate?(url: string): boolean {
|
|
21
|
-
return this.canHandle(url);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
extractPlaylist?(url: string, requestedBy: string): Promise<Track[]> {
|
|
25
|
-
return Promise.resolve([]);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
import { SourcePlugin, Track, SearchResult, StreamInfo } from "../types";
|
|
2
|
+
|
|
3
|
+
export abstract class BasePlugin implements SourcePlugin {
|
|
4
|
+
abstract name: string;
|
|
5
|
+
abstract version: string;
|
|
6
|
+
priority?: number = 0; // Higher = run first
|
|
7
|
+
|
|
8
|
+
abstract canHandle(query: string): boolean;
|
|
9
|
+
abstract search(query: string, requestedBy: string): Promise<SearchResult>;
|
|
10
|
+
abstract getStream(track: Track, signal?: AbortSignal): Promise<StreamInfo>;
|
|
11
|
+
|
|
12
|
+
getFallback?(track: Track, signal?: AbortSignal): Promise<StreamInfo> {
|
|
13
|
+
throw new Error("getFallback not implemented");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getRelatedTracks?(trackURL: Track, opts?: { limit?: number; offset?: number; history?: Track[] }): Promise<Track[]> {
|
|
17
|
+
return Promise.resolve([]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validate?(url: string): boolean {
|
|
21
|
+
return this.canHandle(url);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
extractPlaylist?(url: string, requestedBy: string): Promise<Track[]> {
|
|
25
|
+
return Promise.resolve([]);
|
|
26
|
+
}
|
|
27
|
+
}
|