ziplayer 0.3.4 → 0.3.6
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/dist/plugins/index.d.ts +6 -15
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +207 -218
- package/dist/plugins/index.js.map +1 -1
- package/dist/structures/Player.d.ts.map +1 -1
- package/dist/structures/Player.js +4 -1
- package/dist/structures/Player.js.map +1 -1
- package/dist/structures/StreamManager.d.ts +1 -0
- package/dist/structures/StreamManager.d.ts.map +1 -1
- package/dist/structures/StreamManager.js +1 -0
- package/dist/structures/StreamManager.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/index.ts +245 -261
- package/src/structures/Player.ts +6 -3
- package/src/structures/StreamManager.ts +2 -0
- package/src/types/index.ts +1 -0
package/dist/plugins/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ type PluginManagerOptions = {
|
|
|
11
11
|
searchMinScore?: number;
|
|
12
12
|
};
|
|
13
13
|
export { BasePlugin } from "./BasePlugin";
|
|
14
|
+
type ExtractedMediaId = {
|
|
15
|
+
platform: "youtube" | "spotify" | "soundcloud" | "unknown";
|
|
16
|
+
id: string;
|
|
17
|
+
url: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function extractMediaId(input: string): ExtractedMediaId | null;
|
|
14
20
|
export declare class PluginManager {
|
|
15
21
|
private options;
|
|
16
22
|
private player;
|
|
@@ -41,21 +47,6 @@ export declare class PluginManager {
|
|
|
41
47
|
* @returns SearchScore object with score and reason
|
|
42
48
|
*/
|
|
43
49
|
evaluateTrackMatch(track: Track, query: string): SearchScore;
|
|
44
|
-
/**
|
|
45
|
-
* Evaluate and rank all search results from a plugin
|
|
46
|
-
* @param tracks List of tracks to evaluate
|
|
47
|
-
* @param query Original query
|
|
48
|
-
* @param isPlaylistResult Whether this is a playlist result (skip per-track evaluation)
|
|
49
|
-
* @returns List of tracks with scores, sorted by score
|
|
50
|
-
*/
|
|
51
|
-
private evaluateAndRankResults;
|
|
52
|
-
/**
|
|
53
|
-
* Select the best result from multiple plugins
|
|
54
|
-
* @param allResults Results from various plugins
|
|
55
|
-
* @param query Original query
|
|
56
|
-
* @returns The best result
|
|
57
|
-
*/
|
|
58
|
-
private selectBestResult;
|
|
59
50
|
/**
|
|
60
51
|
* Search with deduplication and evaluation of results
|
|
61
52
|
* @param query Search query
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,KAAK,oBAAoB,GAAG;IAC3B,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAE5D,KAAK,oBAAoB,GAAG;IAC3B,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AA0J1C,KAAK,gBAAgB,GAAG;IACvB,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;IAC3D,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAqFrE;AAcD,qBAAa,aAAa;IACzB,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,OAAO,CAAsC;IACrD,OAAO,CAAC,WAAW,CAA4C;IAC/D,OAAO,CAAC,WAAW,CAA4C;IAC/D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiB;IAClD,OAAO,CAAC,cAAc,CAAsD;IAC5E,OAAO,CAAC,eAAe,CAAwD;IAC/E,OAAO,CAAC,aAAa,CAAC,CAAgB;gBAE1B,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,oBAAoB;IAYjF,KAAK,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,EAAE,GAAG,IAAI;IAMpD,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IASlC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAMjC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIzC,MAAM,IAAI,UAAU,EAAE;IAItB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IASjD,KAAK,IAAI,IAAI;IAQb,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAK9C,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,eAAe;IAYvB;;;;;OAKG;IACI,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW;IA4FnE;;;;;OAKG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;YAoChE,cAAc;IAiE5B;;OAEG;IACH,qBAAqB,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE;IAoBjF;;OAEG;IACH,gBAAgB,IAAI,IAAI;IAMxB;;OAEG;IACH,mBAAmB,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE;IAWvD,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,eAAe;YAYT,mBAAmB;YAmBnB,iBAAiB;IA2LzB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAkBzD,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAQnC,gBAAgB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAsEtD,gBAAgB,IAAI,IAAI;IAMxB,QAAQ,IAAI,MAAM;YAWJ,0BAA0B;IAWxC,OAAO,CAAC,wBAAwB;CAwBhC"}
|
package/dist/plugins/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PluginManager = exports.BasePlugin = void 0;
|
|
4
|
+
exports.extractMediaId = extractMediaId;
|
|
4
5
|
const timeout_1 = require("../utils/timeout");
|
|
5
6
|
var BasePlugin_1 = require("./BasePlugin");
|
|
6
7
|
Object.defineProperty(exports, "BasePlugin", { enumerable: true, get: function () { return BasePlugin_1.BasePlugin; } });
|
|
@@ -33,8 +34,84 @@ function normalize(str) {
|
|
|
33
34
|
.replace(/\s+/g, " ")
|
|
34
35
|
.trim();
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
function getContentQualityScore(track) {
|
|
38
|
+
const title = normalize(track.title);
|
|
39
|
+
let score = 0;
|
|
40
|
+
// ưu tiên nhạc official
|
|
41
|
+
for (const k of OFFICIAL_KEYWORDS) {
|
|
42
|
+
if (title.includes(k))
|
|
43
|
+
score += 80;
|
|
44
|
+
}
|
|
45
|
+
// nhạc thường
|
|
46
|
+
for (const k of MUSIC_KEYWORDS) {
|
|
47
|
+
if (title.includes(k))
|
|
48
|
+
score += 10;
|
|
49
|
+
}
|
|
50
|
+
// phạt content rác
|
|
51
|
+
for (const k of BAD_KEYWORDS) {
|
|
52
|
+
if (title.includes(k))
|
|
53
|
+
score -= 120;
|
|
54
|
+
}
|
|
55
|
+
// youtube verified / artist channel
|
|
56
|
+
const author = normalize(track?.author || track?.metadata?.author || "");
|
|
57
|
+
if (author.includes("vevo") || author.includes("official") || author.includes("topic")) {
|
|
58
|
+
score += 20;
|
|
59
|
+
}
|
|
60
|
+
// phạt video quá dài (podcast/review)
|
|
61
|
+
if (track.duration && track.duration > 15 * 60 * 1000) {
|
|
62
|
+
score -= 20;
|
|
63
|
+
}
|
|
64
|
+
return score;
|
|
65
|
+
}
|
|
66
|
+
function dedupeTracks(tracks) {
|
|
67
|
+
const unique = new Map();
|
|
68
|
+
for (const track of tracks) {
|
|
69
|
+
const key = normalize(`${track.title} ${track?.author || track?.metadata?.author || ""}`);
|
|
70
|
+
const existing = unique.get(key);
|
|
71
|
+
if (!existing) {
|
|
72
|
+
unique.set(key, track);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const oldScore = getContentQualityScore(existing);
|
|
76
|
+
const newScore = getContentQualityScore(track);
|
|
77
|
+
if (newScore > oldScore) {
|
|
78
|
+
unique.set(key, track);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return [...unique.values()];
|
|
82
|
+
}
|
|
83
|
+
// const MUSIC_KEYWORDS = ["official", "mv", "audio", "lyrics", "remix", "cover", "ft", "feat", "prod", "music video"];
|
|
37
84
|
const NON_MUSIC_KEYWORDS = ["reaction", "review", "podcast", "interview", "vlog", "live stream", "news", "tiktok"];
|
|
85
|
+
const OFFICIAL_KEYWORDS = ["official", "official video", "official audio", "music video", "mv", "audio", "visualizer", "lyrics"];
|
|
86
|
+
const MUSIC_KEYWORDS = [
|
|
87
|
+
"song",
|
|
88
|
+
"track",
|
|
89
|
+
"remix",
|
|
90
|
+
"cover",
|
|
91
|
+
"instrumental",
|
|
92
|
+
"karaoke",
|
|
93
|
+
"nightcore",
|
|
94
|
+
"sped up",
|
|
95
|
+
"slowed",
|
|
96
|
+
"feat",
|
|
97
|
+
"ft",
|
|
98
|
+
];
|
|
99
|
+
const BAD_KEYWORDS = [
|
|
100
|
+
"reaction",
|
|
101
|
+
"review",
|
|
102
|
+
"podcast",
|
|
103
|
+
"interview",
|
|
104
|
+
"vlog",
|
|
105
|
+
"livestream",
|
|
106
|
+
"live stream",
|
|
107
|
+
"news",
|
|
108
|
+
"analysis",
|
|
109
|
+
"commentary",
|
|
110
|
+
"tiktok",
|
|
111
|
+
"shorts",
|
|
112
|
+
"funny",
|
|
113
|
+
"meme",
|
|
114
|
+
];
|
|
38
115
|
function detectContentType(title) {
|
|
39
116
|
const t = title.toLowerCase();
|
|
40
117
|
let score = 0;
|
|
@@ -64,6 +141,79 @@ function scoreTrack(base, candidate) {
|
|
|
64
141
|
score += detectContentType(candidate.title);
|
|
65
142
|
return score;
|
|
66
143
|
}
|
|
144
|
+
function extractMediaId(input) {
|
|
145
|
+
try {
|
|
146
|
+
const url = new URL(input);
|
|
147
|
+
const host = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
148
|
+
// =====================================================
|
|
149
|
+
// YOUTUBE
|
|
150
|
+
// =====================================================
|
|
151
|
+
if (host === "youtube.com" || host === "m.youtube.com" || host === "music.youtube.com") {
|
|
152
|
+
const videoId = url.searchParams.get("v");
|
|
153
|
+
if (videoId) {
|
|
154
|
+
return {
|
|
155
|
+
platform: "youtube",
|
|
156
|
+
id: videoId,
|
|
157
|
+
url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (host === "youtu.be") {
|
|
162
|
+
const id = url.pathname.slice(1);
|
|
163
|
+
if (id) {
|
|
164
|
+
return {
|
|
165
|
+
platform: "youtube",
|
|
166
|
+
id,
|
|
167
|
+
url: `https://www.youtube.com/watch?v=${id}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// =====================================================
|
|
172
|
+
// SPOTIFY
|
|
173
|
+
// =====================================================
|
|
174
|
+
if (host === "open.spotify.com") {
|
|
175
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
176
|
+
// track/playlist/album/episode/show
|
|
177
|
+
if (parts.length >= 2) {
|
|
178
|
+
const [, id] = parts;
|
|
179
|
+
return {
|
|
180
|
+
platform: "spotify",
|
|
181
|
+
id,
|
|
182
|
+
url: `https://open.spotify.com/${parts[0]}/${id}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// spotify uri
|
|
187
|
+
if (input.startsWith("spotify:")) {
|
|
188
|
+
const parts = input.split(":");
|
|
189
|
+
if (parts.length >= 3) {
|
|
190
|
+
return {
|
|
191
|
+
platform: "spotify",
|
|
192
|
+
id: parts[2],
|
|
193
|
+
url: `https://open.spotify.com/${parts[1]}/${parts[2]}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// =====================================================
|
|
198
|
+
// SOUNDCLOUD
|
|
199
|
+
// =====================================================
|
|
200
|
+
if (host === "soundcloud.com") {
|
|
201
|
+
const path = url.pathname.split("/").filter(Boolean);
|
|
202
|
+
if (path.length >= 2) {
|
|
203
|
+
const id = `${path[0]}/${path[1]}`;
|
|
204
|
+
return {
|
|
205
|
+
platform: "soundcloud",
|
|
206
|
+
id,
|
|
207
|
+
url: `https://soundcloud.com/${id}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
67
217
|
class PluginManager {
|
|
68
218
|
constructor(player, manager, options) {
|
|
69
219
|
this.plugins = new Map();
|
|
@@ -175,10 +325,20 @@ class PluginManager {
|
|
|
175
325
|
exactMatch: true,
|
|
176
326
|
};
|
|
177
327
|
}
|
|
328
|
+
const queryMedia = extractMediaId(query);
|
|
329
|
+
const trackMedia = extractMediaId(track.url || "");
|
|
330
|
+
if (queryMedia && trackMedia && queryMedia.platform === trackMedia.platform && queryMedia.id === trackMedia.id) {
|
|
331
|
+
return {
|
|
332
|
+
score: 100,
|
|
333
|
+
reason: `${queryMedia.platform} exact ID match`,
|
|
334
|
+
matchedBy: "url",
|
|
335
|
+
exactMatch: true,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
178
338
|
// 2. Evaluate title match exactly - 100%
|
|
179
339
|
if (normalizedTitle === normalizedQuery) {
|
|
180
340
|
return {
|
|
181
|
-
score:
|
|
341
|
+
score: 90 + getContentQualityScore(track),
|
|
182
342
|
reason: "Title matches exactly",
|
|
183
343
|
matchedBy: "title",
|
|
184
344
|
exactMatch: true,
|
|
@@ -186,11 +346,9 @@ class PluginManager {
|
|
|
186
346
|
}
|
|
187
347
|
// 3. Evaluate title contains query or vice versa - 70-90%
|
|
188
348
|
if (normalizedTitle.includes(normalizedQuery) && normalizedQuery.length > 5) {
|
|
189
|
-
const ratio = normalizedQuery.length / normalizedTitle.length;
|
|
190
|
-
const score = 70 + Math.min(20, Math.floor(ratio * 20));
|
|
191
349
|
return {
|
|
192
|
-
score,
|
|
193
|
-
reason: `Title contains
|
|
350
|
+
score: 75 + getContentQualityScore(track),
|
|
351
|
+
reason: `Title contains query`,
|
|
194
352
|
matchedBy: "title",
|
|
195
353
|
exactMatch: false,
|
|
196
354
|
};
|
|
@@ -209,9 +367,10 @@ class PluginManager {
|
|
|
209
367
|
const simScore = similarity(normalizedTitle, normalizedQuery);
|
|
210
368
|
const tokenScore = tokenOverlap(normalizedTitle, normalizedQuery);
|
|
211
369
|
const contentTypeBonus = detectContentType(track.title);
|
|
370
|
+
const qualityScore = getContentQualityScore(track);
|
|
212
371
|
// Tính điểm tổng hợp: similarity 60%, token overlap 30%, content type 10%
|
|
213
|
-
let finalScore = simScore *
|
|
214
|
-
finalScore = Math.
|
|
372
|
+
let finalScore = simScore * 35 + tokenScore * 25 + qualityScore * 1.5;
|
|
373
|
+
finalScore = Math.max(0, Math.min(100, Math.floor(finalScore)));
|
|
215
374
|
if (finalScore >= 20) {
|
|
216
375
|
let reason = `Similarity ${Math.floor(simScore * 100)}%`;
|
|
217
376
|
if (contentTypeBonus > 0) {
|
|
@@ -232,68 +391,6 @@ class PluginManager {
|
|
|
232
391
|
exactMatch: false,
|
|
233
392
|
};
|
|
234
393
|
}
|
|
235
|
-
/**
|
|
236
|
-
* Evaluate and rank all search results from a plugin
|
|
237
|
-
* @param tracks List of tracks to evaluate
|
|
238
|
-
* @param query Original query
|
|
239
|
-
* @param isPlaylistResult Whether this is a playlist result (skip per-track evaluation)
|
|
240
|
-
* @returns List of tracks with scores, sorted by score
|
|
241
|
-
*/
|
|
242
|
-
evaluateAndRankResults(tracks, query, isPlaylistResult = false) {
|
|
243
|
-
// 🔥 Nếu là playlist result, giữ nguyên tất cả tracks với điểm 100%
|
|
244
|
-
if (isPlaylistResult && tracks.length > 0) {
|
|
245
|
-
this.debug(`[Evaluation] Playlist detected - keeping all ${tracks.length} tracks`);
|
|
246
|
-
return tracks.map((track) => ({
|
|
247
|
-
track,
|
|
248
|
-
score: {
|
|
249
|
-
score: 100,
|
|
250
|
-
reason: "Part of playlist",
|
|
251
|
-
matchedBy: "playlist",
|
|
252
|
-
exactMatch: false,
|
|
253
|
-
},
|
|
254
|
-
}));
|
|
255
|
-
}
|
|
256
|
-
const evaluated = tracks.map((track) => ({
|
|
257
|
-
track,
|
|
258
|
-
score: this.evaluateTrackMatch(track, query),
|
|
259
|
-
}));
|
|
260
|
-
// Sort by score in descending order (cao xuống thấp)
|
|
261
|
-
evaluated.sort((a, b) => b.score.score - a.score.score);
|
|
262
|
-
// Log evaluation results
|
|
263
|
-
for (const item of evaluated.slice(0, 5)) {
|
|
264
|
-
this.debug(`[Evaluation] "${item.track.title}" -> ${item.score.score}% (${item.score.reason})`);
|
|
265
|
-
}
|
|
266
|
-
return evaluated;
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Select the best result from multiple plugins
|
|
270
|
-
* @param allResults Results from various plugins
|
|
271
|
-
* @param query Original query
|
|
272
|
-
* @returns The best result
|
|
273
|
-
*/
|
|
274
|
-
selectBestResult(allResults, query) {
|
|
275
|
-
let bestResult = null;
|
|
276
|
-
let bestScore = -1;
|
|
277
|
-
for (const [source, result] of allResults) {
|
|
278
|
-
if (!result.tracks || result.tracks.length === 0)
|
|
279
|
-
continue;
|
|
280
|
-
// Evaluate the first (best) track of this result
|
|
281
|
-
const bestTrackScore = this.evaluateTrackMatch(result.tracks[0], query);
|
|
282
|
-
if (bestTrackScore.score > bestScore) {
|
|
283
|
-
bestScore = bestTrackScore.score;
|
|
284
|
-
bestResult = {
|
|
285
|
-
...result,
|
|
286
|
-
source,
|
|
287
|
-
score: bestTrackScore,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
this.debug(`[Selection] Plugin ${source} -> best match: ${bestTrackScore.score}%`);
|
|
291
|
-
}
|
|
292
|
-
if (bestResult && bestResult.score) {
|
|
293
|
-
this.debug(`[Selection] Selected result from ${bestResult.source} with score ${bestResult.score.score}%`);
|
|
294
|
-
}
|
|
295
|
-
return bestResult;
|
|
296
|
-
}
|
|
297
394
|
/**
|
|
298
395
|
* Search with deduplication and evaluation of results
|
|
299
396
|
* @param query Search query
|
|
@@ -332,161 +429,53 @@ class PluginManager {
|
|
|
332
429
|
}
|
|
333
430
|
async searchInternal(query, requestedBy) {
|
|
334
431
|
const timeoutMs = this.options.extractorTimeout ?? 15000;
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
const allSearchPlugins = this.getAll()
|
|
338
|
-
.filter((p) => typeof p.search === "function")
|
|
339
|
-
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
340
|
-
if (allSearchPlugins.length === 0) {
|
|
341
|
-
this.debug(`[Search] No plugins support search`);
|
|
432
|
+
const plugins = this.getAll().filter((p) => typeof p.search === "function");
|
|
433
|
+
if (!plugins.length)
|
|
342
434
|
return null;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
priorityGroups.set(priority, []);
|
|
349
|
-
}
|
|
350
|
-
priorityGroups.get(priority).push(plugin);
|
|
351
|
-
}
|
|
352
|
-
const sortedPriorities = Array.from(priorityGroups.keys()).sort((a, b) => b - a);
|
|
353
|
-
this.debug(`[Search] Priority groups: ${sortedPriorities.map((p) => `${p} (${priorityGroups.get(p).length} plugins)`).join(", ")}`);
|
|
354
|
-
const allResults = new Map();
|
|
355
|
-
const errors = [];
|
|
356
|
-
let foundHighQualityResult = false;
|
|
357
|
-
let bestResultOverall = null;
|
|
358
|
-
let bestScoreOverall = -1;
|
|
359
|
-
// 🔥 PROCESS EACH PRIORITY GROUP
|
|
360
|
-
for (const priority of sortedPriorities) {
|
|
361
|
-
const groupPlugins = priorityGroups.get(priority);
|
|
362
|
-
this.debug(`[Search] Processing priority group ${priority} with ${groupPlugins.length} plugins`);
|
|
363
|
-
const groupResults = new Map();
|
|
364
|
-
// Process all plugins in current priority group
|
|
365
|
-
for (const plugin of groupPlugins) {
|
|
366
|
-
// Skip if we already found a high-quality result (>=90%) from previous group
|
|
367
|
-
if (foundHighQualityResult) {
|
|
368
|
-
this.debug(`[Search] Skipping plugin ${plugin.name} (priority ${priority}) - already have high-quality result`);
|
|
369
|
-
break;
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
this.debug(`[Search] Trying plugin: ${plugin.name} (priority: ${priority})`);
|
|
373
|
-
const startTime = Date.now();
|
|
374
|
-
const result = await (0, timeout_1.withTimeout)(plugin.search(query, requestedBy), timeoutMs, `Search timeout for ${plugin.name}`);
|
|
375
|
-
const duration = Date.now() - startTime;
|
|
376
|
-
if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
|
|
377
|
-
// 🔥 KIỂM TRA XEM CÓ PLAYLIST HAY KHÔNG
|
|
378
|
-
const hasPlaylist = !!result.playlist;
|
|
379
|
-
const isPlaylistResult = hasPlaylist && result.tracks.length > 1;
|
|
380
|
-
// Evaluate and rank results from this plugin
|
|
381
|
-
const evaluatedTracks = this.evaluateAndRankResults(result.tracks, query, isPlaylistResult);
|
|
382
|
-
const bestScore = evaluatedTracks[0]?.score.score || 0;
|
|
383
|
-
// Filter tracks below minimum score (chỉ áp dụng cho non-playlist)
|
|
384
|
-
let validTracks;
|
|
385
|
-
if (isPlaylistResult) {
|
|
386
|
-
// 🔥 PLAYLIST: Giữ nguyên tất cả tracks, không lọc theo score
|
|
387
|
-
validTracks = evaluatedTracks.map((item) => item.track);
|
|
388
|
-
this.debug(`[Search] Plugin ${plugin.name} returned playlist with ${validTracks.length} tracks (keeping all)`);
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
// Single track search: filter by min score
|
|
392
|
-
validTracks = evaluatedTracks.filter((item) => item.score.score >= minScore).map((item) => item.track);
|
|
393
|
-
this.debug(`[Search] Plugin ${plugin.name} returned ${validTracks.length}/${result.tracks.length} valid tracks in ${duration}ms (best: ${bestScore}%)`);
|
|
394
|
-
}
|
|
395
|
-
if (validTracks.length > 0) {
|
|
396
|
-
const pluginResult = {
|
|
397
|
-
tracks: validTracks,
|
|
398
|
-
playlist: result.playlist,
|
|
399
|
-
query,
|
|
400
|
-
score: evaluatedTracks[0].score,
|
|
401
|
-
source: plugin.name,
|
|
402
|
-
};
|
|
403
|
-
groupResults.set(plugin.name, pluginResult);
|
|
404
|
-
allResults.set(plugin.name, pluginResult);
|
|
405
|
-
// 🔥 Nếu là playlist hoặc kết quả chất lượng cao, dừng search
|
|
406
|
-
if (isPlaylistResult || bestScore >= 90) {
|
|
407
|
-
foundHighQualityResult = true;
|
|
408
|
-
bestResultOverall = pluginResult;
|
|
409
|
-
bestScoreOverall = bestScore;
|
|
410
|
-
this.debug(`[Search] ${isPlaylistResult ? "Playlist" : "High-quality result"} found, stopping search`);
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
// Track best result overall
|
|
414
|
-
if (bestScore > bestScoreOverall) {
|
|
415
|
-
bestScoreOverall = bestScore;
|
|
416
|
-
bestResultOverall = pluginResult;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
this.debug(`[Search] Plugin ${plugin.name} returned ${result.tracks.length} tracks but none passed min score ${minScore}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
this.debug(`[Search] Plugin ${plugin.name} returned no tracks in ${duration}ms`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
catch (error) {
|
|
428
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
429
|
-
this.debug(`[Search] Plugin ${plugin.name} failed:`, err.message);
|
|
430
|
-
errors.push({ plugin: plugin.name, error: err });
|
|
435
|
+
const settled = await Promise.allSettled(plugins.map(async (plugin) => {
|
|
436
|
+
try {
|
|
437
|
+
const result = await (0, timeout_1.withTimeout)(plugin.search(query, requestedBy), timeoutMs, `Search timeout for ${plugin.name}`);
|
|
438
|
+
if (!result?.tracks?.length) {
|
|
439
|
+
return [];
|
|
431
440
|
}
|
|
441
|
+
return result.tracks.map((track) => ({
|
|
442
|
+
...track,
|
|
443
|
+
source: plugin.name,
|
|
444
|
+
}));
|
|
432
445
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
// If current group has at least one valid result, check if we should continue to lower priority groups
|
|
439
|
-
if (groupResults.size > 0) {
|
|
440
|
-
// Get best score from this group
|
|
441
|
-
const bestGroupScore = Math.max(...Array.from(groupResults.values()).map((r) => r.score?.score || 0));
|
|
442
|
-
// If best score in this group is >= 70%, don't try lower priority groups
|
|
443
|
-
if (bestGroupScore >= 70) {
|
|
444
|
-
this.debug(`[Search] Priority group ${priority} has results with score ${bestGroupScore}% (>=70%), stopping search`);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
// If best score is between 50-70%, continue to lower groups but log it
|
|
448
|
-
if (bestGroupScore >= 50) {
|
|
449
|
-
this.debug(`[Search] Priority group ${priority} has results with score ${bestGroupScore}% (50-70%), will try lower priority groups for better results`);
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
this.debug(`[Search] Priority group ${priority} has low quality results (${bestGroupScore}% <50%), trying lower priority groups`);
|
|
453
|
-
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
this.debug(`[Search] ${plugin.name} failed`, e);
|
|
448
|
+
return [];
|
|
454
449
|
}
|
|
455
|
-
|
|
456
|
-
|
|
450
|
+
}));
|
|
451
|
+
const allTracks = [];
|
|
452
|
+
for (const result of settled) {
|
|
453
|
+
if (result.status === "fulfilled") {
|
|
454
|
+
allTracks.push(...result.value);
|
|
457
455
|
}
|
|
458
456
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
// Cache the result
|
|
462
|
-
this.setCachedSearch(query, requestedBy, bestResultOverall);
|
|
463
|
-
// Log summary
|
|
464
|
-
const allTracksCount = Array.from(allResults.values()).reduce((sum, r) => sum + r.tracks.length, 0);
|
|
465
|
-
this.debug(`[Search] Complete - Best from ${bestResultOverall.source}: ${bestResultOverall.tracks.length} tracks (${bestResultOverall.score?.score || 0}%), Total candidates: ${allTracksCount}`);
|
|
466
|
-
return bestResultOverall;
|
|
467
|
-
}
|
|
468
|
-
// 🔥 FALLBACK: If we have any results that didn't meet min score, return the best one
|
|
469
|
-
if (allResults.size === 0 && errors.length > 0) {
|
|
470
|
-
// Try to get best result from allResults even if below min score
|
|
471
|
-
let fallbackResult = null;
|
|
472
|
-
let fallbackScore = -1;
|
|
473
|
-
for (const [source, result] of allResults) {
|
|
474
|
-
const score = result.score?.score || 0;
|
|
475
|
-
if (score > fallbackScore) {
|
|
476
|
-
fallbackScore = score;
|
|
477
|
-
fallbackResult = result;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
if (fallbackResult) {
|
|
481
|
-
this.debug(`[Search] Using fallback result with score ${fallbackScore}% (below minimum ${minScore}%)`);
|
|
482
|
-
return fallbackResult;
|
|
483
|
-
}
|
|
484
|
-
this.debug(`[Search] All ${allSearchPlugins.length} plugins failed for query: ${query}`);
|
|
485
|
-
const lastError = errors[errors.length - 1]?.error;
|
|
486
|
-
if (lastError)
|
|
487
|
-
throw lastError;
|
|
457
|
+
if (!allTracks.length) {
|
|
458
|
+
return null;
|
|
488
459
|
}
|
|
489
|
-
|
|
460
|
+
// dedupe
|
|
461
|
+
const deduped = dedupeTracks(allTracks);
|
|
462
|
+
// score + sort
|
|
463
|
+
const ranked = deduped
|
|
464
|
+
.map((track) => ({
|
|
465
|
+
track,
|
|
466
|
+
score: this.evaluateTrackMatch(track, query),
|
|
467
|
+
}))
|
|
468
|
+
.sort((a, b) => b.score.score - a.score.score);
|
|
469
|
+
const tracks = ranked.map((x) => x.track);
|
|
470
|
+
const finalResult = {
|
|
471
|
+
query,
|
|
472
|
+
tracks,
|
|
473
|
+
source: "multi-search",
|
|
474
|
+
score: ranked[0]?.score,
|
|
475
|
+
};
|
|
476
|
+
this.setCachedSearch(query, requestedBy, finalResult);
|
|
477
|
+
this.debug(`[Search] Aggregated ${tracks.length} tracks from ${plugins.length} plugins`);
|
|
478
|
+
return finalResult;
|
|
490
479
|
}
|
|
491
480
|
/**
|
|
492
481
|
* Get plugin priority groups info for debugging
|