ziplayer 0.3.3 → 0.3.4

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 (35) hide show
  1. package/AGENTS.md +717 -653
  2. package/README.md +658 -639
  3. package/dist/extensions/BaseExtension.d.ts +10 -1
  4. package/dist/extensions/BaseExtension.d.ts.map +1 -1
  5. package/dist/extensions/BaseExtension.js +27 -1
  6. package/dist/extensions/BaseExtension.js.map +1 -1
  7. package/dist/extensions/index.d.ts.map +1 -1
  8. package/dist/extensions/index.js +24 -6
  9. package/dist/extensions/index.js.map +1 -1
  10. package/dist/plugins/index.d.ts.map +1 -1
  11. package/dist/plugins/index.js +104 -50
  12. package/dist/plugins/index.js.map +1 -1
  13. package/dist/structures/Player.d.ts +74 -43
  14. package/dist/structures/Player.d.ts.map +1 -1
  15. package/dist/structures/Player.js +440 -114
  16. package/dist/structures/Player.js.map +1 -1
  17. package/dist/structures/PlayerManager.d.ts +41 -6
  18. package/dist/structures/PlayerManager.d.ts.map +1 -1
  19. package/dist/structures/PlayerManager.js +94 -125
  20. package/dist/structures/PlayerManager.js.map +1 -1
  21. package/dist/types/extension.d.ts +3 -0
  22. package/dist/types/extension.d.ts.map +1 -1
  23. package/dist/types/index.d.ts +38 -11
  24. package/dist/types/index.d.ts.map +1 -1
  25. package/dist/types/index.js +7 -0
  26. package/dist/types/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/extensions/BaseExtension.ts +31 -1
  29. package/src/extensions/index.ts +30 -7
  30. package/src/plugins/index.ts +136 -53
  31. package/src/structures/Player.ts +2937 -2544
  32. package/src/structures/PlayerManager.ts +916 -955
  33. package/src/structures/Queue.ts +621 -621
  34. package/src/types/extension.ts +3 -0
  35. package/src/types/index.ts +43 -11
@@ -61,8 +61,13 @@ export class ExtensionManager {
61
61
  this.streamCache = new Map();
62
62
  this.pendingSearches = new Map();
63
63
  this.pendingStreams = new Map();
64
- this.extensionContext = Object.freeze({ player, manager });
65
-
64
+ this.extensionContext = Object.freeze({
65
+ player,
66
+ manager,
67
+ playNext: () => (player as any).playNext?.(),
68
+ skip: () => (player as any).skip?.(),
69
+ emit: (event: string, ...args: any[]) => player.emit(event as any, ...args),
70
+ });
66
71
  // Auto-cleanup caches periodically
67
72
  setInterval(() => this.cleanupCaches(), 5 * 60 * 1000);
68
73
  }
@@ -314,7 +319,10 @@ export class ExtensionManager {
314
319
 
315
320
  // Check cache first
316
321
  const cached = this.getCachedStream(track);
317
- if (cached) return cached;
322
+ if (cached) {
323
+ this.debug(`[Cache] Stream hit for: ${track.title}`);
324
+ return cached;
325
+ }
318
326
 
319
327
  // Deduplicate concurrent requests
320
328
  const cacheKey = this.getCacheKey("stream", track.url || track.id || track.title);
@@ -333,16 +341,31 @@ export class ExtensionManager {
333
341
  if (typeof hook !== "function") continue;
334
342
 
335
343
  try {
344
+ this.debug(`Trying extension ${extension.name} for stream: ${track.title}`);
336
345
  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;
346
+
347
+ if (result) {
348
+ const isRemote = (result as StreamInfo).remote;
349
+ const hasStream = !!(result as StreamInfo).stream;
350
+ const hasHandle = !!(result as StreamInfo).handle;
351
+
352
+ this.debug(
353
+ `Extension ${extension.name} returned stream for ${track.title}: remote=${isRemote}, hasStream=${hasStream}, hasHandle=${hasHandle}`,
354
+ );
355
+
356
+ if (hasStream || hasHandle) {
357
+ // Only cache if it's a reusable stream (not remote with handle)
358
+ if (!isRemote) {
359
+ this.setCachedStream(track, result as StreamInfo);
360
+ }
361
+ return result as StreamInfo;
362
+ }
341
363
  }
342
364
  } catch (err) {
343
365
  this.debug(`Extension ${extension.name} provideStream error:`, err);
344
366
  }
345
367
  }
368
+ this.debug(`No extension provided stream for: ${track.title}`);
346
369
  return null;
347
370
  })();
348
371
 
@@ -704,43 +704,123 @@ export class PluginManager {
704
704
  }
705
705
 
