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.
Files changed (57) hide show
  1. package/AI-Guide.md +607 -0
  2. package/README.md +513 -196
  3. package/dist/extensions/BaseExtension.d.ts +1 -0
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js.map +1 -1
  6. package/dist/extensions/index.d.ts +38 -3
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +259 -41
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/persistence/PersistenceManager.d.ts +61 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +551 -0
  13. package/dist/persistence/PersistenceManager.js.map +1 -0
  14. package/dist/plugins/BasePlugin.js +1 -1
  15. package/dist/plugins/BasePlugin.js.map +1 -1
  16. package/dist/plugins/index.d.ts +19 -4
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +273 -146
  19. package/dist/plugins/index.js.map +1 -1
  20. package/dist/structures/FilterManager.js +3 -3
  21. package/dist/structures/FilterManager.js.map +1 -1
  22. package/dist/structures/Player.d.ts +64 -14
  23. package/dist/structures/Player.d.ts.map +1 -1
  24. package/dist/structures/Player.js +344 -91
  25. package/dist/structures/Player.js.map +1 -1
  26. package/dist/structures/PlayerManager.d.ts +125 -91
  27. package/dist/structures/PlayerManager.d.ts.map +1 -1
  28. package/dist/structures/PlayerManager.js +406 -111
  29. package/dist/structures/PlayerManager.js.map +1 -1
  30. package/dist/structures/Queue.d.ts +136 -31
  31. package/dist/structures/Queue.d.ts.map +1 -1
  32. package/dist/structures/Queue.js +265 -46
  33. package/dist/structures/Queue.js.map +1 -1
  34. package/dist/types/index.d.ts +39 -6
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/index.js +1 -0
  37. package/dist/types/index.js.map +1 -1
  38. package/dist/types/persistence.d.ts +55 -0
  39. package/dist/types/persistence.d.ts.map +1 -0
  40. package/dist/types/persistence.js +3 -0
  41. package/dist/types/persistence.js.map +1 -0
  42. package/package.json +47 -46
  43. package/src/extensions/BaseExtension.ts +36 -35
  44. package/src/extensions/index.ts +473 -190
  45. package/src/index.ts +16 -16
  46. package/src/persistence/PersistenceManager.ts +572 -0
  47. package/src/plugins/BasePlugin.ts +27 -27
  48. package/src/plugins/index.ts +403 -236
  49. package/src/structures/FilterManager.ts +303 -303
  50. package/src/structures/Player.ts +1962 -1689
  51. package/src/structures/PlayerManager.ts +788 -416
  52. package/src/structures/Queue.ts +599 -354
  53. package/src/types/index.ts +406 -373
  54. package/src/types/persistence.ts +65 -0
  55. package/src/types/plugin.ts +1 -1
  56. package/src/utils/timeout.ts +10 -10
  57. 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
+ }