ziplayer 0.2.5 → 0.2.7-dev.0
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.
- package/AI-Guide.md +956 -0
- package/README.md +259 -228
- package/dist/extensions/index.d.ts +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +5 -5
- package/dist/extensions/index.js.map +1 -1
- package/dist/plugins/BasePlugin.d.ts +4 -3
- package/dist/plugins/BasePlugin.d.ts.map +1 -1
- package/dist/plugins/BasePlugin.js +4 -1
- package/dist/plugins/BasePlugin.js.map +1 -1
- package/dist/plugins/index.d.ts +10 -1
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +195 -40
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts +0 -9
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +60 -94
- package/dist/structures/Player.js.map +1 -1
- package/dist/types/plugin.d.ts +3 -1
- package/dist/types/plugin.d.ts.map +1 -1
- package/dist/types/plugin.js.map +1 -1
- package/package.json +46 -46
- package/src/extensions/BaseExtension.ts +35 -35
- package/src/extensions/index.ts +190 -194
- package/src/index.ts +16 -16
- package/src/plugins/BasePlugin.ts +27 -26
- package/src/plugins/index.ts +288 -109
- package/src/structures/FilterManager.ts +303 -303
- package/src/structures/Player.ts +1704 -1743
- package/src/structures/PlayerManager.ts +416 -416
- package/src/structures/Queue.ts +354 -354
- package/src/types/index.ts +373 -373
- package/src/types/plugin.ts +3 -1
- package/src/utils/timeout.ts +10 -10
- package/tsconfig.json +22 -23
package/src/plugins/index.ts
CHANGED
|
@@ -1,109 +1,288 @@
|
|
|
1
|
-
import { BasePlugin } from "./BasePlugin";
|
|
2
|
-
import { withTimeout } from "../utils/timeout";
|
|
3
|
-
import type { Track, StreamInfo } from "../types";
|
|
4
|
-
import type { PlayerManager } from "../structures/PlayerManager";
|
|
5
|
-
import type { Player } from "../structures/Player";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
1
|
+
import { BasePlugin } from "./BasePlugin";
|
|
2
|
+
import { withTimeout } from "../utils/timeout";
|
|
3
|
+
import type { Track, StreamInfo } from "../types";
|
|
4
|
+
import type { PlayerManager } from "../structures/PlayerManager";
|
|
5
|
+
import type { Player } from "../structures/Player";
|
|
6
|
+
|
|
7
|
+
type PluginManagerOptions = {
|
|
8
|
+
extractorTimeout: number | undefined;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export { BasePlugin } from "./BasePlugin";
|
|
12
|
+
|
|
13
|
+
function levenshtein(a: string, b: string): number {
|
|
14
|
+
const matrix = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i <= a.length; i++) matrix[i][0] = i;
|
|
17
|
+
for (let j = 0; j <= b.length; j++) matrix[0][j] = j;
|
|
18
|
+
|
|
19
|
+
for (let i = 1; i <= a.length; i++) {
|
|
20
|
+
for (let j = 1; j <= b.length; j++) {
|
|
21
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
22
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return matrix[a.length][b.length];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function similarity(a: string, b: string): number {
|
|
30
|
+
if (!a || !b) return 0;
|
|
31
|
+
|
|
32
|
+
const dist = levenshtein(a, b);
|
|
33
|
+
const maxLen = Math.max(a.length, b.length);
|
|
34
|
+
|
|
35
|
+
return 1 - dist / maxLen; // 0 → 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalize(str: string): string {
|
|
39
|
+
return str
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/\(.*?\)|\[.*?\]/g, "") // remove (remix), [lyrics]
|
|
42
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
43
|
+
.replace(/\s+/g, " ")
|
|
44
|
+
.trim();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const MUSIC_KEYWORDS = ["official", "mv", "audio", "lyrics", "remix", "cover", "ft", "feat", "prod", "music video"];
|
|
48
|
+
|
|
49
|
+
const NON_MUSIC_KEYWORDS = ["reaction", "review", "podcast", "interview", "vlog", "live stream", "news", "tiktok"];
|
|
50
|
+
|
|
51
|
+
function detectContentType(title: string): number {
|
|
52
|
+
const t = title.toLowerCase();
|
|
53
|
+
|
|
54
|
+
let score = 0;
|
|
55
|
+
|
|
56
|
+
for (const k of MUSIC_KEYWORDS) {
|
|
57
|
+
if (t.includes(k)) score += 2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const k of NON_MUSIC_KEYWORDS) {
|
|
61
|
+
if (t.includes(k)) score -= 3;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return score;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tokenOverlap(a: string, b: string): number {
|
|
68
|
+
const setA = new Set(a.split(" "));
|
|
69
|
+
const setB = new Set(b.split(" "));
|
|
70
|
+
|
|
71
|
+
let match = 0;
|
|
72
|
+
for (const word of setA) {
|
|
73
|
+
if (setB.has(word)) match++;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return match / Math.max(setA.size, setB.size);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scoreTrack(base: Track, candidate: Track): number {
|
|
80
|
+
const titleA = normalize(base.title);
|
|
81
|
+
const titleB = normalize(candidate.title);
|
|
82
|
+
|
|
83
|
+
let score = 0;
|
|
84
|
+
|
|
85
|
+
// ===== FUZZY =====
|
|
86
|
+
const sim = similarity(titleA, titleB); // 0 → 1
|
|
87
|
+
score += sim * 50;
|
|
88
|
+
|
|
89
|
+
// ===== TOKEN MATCH =====
|
|
90
|
+
score += tokenOverlap(titleA, titleB) * 30;
|
|
91
|
+
|
|
92
|
+
// ===== CONTENT TYPE =====
|
|
93
|
+
score += detectContentType(candidate.title);
|
|
94
|
+
|
|
95
|
+
return score;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Plugin factory
|
|
99
|
+
export class PluginManager {
|
|
100
|
+
private options: PluginManagerOptions;
|
|
101
|
+
private player: Player;
|
|
102
|
+
private manager: PlayerManager;
|
|
103
|
+
private plugins: Map<string, BasePlugin> = new Map();
|
|
104
|
+
|
|
105
|
+
constructor(player: Player, manager: PlayerManager, options: PluginManagerOptions) {
|
|
106
|
+
this.player = player;
|
|
107
|
+
this.manager = manager;
|
|
108
|
+
this.options = options;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
debug(message?: any, ...optionalParams: any[]): void {
|
|
112
|
+
if (this.manager.debugEnabled) {
|
|
113
|
+
this.manager.emit("debug", `[Plugins] ${message}`, ...optionalParams);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
register(plugin: BasePlugin): void {
|
|
118
|
+
this.plugins.set(plugin.name, plugin);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
unregister(name: string): boolean {
|
|
122
|
+
return this.plugins.delete(name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get(name: string): BasePlugin | undefined {
|
|
126
|
+
return this.plugins.get(name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
getAll(): BasePlugin[] {
|
|
130
|
+
return Array.from(this.plugins.values());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
findPlugin(query: string): BasePlugin | undefined {
|
|
134
|
+
return this.getAll().find((plugin) => plugin.canHandle(query));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
clear(): void {
|
|
138
|
+
this.plugins.clear();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getStream(track: Track): Promise<StreamInfo | null> {
|
|
142
|
+
const timeoutMs = this.options.extractorTimeout ?? 50000;
|
|
143
|
+
const primary = this.get(track.source) || this.findPlugin(track.url);
|
|
144
|
+
if (!primary) {
|
|
145
|
+
this.debug(`No plugin found for track: ${track.title}`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const result = await withTimeout(primary.getStream(track, controller.signal), timeoutMs, "Primary timeout");
|
|
151
|
+
if (result?.stream) return result;
|
|
152
|
+
throw new Error("Primary failed");
|
|
153
|
+
} catch {
|
|
154
|
+
this.debug("Primary failed → fallback parallel");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ===== FALLBACK PARALLEL =====
|
|
158
|
+
const plugins = this.getAll()
|
|
159
|
+
.filter((p) => p !== primary)
|
|
160
|
+
.map((p) => {
|
|
161
|
+
p.priority ??= 0;
|
|
162
|
+
return p;
|
|
163
|
+
})
|
|
164
|
+
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
165
|
+
|
|
166
|
+
// group by priority
|
|
167
|
+
const groups = new Map<number, BasePlugin[]>();
|
|
168
|
+
for (const p of plugins) {
|
|
169
|
+
if (!groups.has(p.priority ?? 0)) groups.set(p.priority ?? 0, []);
|
|
170
|
+
groups.get(p.priority ?? 0)!.push(p);
|
|
171
|
+
}
|
|
172
|
+
for (const [priority, group] of groups) {
|
|
173
|
+
this.debug(`Running group priority=${priority}`);
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
try {
|
|
176
|
+
const promises = group.map((p) => {
|
|
177
|
+
const run = async () => {
|
|
178
|
+
try {
|
|
179
|
+
let result: StreamInfo | null = null;
|
|
180
|
+
|
|
181
|
+
if (p.getStream) {
|
|
182
|
+
try {
|
|
183
|
+
result = await withTimeout(p.getStream(track, controller.signal), timeoutMs, `Timeout ${p.name}`);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
// getStream thất bại → log rồi thử getFallback
|
|
186
|
+
this.debug(`getStream failed for ${p.name}, trying getFallback`, err);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result?.stream) {
|
|
190
|
+
this.debug(`Success via ${p.name}`);
|
|
191
|
+
controller.abort();
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (p.getFallback) {
|
|
197
|
+
result = await withTimeout(p.getFallback(track, controller.signal), timeoutMs, `Fallback timeout ${p.name}`);
|
|
198
|
+
if (result?.stream) {
|
|
199
|
+
this.debug(`Fallback via ${p.name}`);
|
|
200
|
+
controller.abort();
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error("No stream");
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (controller.signal.aborted) throw new Error("Aborted");
|
|
208
|
+
this.debug(`Failed ${p.name}`, err);
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
return run();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const result = await Promise.any(promises);
|
|
216
|
+
if (result?.stream) return result;
|
|
217
|
+
} catch {
|
|
218
|
+
this.debug(`Priority group ${priority} failed`);
|
|
219
|
+
controller.abort();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error(`All plugins failed for track: ${track.title}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get related tracks for a given track
|
|
228
|
+
* @param {Track} track Track to find related tracks for
|
|
229
|
+
* @returns {Track[]} Related tracks or empty array
|
|
230
|
+
* @example
|
|
231
|
+
* const related = await player.getRelatedTracks(track);
|
|
232
|
+
* console.log(`Found ${related.length} related tracks`);
|
|
233
|
+
*/
|
|
234
|
+
async getRelatedTracks(track: Track): Promise<Track[]> {
|
|
235
|
+
if (!track) return [];
|
|
236
|
+
|
|
237
|
+
const timeoutMs = this.options.extractorTimeout ?? 15000;
|
|
238
|
+
const limit = 20;
|
|
239
|
+
|
|
240
|
+
const allPlugins = this.getAll()
|
|
241
|
+
.filter((p) => typeof p.getRelatedTracks === "function")
|
|
242
|
+
.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
243
|
+
|
|
244
|
+
const history = this.player.queue.previousTracks;
|
|
245
|
+
|
|
246
|
+
const results: Track[] = [];
|
|
247
|
+
|
|
248
|
+
// ===== TRY ALL PLUGINS (NOT JUST FIRST SUCCESS) =====
|
|
249
|
+
await Promise.allSettled(
|
|
250
|
+
allPlugins.map(async (p) => {
|
|
251
|
+
try {
|
|
252
|
+
this.debug(`[RelatedTracks] Querying ${p.name}`);
|
|
253
|
+
|
|
254
|
+
const related = await withTimeout(p.getRelatedTracks!(track, { limit, history }), timeoutMs, `Timeout ${p.name}`);
|
|
255
|
+
|
|
256
|
+
if (Array.isArray(related)) {
|
|
257
|
+
results.push(...related);
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
this.debug(`[RelatedTracks] ${p.name} failed`, err);
|
|
261
|
+
}
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (results.length === 0) {
|
|
266
|
+
this.debug(`[RelatedTracks] No results`);
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ===== DEDUPE =====
|
|
271
|
+
const unique = new Map<string, Track>();
|
|
272
|
+
for (const t of results) {
|
|
273
|
+
if (!unique.has(t.url)) {
|
|
274
|
+
unique.set(t.url, t);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ===== SCORE + SORT =====
|
|
279
|
+
const ranked = Array.from(unique.values())
|
|
280
|
+
.map((t) => ({ track: t, score: scoreTrack(track, t) }))
|
|
281
|
+
.sort((a, b) => b.score - a.score)
|
|
282
|
+
.slice(0, limit)
|
|
283
|
+
.map((x) => x.track);
|
|
284
|
+
|
|
285
|
+
this.debug(`[RelatedTracks] Final ${ranked.length} tracks`);
|
|
286
|
+
return ranked;
|
|
287
|
+
}
|
|
288
|
+
}
|