ziplayer 0.2.7 → 0.3.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 (63) hide show
  1. package/AI-Guide.md +624 -956
  2. package/README.md +277 -10
  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 +95 -0
  11. package/dist/persistence/PersistenceManager.d.ts.map +1 -0
  12. package/dist/persistence/PersistenceManager.js +975 -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 +74 -8
  17. package/dist/plugins/index.d.ts.map +1 -1
  18. package/dist/plugins/index.js +657 -116
  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/PersistenceManager.d.ts +96 -0
  23. package/dist/structures/PersistenceManager.d.ts.map +1 -0
  24. package/dist/structures/PersistenceManager.js +1008 -0
  25. package/dist/structures/PersistenceManager.js.map +1 -0
  26. package/dist/structures/Player.d.ts +158 -14
  27. package/dist/structures/Player.d.ts.map +1 -1
  28. package/dist/structures/Player.js +1175 -188
  29. package/dist/structures/Player.js.map +1 -1
  30. package/dist/structures/PlayerManager.d.ts +106 -91
  31. package/dist/structures/PlayerManager.d.ts.map +1 -1
  32. package/dist/structures/PlayerManager.js +365 -124
  33. package/dist/structures/PlayerManager.js.map +1 -1
  34. package/dist/structures/Queue.d.ts +136 -31
  35. package/dist/structures/Queue.d.ts.map +1 -1
  36. package/dist/structures/Queue.js +265 -46
  37. package/dist/structures/Queue.js.map +1 -1
  38. package/dist/structures/StreamManager.d.ts +137 -0
  39. package/dist/structures/StreamManager.d.ts.map +1 -0
  40. package/dist/structures/StreamManager.js +420 -0
  41. package/dist/structures/StreamManager.js.map +1 -0
  42. package/dist/types/index.d.ts +181 -8
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/dist/types/index.js.map +1 -1
  45. package/dist/types/persistence.d.ts +77 -0
  46. package/dist/types/persistence.d.ts.map +1 -0
  47. package/dist/types/persistence.js +3 -0
  48. package/dist/types/persistence.js.map +1 -0
  49. package/package.json +3 -2
  50. package/src/extensions/BaseExtension.ts +1 -0
  51. package/src/extensions/index.ts +320 -37
  52. package/src/plugins/BasePlugin.ts +1 -1
  53. package/src/plugins/index.ts +809 -139
  54. package/src/structures/FilterManager.ts +3 -3
  55. package/src/structures/Player.ts +2810 -1693
  56. package/src/structures/PlayerManager.ts +438 -129
  57. package/src/structures/Queue.ts +300 -55
  58. package/src/structures/StreamManager.ts +524 -0
  59. package/src/types/extension.ts +129 -129
  60. package/src/types/fillter.ts +264 -264
  61. package/src/types/index.ts +187 -12
  62. package/src/types/plugin.ts +59 -59
  63. package/tsconfig.json +0 -1
@@ -16,19 +16,57 @@ import { BaseExtension } from "./BaseExtension";
16
16
 
17
17
  export { BaseExtension } from "./BaseExtension";
18
18
 
