ziplayer 0.2.1 → 0.2.3-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 (43) hide show
  1. package/dist/structures/FilterManager.d.ts +1 -0
  2. package/dist/structures/FilterManager.d.ts.map +1 -1
  3. package/dist/structures/FilterManager.js +49 -10
  4. package/dist/structures/FilterManager.js.map +1 -1
  5. package/dist/structures/Player.d.ts +15 -2
  6. package/dist/structures/Player.d.ts.map +1 -1
  7. package/dist/structures/Player.js +123 -37
  8. package/dist/structures/Player.js.map +1 -1
  9. package/dist/types/extension.d.ts +114 -0
  10. package/dist/types/extension.d.ts.map +1 -0
  11. package/dist/types/extension.js +3 -0
  12. package/dist/types/extension.js.map +1 -0
  13. package/dist/types/fillter.d.ts +44 -0
  14. package/dist/types/fillter.d.ts.map +1 -0
  15. package/dist/types/fillter.js +226 -0
  16. package/dist/types/fillter.js.map +1 -0
  17. package/dist/types/index.d.ts +14 -209
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/types/index.js +17 -223
  20. package/dist/types/index.js.map +1 -1
  21. package/dist/types/plugin.d.ts +58 -0
  22. package/dist/types/plugin.d.ts.map +1 -0
  23. package/dist/types/plugin.js +21 -0
  24. package/dist/types/plugin.js.map +1 -0
  25. package/package.json +7 -2
  26. package/src/structures/FilterManager.ts +303 -262
  27. package/src/structures/Player.ts +135 -41
  28. package/src/types/extension.ts +129 -0
  29. package/src/types/fillter.ts +264 -0
  30. package/src/types/index.ts +15 -443
  31. package/src/types/plugin.ts +57 -0
  32. package/dist/plugins/SoundCloudPlugin.d.ts +0 -22
  33. package/dist/plugins/SoundCloudPlugin.d.ts.map +0 -1
  34. package/dist/plugins/SoundCloudPlugin.js +0 -171
  35. package/dist/plugins/SoundCloudPlugin.js.map +0 -1
  36. package/dist/plugins/SpotifyPlugin.d.ts +0 -26
  37. package/dist/plugins/SpotifyPlugin.d.ts.map +0 -1
  38. package/dist/plugins/SpotifyPlugin.js +0 -183
  39. package/dist/plugins/SpotifyPlugin.js.map +0 -1
  40. package/dist/plugins/YouTubePlugin.d.ts +0 -25
  41. package/dist/plugins/YouTubePlugin.d.ts.map +0 -1
  42. package/dist/plugins/YouTubePlugin.js +0 -314
  43. package/dist/plugins/YouTubePlugin.js.map +0 -1
@@ -13,7 +13,6 @@ import {
13
13
  StreamType,
14
14
  } from "@discordjs/voice";
15
15
 
16
- import { VoiceChannel } from "discord.js";
17
16
  import { Readable } from "stream";
18
17
  import type { BaseExtension } from "../extensions";
