ziplayer 0.3.1 → 0.3.3

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 (41) hide show
  1. package/{AI-Guide.md → AGENTS.md} +36 -7
  2. package/README.md +113 -0
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +3 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/index.js +1 -1
  8. package/dist/plugins/index.js.map +1 -1
  9. package/dist/structures/Player.d.ts +48 -17
  10. package/dist/structures/Player.d.ts.map +1 -1
  11. package/dist/structures/Player.js +154 -377
  12. package/dist/structures/Player.js.map +1 -1
  13. package/dist/structures/PlayerManager.d.ts +35 -6
  14. package/dist/structures/PlayerManager.d.ts.map +1 -1
  15. package/dist/structures/PlayerManager.js +215 -19
  16. package/dist/structures/PlayerManager.js.map +1 -1
  17. package/dist/structures/PreloadManager.d.ts +32 -0
  18. package/dist/structures/PreloadManager.d.ts.map +1 -0
  19. package/dist/structures/PreloadManager.js +230 -0
  20. package/dist/structures/PreloadManager.js.map +1 -0
  21. package/dist/structures/Queue.d.ts +19 -0
  22. package/dist/structures/Queue.d.ts.map +1 -1
  23. package/dist/structures/Queue.js +21 -0
  24. package/dist/structures/Queue.js.map +1 -1
  25. package/dist/structures/StreamManager.d.ts +1 -0
  26. package/dist/structures/StreamManager.d.ts.map +1 -1
  27. package/dist/structures/StreamManager.js +37 -3
  28. package/dist/structures/StreamManager.js.map +1 -1
  29. package/dist/types/index.d.ts +41 -1
  30. package/dist/types/index.d.ts.map +1 -1
  31. package/dist/types/index.js +6 -0
  32. package/dist/types/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/index.ts +1 -0
  35. package/src/plugins/index.ts +1 -1
  36. package/src/structures/Player.ts +174 -440
  37. package/src/structures/PlayerManager.ts +253 -23
  38. package/src/structures/PreloadManager.ts +274 -0
  39. package/src/structures/Queue.ts +22 -0
  40. package/src/structures/StreamManager.ts +41 -4
  41. package/src/types/index.ts +47 -1