706
706
  private async getStreamInternal(track: Track, primary: BasePlugin): Promise<StreamInfo | null> {
707
+ // Reuse existing stream from StreamManager
707
708
  if (this.streamManager) {
708
709
  const existingStream = this.streamManager.getStreamByTrack(track.id || track.title);
710
+
709
711
  if (existingStream) {
710
712
  this.debug(`[Stream] Using existing stream from manager`);
711
- return { stream: existingStream, type: "arbitrary" };
713
+
714
+ return {
715
+ stream: existingStream,
716
+ type: "arbitrary",
717
+ };
712
718
  }
713
719
  }
714
720
 
715
721
  const timeoutMs = this.options.extractorTimeout ?? 50000;
716
722
 
723
+ // Cache
717
724
  const cached = this.getCachedStream(track);
718
- if (cached) return cached;
719
725
 
720
- try {
721
- this.debug(`[Stream] Trying ${primary.name} for track: ${track.title}`);
726
+ if (cached) {
727
+ this.debug(`[Stream] Using cached stream for: ${track.title}`);
728
+ return cached;
729
+ }
730
+
731
+ /**
732
+ * Try resolve stream from plugin
733
+ * Flow:
734
+ * 1. plugin.getStream()
735
+ * 2. validate stream
736
+ * 3. if failed -> plugin.getFallback()
737
+ */
738
+ const tryPlugin = async (
739
+ plugin: BasePlugin,
740
+ isPrimary: boolean = false,
741
+ ): Promise<{ result: StreamInfo | null; similarity: number }> => {
722
742
  const controller = new AbortController();
723
- const result = await withTimeout(
724
- primary.getStream(track, controller.signal),
725
- timeoutMs,
726
- `Primary timeout: ${primary.name}`,
727
- );
728
743
 
729
- if (result?.stream) {
730
- const isValid = await this.validateStreamMatchesTrack(result, track);
731
- if (isValid) {
732
- this.debug(`[Stream] Success via ${primary.name}`);
733
- this.setCachedStream(track, result);
734
- return result;
744
+ let result: StreamInfo | null = null;
745
+
746
+ // =========================================================
747
+ // 1. TRY DIRECT STREAM
748
+ // =========================================================
749
+ if (plugin.getStream) {
750
+ try {
751
+ this.debug(`[Stream] ${plugin.name} trying direct stream`);
752
+
753
+ result = await withTimeout(plugin.getStream(track, controller.signal), timeoutMs, `${plugin.name} getStream timeout`);
754
+
755
+ if (result?.stream) {
756
+ const valid = await this.validateStreamMatchesTrack(result, track);
757
+
758
+ if (valid) {
759
+ this.debug(`[Stream] ${plugin.name} direct stream success`);
760
+
761
+ return {
762
+ result,
763
+ similarity: 1,
764
+ };
765
+ }
766
+
767
+ this.debug(`[Stream] ${plugin.name} returned invalid stream`);
768
+ } else {
769
+ this.debug(`[Stream] ${plugin.name} no direct stream returned`);
770
+ }
771
+ } catch (error) {
772
+ this.debug(`[Stream] ${plugin.name} getStream failed:`, error instanceof Error ? error.message : error);
735
773
  }
736
- this.debug(`[Stream] Stream validation failed - wrong track returned`);
737
774
  }
738
- throw new Error("Primary plugin returned no stream or invalid stream");
739
- } catch (error) {
740
- this.debug(`[Stream] Primary failed: ${primary.name}`, error);
775
+
776
+ // =========================================================
777
+ // 2. TRY FALLBACK SEARCH
778
+ // =========================================================
779
+ if (plugin.getFallback) {
780
+ try {
781
+ this.debug(`[Stream] ${plugin.name} trying fallback resolver`);
782
+
783
+ result = await withTimeout(plugin.getFallback(track, controller.signal), timeoutMs, `${plugin.name} fallback timeout`);
784
+
785
+ if (result?.stream) {
786
+ const similarity = this.calculateTrackSimilarity(track, {
787
+ title: result.metadata?.title || result.metadata?.originalTitle || track.title,
788
+ });
789
+
790
+ this.debug(`[Stream] ${plugin.name} fallback success (${similarity})`);
791
+
792
+ return {
793
+ result,
794
+ similarity,
795
+ };
796
+ }
797
+
798
+ this.debug(`[Stream] ${plugin.name} fallback returned no stream`);
799
+ } catch (error) {
800
+ this.debug(`[Stream] ${plugin.name} fallback failed:`, error instanceof Error ? error.message : error);
801
+ }
802
+ }
803
+
804
+ return {
805
+ result: null,
806
+ similarity: 0,
807
+ };
808
+ };
809
+
810
+ // =========================================================
811
+ // PRIMARY PLUGIN
812
+ // =========================================================
813
+ const primaryResult = await tryPlugin(primary, true);
814
+
815
+ if (primaryResult.result?.stream) {
816
+ this.setCachedStream(track, primaryResult.result);
817
+
818
+ return primaryResult.result;
741
819
  }
742
820
 
743
- // Fallback logic...
821
+ // =========================================================
822
+ // FALLBACK PLUGINS
823
+ // =========================================================
744
824
  const fallbackPlugins = this.getAll()
745
825
  .filter((p) => p !== primary && p.name !== primary.name)
746
826
  .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
@@ -752,61 +832,64 @@ export class PluginManager {
752
832
 
753
833
  this.debug(`[Stream] Trying ${fallbackPlugins.length} fallback plugins`);
754
834
 
755
- const validResults: Array<{ plugin: string; streamInfo: StreamInfo; score: number }> = [];
835
+ const validResults: Array<{
836
+ plugin: string;
837
+ streamInfo: StreamInfo;
838
+ score: number;
839
+ }> = [];
756
840
 
757
841
  let attempt = 0;
842
+
758
843
  for (const plugin of fallbackPlugins) {
759
844
  attempt++;
845
+
760
846
  if (attempt > (this.options.maxFallbackAttempts ?? 3)) {
761
- this.debug(`[Stream] Max attempts reached`);
847
+ this.debug(`[Stream] Max fallback attempts reached`);
762
848
  break;
763
849
  }
764
850
 
765
- try {
766
- this.debug(`[Stream] Fallback ${attempt}: ${plugin.name}`);
767
- const controller = new AbortController();
768
-
769
- let result: StreamInfo | null = null;
851
+ const { result, similarity } = await tryPlugin(plugin);
770
852
 
771
- if (plugin.getStream) {
772
- result = await withTimeout(plugin.getStream(track, controller.signal), timeoutMs, `Timeout: ${plugin.name}`);
773
- }
853
+ if (!result?.stream) {
854
+ continue;
855
+ }
774
856
 
775
- if (!result?.stream && plugin.getFallback) {
776
- result = await withTimeout(plugin.getFallback(track, controller.signal), timeoutMs, `Fallback timeout: ${plugin.name}`);
777
- }
857
+ // Perfect / good match
858
+ if (similarity >= 0.7) {
859
+ this.debug(`[Stream] Success via fallback ${plugin.name} (score: ${similarity})`);
778
860
 
779
- if (result?.stream) {
780
- const similarityScore = this.calculateTrackSimilarity(track, result);
861
+ this.setCachedStream(track, result);
781
862
 
782
- if (similarityScore > 0.7) {
783
- this.debug(`[Stream] Fallback success via ${plugin.name} (score: ${similarityScore})`);
784
- this.setCachedStream(track, result);
785
- return result;
786
- } else {
787
- validResults.push({
788
- plugin: plugin.name,
789
- streamInfo: result,
790
- score: similarityScore,
791
- });
792
- this.debug(`[Stream] Fallback ${plugin.name} returned low similarity (${similarityScore})`);
793
- }
794
- }
795
- } catch (error) {
796
- this.debug(`[Stream] Fallback failed: ${plugin.name}`, error);
863
+ return result;
797
864
  }
865
+
866
+ // Keep low similarity result as backup
867
+ validResults.push({
868
+ plugin: plugin.name,
869
+ streamInfo: result,
870
+ score: similarity,
871
+ });
872
+
873
+ this.debug(`[Stream] ${plugin.name} low similarity match (${similarity})`);
798
874
  }
799
875
 
876
+ // =========================================================
877
+ // BEST AVAILABLE MATCH
878
+ // =========================================================
800
879
  if (validResults.length > 0) {
801
880
  const bestMatch = validResults.sort((a, b) => b.score - a.score)[0];
802
- this.debug(`[Stream] Using best available match from ${bestMatch.plugin} (score: ${bestMatch.score})`);
881
+
882
+ this.debug(`[Stream] Using best available match from ${bestMatch.plugin} (${bestMatch.score})`);
883
+
884
+ this.setCachedStream(track, bestMatch.streamInfo);
885
+
803
886
  return bestMatch.streamInfo;
804
887
  }
805
888
 
806
- this.debug(`[Stream] All plugins failed for track: ${track.title}`);
889
+ this.debug(`[Stream] All plugins failed for: ${track.title}`);
890
+
807
891
  return null;
808
892
  }
809
-
810
893
  async getStream(track: Track): Promise<StreamInfo | null> {
811
894
  if (!track) {
812
895
  this.debug(`[getStream] No track provided`);