19
- // Extension factory
19
+ interface ExtensionCacheEntry<T> {
20
+ data: T;
21
+ timestamp: number;
22
+ expiresAt: number;
23
+ }
24
+
25
+ interface ExtensionMetadata {
26
+ name: string;
27
+ priority: number;
28
+ registeredAt: number;
29
+ hasSearch: boolean;
30
+ hasStream: boolean;
31
+ hasBeforePlay: boolean;
32
+ hasAfterPlay: boolean;
33
+ }
34
+
20
35
  export class ExtensionManager {
21
36
  private extensions: Map<string, BaseExtension>;
37
+ private extensionMetadata: Map<string, ExtensionMetadata>;
22
38
  private player: Player;
23
39
  private manager: PlayerManager;
24
40
  private extensionContext: ExtensionContext;
25
41
 
42
+ // Caches for different operations
43
+ private searchCache: Map<string, ExtensionCacheEntry<SearchResult>>;
44
+ private streamCache: Map<string, ExtensionCacheEntry<StreamInfo>>;
45
+
46
+ // Cache TTLs
47
+ private readonly SEARCH_CACHE_TTL = 60 * 1000; // 1 minute
48
+ private readonly STREAM_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
49
+ private readonly MAX_CACHE_SIZE = 100;
50
+
51
+ // Pending requests for deduplication
52
+ private pendingSearches: Map<string, Promise<SearchResult | null>>;
53
+ private pendingStreams: Map<string, Promise<StreamInfo | null>>;
54
+
26
55
  constructor(player: Player, manager: PlayerManager) {
27
56
  this.player = player;
28
57
  this.manager = manager;
29
58
  this.extensions = new Map();
59
+ this.extensionMetadata = new Map();
60
+ this.searchCache = new Map();
61
+ this.streamCache = new Map();
62
+ this.pendingSearches = new Map();
63
+ this.pendingStreams = new Map();
30
64
  this.extensionContext = Object.freeze({ player, manager });
65
+
66
+ // Auto-cleanup caches periodically
67
+ setInterval(() => this.cleanupCaches(), 5 * 60 * 1000);
31
68
  }
69
+
32
70
  debug(message?: any, ...optionalParams: any[]): void {
33
71
  if (this.manager.debugEnabled) {
34
72
  this.manager.emit("debug", `[ExtensionManager] ${message}`, ...optionalParams);
@@ -37,29 +75,55 @@ export class ExtensionManager {
37
75
 
38
76
  register(extension: BaseExtension): void {
39
77
  if (this.extensions.has(extension.name)) {
78
+ this.debug(`Extension ${extension.name} already registered, skipping`);
40
79
  return;
41
80
  }
81
+
42
82
  if (!extension.player) {
43
83
  extension.player = this.player;
44
84
  }
85
+
86
+ // Set default priority if not set
87
+ extension.priority ??= 0;
88
+
89
+ // Store metadata for optimization
90
+ const metadata: ExtensionMetadata = {
91
+ name: extension.name,
92
+ priority: extension.priority,
93
+ registeredAt: Date.now(),
94
+ hasSearch: typeof (extension as any).provideSearch === "function",
95
+ hasStream: typeof (extension as any).provideStream === "function",
96
+ hasBeforePlay: typeof (extension as any).beforePlay === "function",
97
+ hasAfterPlay: typeof (extension as any).afterPlay === "function",
98
+ };
99
+
45
100
  this.extensions.set(extension.name, extension);
101
+ this.extensionMetadata.set(extension.name, metadata);
102
+ this.invokeExtensionLifecycle(extension, "onRegister");
103
+ this.debug(`Registered extension: ${extension.name} (priority: ${extension.priority})`);
46
104
  }
47
105
 
48
106
  unregister(extension: BaseExtension): boolean {
49
107
  const name = extension.name;
50
108
  const result = this.extensions.delete(name);
51
109
  if (result) {
110
+ this.extensionMetadata.delete(name);
52
111
  this.invokeExtensionLifecycle(extension, "onDestroy");
112
+ this.debug(`Unregistered extension: ${name}`);
53
113
  }
54
114
  return result;
55
115
  }
56
116
 
57
117
  destroy(): void {
58
- this.debug(`[ExtensionManager] destroying all extensions`);
118
+ this.debug(`Destroying all extensions`);
59
119
  for (const extension of this.extensions.values()) {
60
120
  this.unregister(extension);
61
121
  }
62
122
  this.extensions.clear();
123
+ this.extensionMetadata.clear();
124
+ this.clearAllCaches();
125
+ this.pendingSearches.clear();
126
+ this.pendingStreams.clear();
63
127
  }
64
128
 
65
129
  get(name: string): BaseExtension | undefined {
@@ -67,15 +131,41 @@ export class ExtensionManager {
67
131
  }
68
132
 
69
133
  getAll(): BaseExtension[] {
70
- return Array.from(this.extensions.values());
134
+ // Sort by priority (higher first)
135
+ return Array.from(this.extensions.values()).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
136
+ }
137
+
138
+ getMetadata(name: string): ExtensionMetadata | undefined {
139
+ return this.extensionMetadata.get(name);
71
140
  }
72
141
 
73
- findExtension(alas: any): BaseExtension | undefined {
74
- return this.getAll().find((extension) => extension.active(alas));
142
+ getAllMetadata(): ExtensionMetadata[] {
143
+ return Array.from(this.extensionMetadata.values()).sort((a, b) => b.priority - a.priority);
144
+ }
145
+
146
+ findExtension(query: unknown): BaseExtension | undefined {
147
+ return this.getAll().find((extension) => extension.active?.(query) ?? false);
148
+ }
149
+
150
+ findExtensionsByCapability(capability: "search" | "stream" | "beforePlay" | "afterPlay"): BaseExtension[] {
151
+ const capabilityMap = {
152
+ search: "hasSearch",
153
+ stream: "hasStream",
154
+ beforePlay: "hasBeforePlay",
155
+ afterPlay: "hasAfterPlay",
156
+ };
157
+
158
+ const metaKey = capabilityMap[capability];
159
+ return this.getAll().filter((ext) => {
160
+ const meta = this.extensionMetadata.get(ext.name);
161
+ return meta?.[metaKey as keyof ExtensionMetadata] ?? false;
162
+ });
75
163
  }
76
164
 
77
165
  clear(): void {
78
166
  this.extensions.clear();
167
+ this.extensionMetadata.clear();
168
+ this.clearAllCaches();
79
169
  }
80
170
 
81
171
  private invokeExtensionLifecycle(extension: BaseExtension | undefined, hook: "onRegister" | "onDestroy"): void {
@@ -85,60 +175,204 @@ export class ExtensionManager {
85
175
  try {
86
176
  const result = fn.call(extension, this.extensionContext);
87
177
  if (result && typeof (result as Promise<unknown>).then === "function") {
88
- (result as Promise<unknown>).catch((err) => this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err));
178
+ (result as Promise<unknown>).catch((err) => this.debug(`Extension ${extension.name} ${hook} error:`, err));
89
179
  }
90
180
  } catch (err) {
91
- this.debug(`[Player] Extension ${extension.name} ${hook} error:`, err);
181
+ this.debug(`Extension ${extension.name} ${hook} error:`, err);
182
+ }
183
+ }
184
+
185
+ private getCacheKey(prefix: string, ...parts: string[]): string {
186
+ return `${prefix}:${parts.join(":")}`;
187
+ }
188
+
189
+ private cleanupCaches(): void {
190
+ const now = Date.now();
191
+
192
+ // Clean search cache
193
+ for (const [key, entry] of this.searchCache) {
194
+ if (now >= entry.expiresAt) {
195
+ this.searchCache.delete(key);
196
+ }
197
+ }
198
+
199
+ // Clean stream cache
200
+ for (const [key, entry] of this.streamCache) {
201
+ if (now >= entry.expiresAt) {
202
+ this.streamCache.delete(key);
203
+ }
204
+ }
205
+
206
+ this.debug(`Cache cleanup completed - Search: ${this.searchCache.size}, Stream: ${this.streamCache.size}`);
207
+ }
208
+
209
+ private clearAllCaches(): void {
210
+ this.searchCache.clear();
211
+ this.streamCache.clear();
212
+ this.debug("All caches cleared");
213
+ }
214
+
215
+ private getCachedSearch(query: string): SearchResult | null {
216
+ const key = this.getCacheKey("search", query.toLowerCase().trim());
217
+ const cached = this.searchCache.get(key);
218
+ if (cached && Date.now() < cached.expiresAt) {
219
+ this.debug(`[Cache] Search hit for: ${query}`);
220
+ return cached.data;
221
+ }
222
+ return null;
223
+ }
224
+
225
+ private setCachedSearch(query: string, result: SearchResult): void {
226
+ if (this.searchCache.size >= this.MAX_CACHE_SIZE) {
227
+ // Remove oldest entries (LRU approximation)
228
+ const oldest = Array.from(this.searchCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
229
+ if (oldest) this.searchCache.delete(oldest[0]);
230
+ }
231
+
232
+ const key = this.getCacheKey("search", query.toLowerCase().trim());
233
+ this.searchCache.set(key, {
234
+ data: result,
235
+ timestamp: Date.now(),
236
+ expiresAt: Date.now() + this.SEARCH_CACHE_TTL,
237
+ });
238
+ this.debug(`[Cache] Search stored for: ${query}`);
239
+ }
240
+
241
+ private getCachedStream(track: Track): StreamInfo | null {
242
+ const key = this.getCacheKey("stream", track.url || track.id || track.title);
243
+ const cached = this.streamCache.get(key);
244
+ if (cached && Date.now() < cached.expiresAt) {
245
+ this.debug(`[Cache] Stream hit for: ${track.title}`);
246
+ return cached.data;
247
+ }
248
+ return null;
249
+ }
250
+
251
+ private setCachedStream(track: Track, stream: StreamInfo): void {
252
+ if (this.streamCache.size >= this.MAX_CACHE_SIZE) {
253
+ const oldest = Array.from(this.streamCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)[0];
254
+ if (oldest) this.streamCache.delete(oldest[0]);
92
255
  }
256
+
257
+ const key = this.getCacheKey("stream", track.url || track.id || track.title);
258
+ this.streamCache.set(key, {
259
+ data: stream,
260
+ timestamp: Date.now(),
261
+ expiresAt: Date.now() + this.STREAM_CACHE_TTL,
262
+ });
263
+ this.debug(`[Cache] Stream stored for: ${track.title}`);
93
264
  }
94
265
 
95
266
  async provideSearch(query: string, requestedBy: string): Promise<SearchResult | null> {
267
+ if (!query) return null;
268
+
269
+ // Check cache first
270
+ const cached = this.getCachedSearch(query);
271
+ if (cached) return cached;
272
+
273
+ // Deduplicate concurrent requests
274
+ const cacheKey = this.getCacheKey("search", query.toLowerCase().trim());
275
+ if (this.pendingSearches.has(cacheKey)) {
276
+ this.debug(`[Dedupe] Waiting for pending search: ${query}`);
277
+ return this.pendingSearches.get(cacheKey)!;
278
+ }
279
+
96
280
  const request: ExtensionSearchRequest = { query, requestedBy };
97
- for (const extension of this.getAll()) {
98
- const hook = (extension as any).provideSearch;
99
- if (typeof hook !== "function") continue;
100
- try {
101
- const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
102
- if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
103
- this.debug(`[Player] Extension ${extension.name} handled search for query: ${query}`);
104
- return result as SearchResult;
281
+ const searchPromise = (async () => {
282
+ // Only query extensions that have provideSearch capability
283
+ const searchExtensions = this.findExtensionsByCapability("search");
284
+
285
+ for (const extension of searchExtensions) {
286
+ const hook = (extension as any).provideSearch;
287
+ if (typeof hook !== "function") continue;
288
+
289
+ try {
290
+ const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
291
+ if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
292
+ this.debug(`Extension ${extension.name} handled search for: ${query}`);
293
+ this.setCachedSearch(query, result as SearchResult);
294
+ return result as SearchResult;
295
+ }
296
+ } catch (err) {
297
+ this.debug(`Extension ${extension.name} provideSearch error:`, err);
105
298
  }
106
- } catch (err) {
107
- this.debug(`[Player] Extension ${extension.name} provideSearch error:`, err);
108
299
  }
300
+ return null;
301
+ })();
302
+
303
+ this.pendingSearches.set(cacheKey, searchPromise);
304
+
305
+ try {
306
+ return await searchPromise;
307
+ } finally {
308
+ this.pendingSearches.delete(cacheKey);
109
309
  }
110
- return null;
111
310
  }
112
311
 
113
312
  async provideStream(track: Track): Promise<StreamInfo | null> {
313
+ if (!track) return null;
314
+
315
+ // Check cache first
316
+ const cached = this.getCachedStream(track);
317
+ if (cached) return cached;
318
+
319
+ // Deduplicate concurrent requests
320
+ const cacheKey = this.getCacheKey("stream", track.url || track.id || track.title);
321
+ if (this.pendingStreams.has(cacheKey)) {
322
+ this.debug(`[Dedupe] Waiting for pending stream: ${track.title}`);
323
+ return this.pendingStreams.get(cacheKey)!;
324
+ }
325
+
114
326
  const request: ExtensionStreamRequest = { track };
115
- for (const extension of this.getAll()) {
116
- const hook = (extension as any).provideStream;
117
- if (typeof hook !== "function") continue;
118
- try {
119
- const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
120
- if (result && (result as StreamInfo).stream) {
121
- this.debug(`[Player] Extension ${extension.name} provided stream for track: ${track.title}`);
122
- return result as StreamInfo;
327
+ const streamPromise = (async () => {
328
+ // Only query extensions that have provideStream capability
329
+ const streamExtensions = this.findExtensionsByCapability("stream");
330
+
331
+ for (const extension of streamExtensions) {
332
+ const hook = (extension as any).provideStream;
333
+ if (typeof hook !== "function") continue;
334
+
335
+ try {
336
+ const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
337
+ if (result && (result as StreamInfo).stream) {
338
+ this.debug(`Extension ${extension.name} provided stream for: ${track.title}`);
339
+ this.setCachedStream(track, result as StreamInfo);
340
+ return result as StreamInfo;
341
+ }
342
+ } catch (err) {
343
+ this.debug(`Extension ${extension.name} provideStream error:`, err);
123
344
  }
124
- } catch (err) {
125
- this.debug(`[Player] Extension ${extension.name} provideStream error:`, err);
126
345
  }
346
+ return null;
347
+ })();
348
+
349
+ this.pendingStreams.set(cacheKey, streamPromise);
350
+
351
+ try {
352
+ return await streamPromise;
353
+ } finally {
354
+ this.pendingStreams.delete(cacheKey);
127
355
  }
128
- return null;
129
356
  }
130
357
 
131
- async BeforePlayHooks(
358
+ async beforePlayHooks(
132
359
  initial: ExtensionPlayRequest,
133
360
  ): Promise<{ request: ExtensionPlayRequest; response: ExtensionPlayResponse }> {
134
361
  const request: ExtensionPlayRequest = { ...initial };
135
362
  const response: ExtensionPlayResponse = {};
136
- for (const extension of this.getAll()) {
363
+
364
+ // Only query extensions that have beforePlay capability
365
+ const beforePlayExtensions = this.findExtensionsByCapability("beforePlay");
366
+
367
+ for (const extension of beforePlayExtensions) {
137
368
  const hook = (extension as any).beforePlay;
138
369
  if (typeof hook !== "function") continue;
370
+
139
371
  try {
140
372
  const result = await Promise.resolve(hook.call(extension, this.extensionContext, request));
141
373
  if (!result) continue;
374
+
375
+ // Merge results
142
376
  if (result.query !== undefined) {
143
377
  request.query = result.query;
144
378
  response.query = result.query;
@@ -164,27 +398,76 @@ export class ExtensionManager {
164
398
  if (result.handled) break;
165
399
  }
166
400
  } catch (err) {
167
- this.debug(`[Player] Extension ${extension.name} beforePlay error:`, err);
401
+ this.debug(`Extension ${extension.name} beforePlay error:`, err);
168
402
  }
169
403
  }
404
+
170
405
  return { request, response };
171
406
  }
172
407
 
173
- async AfterPlayHooks(payload: ExtensionAfterPlayPayload): Promise<void> {
174
- if (this.getAll().length === 0) return;
408
+ async afterPlayHooks(payload: ExtensionAfterPlayPayload): Promise<void> {
409
+ const afterPlayExtensions = this.findExtensionsByCapability("afterPlay");
410
+ if (afterPlayExtensions.length === 0) return;
411
+
412
+ // Create immutable payload
175
413
  const safeTracks = payload.tracks ? [...payload.tracks] : undefined;
176
414
  if (safeTracks) {
177
415
  Object.freeze(safeTracks);
178
416
  }
179
417
  const immutablePayload = Object.freeze({ ...payload, tracks: safeTracks });
180
- for (const extension of this.getAll()) {
418
+
419
+ // Execute hooks in parallel for better performance
420
+ const hooks = afterPlayExtensions.map(async (extension) => {
181
421
  const hook = (extension as any).afterPlay;
182
- if (typeof hook !== "function") continue;
422
+ if (typeof hook !== "function") return;
423
+
183
424
  try {
184
425
  await Promise.resolve(hook.call(extension, this.extensionContext, immutablePayload));
185
426
  } catch (err) {
186
- this.debug(`[Player] Extension ${extension.name} afterPlay error:`, err);
427
+ this.debug(`Extension ${extension.name} afterPlay error:`, err);
187
428
  }
429
+ });
430
+
431
+ await Promise.allSettled(hooks);
432
+ }
433
+
434
+ /**
435
+ * Get extension statistics
436
+ */
437
+ getStats(): object {
438
+ const metadata = this.getAllMetadata();
439
+ return {
440
+ totalExtensions: this.extensions.size,
441
+ extensions: metadata.map((m) => ({
442
+ name: m.name,
443
+ priority: m.priority,
444
+ capabilities: {
445
+ search: m.hasSearch,
446
+ stream: m.hasStream,
447
+ beforePlay: m.hasBeforePlay,
448
+ afterPlay: m.hasAfterPlay,
449
+ },
450
+ })),
451
+ cacheStats: {
452
+ searchCacheSize: this.searchCache.size,
453
+ streamCacheSize: this.streamCache.size,
454
+ pendingSearches: this.pendingSearches.size,
455
+ pendingStreams: this.pendingStreams.size,
456
+ },
457
+ };
458
+ }
459
+
460
+ /**
461
+ * Clear specific cache
462
+ */
463
+ clearCache(type?: "search" | "stream"): void {
464
+ if (!type || type === "search") {
465
+ this.searchCache.clear();
466
+ this.debug("Search cache cleared");
467
+ }
468
+ if (!type || type === "stream") {
469
+ this.streamCache.clear();
470
+ this.debug("Stream cache cleared");
188
471
  }
189
472
  }
190
473
  }
@@ -3,7 +3,7 @@ import { SourcePlugin, Track, SearchResult, StreamInfo } from "../types";
3
3
  export abstract class BasePlugin implements SourcePlugin {
4
4
  abstract name: string;
5
5
  abstract version: string;
6
- priority?: number = 0;
6
+ priority?: number = 0; // Higher = run first
7
7
 
8
8
  abstract canHandle(query: string): boolean;
9
9
  abstract search(query: string, requestedBy: string): Promise<SearchResult>;