19
18
  import type {
@@ -26,6 +25,7 @@ import type {
26
25
  LoopMode,
27
26
  StreamInfo,
28
27
  SaveOptions,
28
+ VoiceChannel,
29
29
  ExtensionPlayRequest,
30
30
  ExtensionPlayResponse,
31
31
  ExtensionAfterPlayPayload,
@@ -160,6 +160,25 @@ export class Player extends EventEmitter {
160
160
  }
161
161
  }
162
162
 
163
+ /**
164
+ * Destroy current stream to prevent memory leaks
165
+ * @private
166
+ */
167
+ private destroyCurrentStream(): void {
168
+ try {
169
+ // Get the metadata from current resource to find the stream
170
+ if (this.currentResource) {
171
+ const stream = (this.currentResource as any)?.metadata?.stream || (this.currentResource as any)?.stream;
172
+ if (stream && typeof stream.destroy === "function") {
173
+ stream.destroy();
174
+ this.debug(`[Player] Destroyed current stream`);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ this.debug(`[Player] Error destroying current stream:`, error);
179
+ }
180
+ }
181
+
163
182
  //#region Search
164
183
 
165
184
  /**
@@ -335,13 +354,21 @@ export class Player extends EventEmitter {
335
354
  };
336
355
  }
337
356
 
338
- private async generateWillNext(): Promise<void> {
339
- const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
340
- if (!lastTrack) return;
357
+ /**
358
+ * Get related tracks for a given track
359
+ * @param {Track} track Track to find related tracks for
360
+ * @returns {Track[]} Related tracks or empty array
361
+ * @example
362
+ * const related = await player.getRelatedTracks(track);
363
+ * console.log(`Found ${related.length} related tracks`);
364
+ */
365
+ async getRelatedTracks(track: Track): Promise<Track[]> {
366
+ if (!track) return [];
367
+
368
+ const preferred = this.pluginManager.findPlugin(track.url) || this.pluginManager.get(track.source);
341
369
 
342
- // Build list of candidate plugins: preferred first, then others with getRelatedTracks
343
- const preferred = this.pluginManager.findPlugin(lastTrack.url) || this.pluginManager.get(lastTrack.source);
344
370
  const all = this.pluginManager.getAll();
371
+
345
372
  const candidates = [...(preferred ? [preferred] : []), ...all.filter((p) => p !== preferred)].filter(
346
373
  (p) => typeof (p as any).getRelatedTracks === "function",
347
374
  );
@@ -350,7 +377,7 @@ export class Player extends EventEmitter {
350
377
  try {
351
378
  this.debug(`[Player] Trying related from plugin: ${p.name}`);
352
379
  const related = await withTimeout(
353
- (p as any).getRelatedTracks(lastTrack.url, {
380
+ (p as any).getRelatedTracks(track.url, {
354
381
  limit: 10,
355
382
  history: this.queue.previousTracks,
356
383
  }),
@@ -359,20 +386,29 @@ export class Player extends EventEmitter {
359
386
  );
360
387
 
361
388
  if (Array.isArray(related) && related.length > 0) {
362
- const randomchoice = Math.floor(Math.random() * related.length);
363
- const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
364
- this.queue.willNextTrack(nextTrack);
365
- this.queue.relatedTracks(related);
366
- this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title} (via ${p.name})`);
367
- this.emit("willPlay", nextTrack, related);
368
- return; // success
389
+ return related; // success
369
390
  }
370
391
  this.debug(`[Player] ${p.name} returned no related tracks`);
371
392
  } catch (err) {
372
393
  this.debug(`[Player] getRelatedTracks error from ${p.name}:`, err);
394
+ return [];
373
395
  // try next candidate
374
396
  }
375
397
  }
398
+ return [];
399
+ }
400
+
401
+ private async generateWillNext(): Promise<void> {
402
+ const lastTrack = this.queue.previousTracks[this.queue.previousTracks.length - 1] ?? this.queue.currentTrack;
403
+ if (!lastTrack) return;
404
+ const related = await this.getRelatedTracks(lastTrack);
405
+ if (!related || related.length === 0) return;
406
+ const randomchoice = Math.floor(Math.random() * related.length);
407
+ const nextTrack = this.queue.nextTrack ? this.queue.nextTrack : related[randomchoice];
408
+ this.queue.willNextTrack(nextTrack);
409
+ this.queue.relatedTracks(related);
410
+ this.debug(`[Player] Will next track if autoplay: ${nextTrack?.title}]`);
411
+ this.emit("willPlay", nextTrack, related);
376
412
  }
377
413
  //#endregion
378
414
  //#region Play
@@ -393,13 +429,10 @@ export class Player extends EventEmitter {
393
429
  */
394
430
  async play(query: string | Track | SearchResult | null, requestedBy?: string): Promise<boolean> {
395
431
  const debugInfo =
396
- query === null
397
- ? "null"
398
- : typeof query === "string"
399
- ? query
400
- : "tracks" in query
401
- ? `${query.tracks.length} tracks`
402
- : query.title || "unknown";
432
+ query === null ? "null"
433
+ : typeof query === "string" ? query
434
+ : "tracks" in query ? `${query.tracks.length} tracks`
435
+ : query.title || "unknown";
403
436
  this.debug(`[Player] Play called with query: ${debugInfo}`);
404
437
  this.clearLeaveTimeout();
405
438
  let tracksToAdd: Track[] = [];
@@ -567,11 +600,9 @@ export class Player extends EventEmitter {
567
600
  const resource = createAudioResource(stream, {
568
601
  metadata: track,
569
602
  inputType:
570
- streamInfo.type === "webm/opus"
571
- ? StreamType.WebmOpus
572
- : streamInfo.type === "ogg/opus"
573
- ? StreamType.OggOpus
574
- : StreamType.Arbitrary,
603
+ streamInfo.type === "webm/opus" ? StreamType.WebmOpus
604
+ : streamInfo.type === "ogg/opus" ? StreamType.OggOpus
605
+ : StreamType.Arbitrary,
575
606
  inlineVolume: true,
576
607
  });
577
608
 
@@ -583,11 +614,9 @@ export class Player extends EventEmitter {
583
614
  const resource = createAudioResource(streamInfo.stream, {
584
615
  metadata: track,
585
616
  inputType:
586
- streamInfo.type === "webm/opus"
587
- ? StreamType.WebmOpus
588
- : streamInfo.type === "ogg/opus"
589
- ? StreamType.OggOpus
590
- : StreamType.Arbitrary,
617
+ streamInfo.type === "webm/opus" ? StreamType.WebmOpus
618
+ : streamInfo.type === "ogg/opus" ? StreamType.OggOpus
619
+ : StreamType.Arbitrary,
591
620
  inlineVolume: true,
592
621
  });
593
622
  return resource;
@@ -615,6 +644,18 @@ export class Player extends EventEmitter {
615
644
  // Kiểm tra nếu có stream thực sự để tạo AudioResource
616
645
  if (streamInfo && (streamInfo as any).stream) {
617
646
  try {
647
+ // Destroy the old stream and resource before creating a new one
648
+ this.destroyCurrentStream();
649
+ if (this.currentResource) {
650
+ try {
651
+ const oldStream = (this.currentResource as any)._readableState?.stream || (this.currentResource as any).stream;
652
+ if (oldStream && typeof oldStream.destroy === "function") {
653
+ oldStream.destroy();
654
+ }
655
+ } catch {}
656
+ this.currentResource = null;
657
+ }
658
+
618
659
  this.currentResource = await this.createResource(streamInfo, track, 0);
619
660
  if (this.volumeInterval) {
620
661
  clearInterval(this.volumeInterval);
@@ -635,11 +676,9 @@ export class Player extends EventEmitter {
635
676
  const fallbackResource = createAudioResource(streamInfo.stream, {
636
677
  metadata: track,
637
678
  inputType:
638
- streamInfo.type === "webm/opus"
639
- ? StreamType.WebmOpus
640
- : streamInfo.type === "ogg/opus"
641
- ? StreamType.OggOpus
642
- : StreamType.Arbitrary,
679
+ streamInfo.type === "webm/opus" ? StreamType.WebmOpus
680
+ : streamInfo.type === "ogg/opus" ? StreamType.OggOpus
681
+ : StreamType.Arbitrary,
643
682
  inlineVolume: true,
644
683
  });
645
684
 
@@ -735,6 +774,9 @@ export class Player extends EventEmitter {
735
774
  this.audioPlayer.state.status === AudioPlayerStatus.Playing ||
736
775
  this.audioPlayer.state.status === AudioPlayerStatus.Buffering;
737
776
 
777
+ let ttsResource: AudioResource | null = null;
778
+ let ttsStream: any = null;
779
+
738
780
  try {
739
781
  if (!this.connection) throw new Error("No voice connection for TTS");
740
782
  const ttsPlayer = this.ensureTTSPlayer();
@@ -744,10 +786,12 @@ export class Player extends EventEmitter {
744
786
  if (!streamInfo) {
745
787
  throw new Error("No stream available for track: ${track.title}");
746
788
  }
789
+ ttsStream = streamInfo.stream;
747
790
  const resource = await this.createResource(streamInfo as StreamInfo, track);
748
791
  if (!resource) {
749
792
  throw new Error("No resource available for track: ${track.title}");
750
793
  }
794
+ ttsResource = resource;
751
795
  if (resource.volume) {
752
796
  resource.volume.setVolume((this.options?.tts?.volume ?? this?.volume ?? 100) / 100);
753
797
  }
@@ -767,8 +811,15 @@ export class Player extends EventEmitter {
767
811
  // Derive timeoutMs from resource/track duration when available, with a sensible cap
768
812
  const md: any = (resource as any)?.metadata ?? {};
769
813
  const declared =
770
- typeof md.duration === "number" ? md.duration : typeof track?.duration === "number" ? track.duration : undefined;
771
- const declaredMs = declared ? (declared > 1000 ? declared : declared * 1000) : undefined;
814
+ typeof md.duration === "number" ? md.duration
815
+ : typeof track?.duration === "number" ? track.duration
816
+ : undefined;
817
+ const declaredMs =
818
+ declared ?
819
+ declared > 1000 ?
820
+ declared
821
+ : declared * 1000
822
+ : undefined;
772
823
  const cap = this.options?.tts?.Max_Time_TTS ?? 60_000;
773
824
  const idleTimeout = declaredMs ? Math.min(cap, Math.max(1_000, declaredMs + 1_500)) : cap;
774
825
  await entersState(ttsPlayer, AudioPlayerStatus.Idle, idleTimeout).catch(() => null);
@@ -779,6 +830,15 @@ export class Player extends EventEmitter {
779
830
  this.debug("[TTS] error while playing:", err);
780
831
  this.emit("playerError", err as Error);
781
832
  } finally {
833
+ // Clean up TTS stream and resource
834
+ try {
835
+ if (ttsStream && typeof ttsStream.destroy === "function") {
836
+ ttsStream.destroy();
837
+ }
838
+ } catch (error) {
839
+ this.debug("[TTS] Error destroying stream:", error);
840
+ }
841
+
782
842
  if (wasPlaying) {
783
843
  try {
784
844
  this.resume();
@@ -1052,9 +1112,20 @@ export class Player extends EventEmitter {
1052
1112
 
1053
1113
  // Apply filters if any are active
1054
1114
  let finalStream = streamInfo.stream;
1055
- if (this.filter.getActiveFilters().length > 0) {
1115
+
1116
+ if (saveOptions.filter || saveOptions.seek) {
1117
+ try {
1118
+ this.filter.clearAll();
1119
+ this.filter.applyFilters(saveOptions.filter || []);
1120
+ } catch (err) {
1121
+ this.debug(`[Player] Error applying save filters:`, err);
1122
+ }
1123
+
1056
1124
  this.debug(`[Player] Applying filters to save stream: ${this.filter.getFilterString() || "none"}`);
1057
- finalStream = await this.filter.applyFiltersAndSeek(streamInfo.stream);
1125
+ finalStream = await this.filter.applyFiltersAndSeek(streamInfo.stream, saveOptions.seek || 0).catch((err) => {
1126
+ this.debug(`[Player] Error applying filters to save stream:`, err);
1127
+ return streamInfo!.stream; // Fallback to original stream
1128
+ });
1058
1129
  }
1059
1130
 
1060
1131
  // Return the stream directly - caller can pipe it to fs.createWriteStream()
@@ -1338,6 +1409,9 @@ export class Player extends EventEmitter {
1338
1409
  this.leaveTimeout = null;
1339
1410
  }
1340
1411
 
1412
+ // Destroy current stream before stopping audio
1413
+ this.destroyCurrentStream();
1414
+
1341
1415
  this.audioPlayer.stop(true);
1342
1416
 
1343
1417
  if (this.ttsPlayer) {
@@ -1358,6 +1432,13 @@ export class Player extends EventEmitter {
1358
1432
  this.extensionManager.destroy();
1359
1433
  this.isPlaying = false;
1360
1434
  this.isPaused = false;
1435
+
1436
+ // Clear any remaining intervals
1437
+ if (this.volumeInterval) {
1438
+ clearInterval(this.volumeInterval);
1439
+ this.volumeInterval = null;
1440
+ }
1441
+
1361
1442
  this.emit("playerDestroy");
1362
1443
  this.removeAllListeners();
1363
1444
  }
@@ -1409,11 +1490,24 @@ export class Player extends EventEmitter {
1409
1490
  // Create AudioResource with filters and seek to current position
1410
1491
  const resource = await this.createResource(streaminfo, track, currentPosition);
1411
1492
 
1412
- // Stop current playback and start new one
1493
+ // Stop current playback and destroy old resource/stream
1413
1494
  const wasPlaying = this.isPlaying;
1414
1495
  const wasPaused = this.isPaused;
1415
1496
 
1416
1497
  this.audioPlayer.stop();
1498
+
1499
+ // Properly destroy the old resource and stream
1500
+ try {
1501
+ if (this.currentResource) {
1502
+ const oldStream = (this.currentResource as any)._readableState?.stream || (this.currentResource as any).stream;
1503
+ if (oldStream && typeof oldStream.destroy === "function") {
1504
+ oldStream.destroy();
1505
+ }
1506
+ }
1507
+ } catch (error) {
1508
+ this.debug(`[Player] Error destroying old stream in refeshPlayerResource:`, error);
1509
+ }
1510
+
1417
1511
  this.currentResource = resource;
1418
1512
 
1419
1513
  // Subscribe to new resource
@@ -0,0 +1,129 @@
1
+ import type { VoiceConnection } from "@discordjs/voice";
2
+ import type { Player } from "../structures/Player";
3
+ import type { PlayerManager } from "../structures/PlayerManager";
4
+ import type { Track, SearchResult, StreamInfo } from ".";
5
+
6
+ /**
7
+ * Extension interface
8
+ *
9
+ * @example
10
+ * const extension: SourceExtension = {
11
+ * name: "YouTube",
12
+ * version: "1.0.0"
13
+ * };
14
+ */
15
+ export interface SourceExtension {
16
+ name: string;
17
+ version: string;
18
+ connection?: VoiceConnection;
19
+ player: Player | null;
20
+ active(alas: any): boolean | Promise<boolean>;
21
+ onRegister?(context: ExtensionContext): void | Promise<void>;
22
+ onDestroy?(context: ExtensionContext): void | Promise<void>;
23
+ beforePlay?(
24
+ context: ExtensionContext,
25
+ payload: ExtensionPlayRequest,
26
+ ): Promise<ExtensionPlayResponse | void> | ExtensionPlayResponse | void;
27
+ afterPlay?(context: ExtensionContext, payload: ExtensionAfterPlayPayload): Promise<void> | void;
28
+ provideSearch?(
29
+ context: ExtensionContext,
30
+ payload: ExtensionSearchRequest,
31
+ ): Promise<SearchResult | null | undefined> | SearchResult | null | undefined;
32
+ provideStream?(
33
+ context: ExtensionContext,
34
+ payload: ExtensionStreamRequest,
35
+ ): Promise<StreamInfo | null | undefined> | StreamInfo | null | undefined;
36
+ }
37
+
38
+ /**
39
+ * Context for the extension
40
+ *
41
+ * @example
42
+ * const context: ExtensionContext = {
43
+ * player: player,
44
+ * manager: manager
45
+ * };
46
+ */
47
+ export interface ExtensionContext {
48
+ player: Player;
49
+ manager: PlayerManager;
50
+ }
51
+
52
+ /**
53
+ * Request for the extension to play a track
54
+ *
55
+ * @example
56
+ * const request: ExtensionPlayRequest = {
57
+ * query: "Song Name",
58
+ * requestedBy: "user123"
59
+ * };
60
+ */
61
+ export interface ExtensionPlayRequest {
62
+ query: string | Track;
63
+ requestedBy?: string;
64
+ }
65
+
66
+ /**
67
+ * Response for the extension to play a track
68
+ *
69
+ * @example
70
+ * const response: ExtensionPlayResponse = {
71
+ * handled: true,
72
+ * query: "Song Name",
73
+ * requestedBy: "user123"
74
+ * };
75
+ */
76
+ export interface ExtensionPlayResponse {
77
+ handled?: boolean;
78
+ query?: string | Track;
79
+ requestedBy?: string;
80
+ tracks?: Track[];
81
+ isPlaylist?: boolean;
82
+ success?: boolean;
83
+ error?: Error;
84
+ }
85
+
86
+ /**
87
+ * Payload for the extension to play a track
88
+ *
89
+ * @example
90
+ * const payload: ExtensionAfterPlayPayload = {
91
+ * success: true,
92
+ * query: "Song Name",
93
+ * requestedBy: "user123"
94
+ * };
95
+ */
96
+ export interface ExtensionAfterPlayPayload {
97
+ success: boolean;
98
+ query: string | Track;
99
+ requestedBy?: string;
100
+ tracks?: Track[];
101
+ isPlaylist?: boolean;
102
+ error?: Error;
103
+ }
104
+
105
+ /**
106
+ * Request for the extension to stream a track
107
+ *
108
+ * @example
109
+ * const request: ExtensionStreamRequest = {
110
+ * track: track
111
+ * };
112
+ */
113
+ export interface ExtensionStreamRequest {
114
+ track: Track;
115
+ }
116
+
117
+ /**
118
+ * Request for the extension to search for a track
119
+ *
120
+ * @example
121
+ * const request: ExtensionSearchRequest = {
122
+ * query: "Song Name",
123
+ * requestedBy: "user123"
124
+ * };
125
+ */
126
+ export interface ExtensionSearchRequest {
127
+ query: string;
128
+ requestedBy: string;
129
+ }