@@ -37,6 +37,7 @@ export interface StreamManagerOptions {
37
37
 
38
38
  export class StreamManager extends EventEmitter {
39
39
  private streams = new Map<string, ManagedStream>();
40
+ private suppressPrematureCloseErrors = new Set<string>();
40
41
  private options: Required<StreamManagerOptions>;
41
42
  private cleanupTimer: NodeJS.Timeout | null = null;
42
43
  private metrics = {
@@ -72,6 +73,20 @@ export class StreamManager extends EventEmitter {
72
73
  * Register a new stream
73
74
  */
74
75
  registerStream(stream: Readable, track: Track, metadata: Partial<ManagedStream["metadata"]> = {}): string {
76
+ for (const existing of this.streams.values()) {
77
+ if (existing.stream === stream) {
78
+ existing.lastAccessed = Date.now();
79
+ existing.track = track;
80
+ existing.metadata = {
81
+ ...existing.metadata,
82
+ source: track.source || existing.metadata.source || "unknown",
83
+ ...metadata,
84
+ };
85
+ this.debug(`Stream already managed, reusing ID: ${existing.id}`);
86
+ return existing.id;
87
+ }
88
+ }
89
+
75
90
  const streamId = this.generateStreamId(track);
76
91
 
77
92
  // Check if stream already exists
@@ -81,8 +96,9 @@ export class StreamManager extends EventEmitter {
81
96
  }
82
97
 
83
98
  // Check concurrent limit
84
- if (this.streams.size >= this.options.maxConcurrentStreams) {
85
- this.evictOldestStream();
99
+ while (this.streams.size >= this.options.maxConcurrentStreams) {
100
+ const evicted = this.evictOldestStream();
101
+ if (!evicted) break;
86
102
  }
87
103
 
88
104
  // Configure stream
@@ -141,6 +157,14 @@ export class StreamManager extends EventEmitter {
141
157
  private createStreamListeners(streamId: string): ManagedStream["listeners"] {
142
158
  return {
143
159
  error: (err: Error) => {
160
+ const isPrematureClose = err?.message?.toLowerCase().includes("premature close");
161
+ if (isPrematureClose && this.suppressPrematureCloseErrors.has(streamId)) {
162
+ this.debug(`Ignored expected premature close [${streamId}] during controlled destroy`);
163
+ this.suppressPrematureCloseErrors.delete(streamId);
164
+ this.unregisterStream(streamId, false);
165
+ return;
166
+ }
167
+
144
168
  this.debug(`Stream error [${streamId}]:`, err);
145
169
  if (this.options.enableMetrics) {
146
170
  this.metrics.totalErrors++;
@@ -232,6 +256,7 @@ export class StreamManager extends EventEmitter {
232
256
  unregisterStream(streamId: string, forceDestroy: boolean = true): boolean {
233
257
  const managed = this.streams.get(streamId);
234
258
  if (!managed) {
259
+ this.suppressPrematureCloseErrors.delete(streamId);
235
260
  return false;
236
261
  }
237
262
 
@@ -258,9 +283,11 @@ export class StreamManager extends EventEmitter {
258
283
  // Force destroy if needed
259
284
  if (forceDestroy && !stream.destroyed && typeof stream.destroy === "function") {
260
285
  try {
286
+ this.suppressPrematureCloseErrors.add(streamId);
261
287
  stream.destroy();
262
288
  managed.status = "destroyed";
263
289
  } catch (err) {
290
+ this.suppressPrematureCloseErrors.delete(streamId);
264
291
  this.debug(`Error destroying stream:`, err);
265
292
  }
266
293
  }
@@ -274,6 +301,7 @@ export class StreamManager extends EventEmitter {
274
301
  }
275
302
 
276
303
  this.emit("streamUnregistered", { streamId, track: managed.track, reason: forceDestroy ? "destroyed" : "natural" });
304
+ this.suppressPrematureCloseErrors.delete(streamId);
277
305
 
278
306
  return true;
279
307
  }
@@ -336,7 +364,7 @@ export class StreamManager extends EventEmitter {
336
364
  /**
337
365
  * Evict oldest stream when limit reached
338
366
  */
339
- private evictOldestStream(): void {
367
+ private evictOldestStream(): boolean {
340
368
  // Evict lowest priority streams first
341
369
  const sorted = Array.from(this.streams.values()).sort((a, b) => a.metadata.priority - b.metadata.priority);
342
370
 
@@ -344,9 +372,18 @@ export class StreamManager extends EventEmitter {
344
372
  if (managed.metadata.isPreload && managed.metadata.priority < 5) {
345
373
  this.debug(`Evicting low priority preload stream: ${managed.track.title}`);
346
374
  this.unregisterStream(managed.id, true);
347
- break;
375
+ return true;
348
376
  }
349
377
  }
378
+
379
+ if (sorted.length > 0) {
380
+ const fallback = sorted[0];
381
+ this.debug(`Evicting fallback stream to enforce limit: ${fallback.track.title}`);
382
+ this.unregisterStream(fallback.id, true);
383
+ return true;
384
+ }
385
+
386
+ return false;
350
387
  }
351
388
 
352
389
  /**
@@ -122,6 +122,43 @@ export interface StreamInfo {
122
122
  metadata?: Record<string, any>;
123
123
  }
124
124
 
125
+ /** Passed to each {@link TrackMiddleware} run (before stream resolution). */
126
+ export interface TrackMiddlewareContext {
127
+ player: Player;
128
+ manager: PlayerManager;
129
+ }
130
+
131
+ /**
132
+ * Runs immediately before stream extraction (`Player.getStream`): after enqueue, before extension `provideStream` and plugins.
133
+ * Prefer mutating `track` in place (especially `metadata`). If you return another object, its fields are merged into the original
134
+ * `track` reference so queue/current-track pointers stay valid.
135
+ */
136
+ export type TrackMiddleware = (track: Track, context: TrackMiddlewareContext) => void | Track | Promise<void | Track>;
137
+
138
+ /** Options for {@link PlayerManager.subscribePlaybackMirror}. */
139
+ export interface PlaybackMirrorOptions {
140
+ leaderGuildId: string;
141
+ followerGuildIds: string[];
142
+ /** User id passed to follower `play()` (often the bot application id). */
143
+ mirrorUserId: string;
144
+ /** When true (default), follower `setVolume` tracks the leader. */
145
+ syncVolume?: boolean;
146
+ /**
147
+ * When enabled, follower connections subscribe directly
148
+ * to leader.audioPlayer instead of creating their own streams.
149
+ *
150
+ * Greatly reduces bandwidth/CPU usage.
151
+ *
152
+ * Default: true
153
+ */
154
+ forwardMode?: boolean;
155
+ }
156
+
157
+ export function normalizeTrackMiddleware(input?: TrackMiddleware | TrackMiddleware[]): TrackMiddleware[] {
158
+ if (!input) return [];
159
+ return Array.isArray(input) ? input : [input];
160
+ }
161
+
125
162
  /**
126
163
  * Configuration options for creating a new player instance.
127
164
  *
@@ -228,7 +265,7 @@ export interface PlayerOptions {
228
265
  autoDisableInLowPerformance?: boolean;
229
266
  /**
230
267
  * Target crossfade duration in milliseconds.
231
- * Default: 5000
268
+ * Default: 500
232
269
  */
233
270
  durationMs?: number;
234
271
  };
@@ -302,6 +339,11 @@ export interface PlayerOptions {
302
339
  */
303
340
  limiterCeiling?: number;
304
341
  };
342
+ /**
343
+ * Chain of middleware applied to every track immediately before stream extraction (after queueing).
344
+ * Merged after {@link PlayerManagerOptions.trackMiddleware} from the manager.
345
+ */
346
+ trackMiddleware?: TrackMiddleware | TrackMiddleware[];
305
347
  }
306
348
 
307
349
  export interface PlayerManagerOptions {
@@ -312,6 +354,10 @@ export interface PlayerManagerOptions {
312
354
  cleanupInterval?: number; // Cleanup interval in ms
313
355
  enableSearchCache?: boolean; // Enable search result caching
314
356
  enableStatsCollection?: boolean; // Enable stats collection events
357
+ /**
358
+ * Global track middleware for every {@link Player} created from this manager (before per-player middleware).
359
+ */
360
+ trackMiddleware?: TrackMiddleware | TrackMiddleware[];
315
361
  }
316
362
 
317
363
  /**