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/src/plugins/index.ts
CHANGED
|
@@ -47,9 +47,100 @@ function normalize(str: string): string {
|
|
|
47
47
|
.trim();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
function getContentQualityScore(track: Track): number {
|
|
51
|
+
const title = normalize(track.title);
|
|
52
|
+
|
|
53
|
+
let score = 0;
|
|
54
|
+
|
|
55
|
+
// ưu tiên nhạc official
|
|
56
|
+
for (const k of OFFICIAL_KEYWORDS) {
|
|
57
|
+
if (title.includes(k)) score += 80;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// nhạc thường
|
|
61
|
+
for (const k of MUSIC_KEYWORDS) {
|
|
62
|
+
if (title.includes(k)) score += 10;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// phạt content rác
|
|
66
|
+
for (const k of BAD_KEYWORDS) {
|
|
67
|
+
if (title.includes(k)) score -= 120;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// youtube verified / artist channel
|
|
71
|
+
const author = normalize(track?.author || track?.metadata?.author || "");
|
|
72
|
+
|
|
73
|
+
if (author.includes("vevo") || author.includes("official") || author.includes("topic")) {
|
|
74
|
+
score += 20;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// phạt video quá dài (podcast/review)
|
|
78
|
+
if (track.duration && track.duration > 15 * 60 * 1000) {
|
|
79
|
+
score -= 20;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return score;
|
|
83
|
+
}
|
|
84
|
+
function dedupeTracks(tracks: Track[]): Track[] {
|
|
85
|
+
const unique = new Map<string, Track>();
|
|
86
|
+
|
|
87
|
+
for (const track of tracks) {
|
|
88
|
+
const key = normalize(`${track.title} ${track?.author || track?.metadata?.author || ""}`);
|
|
89
|
+
|
|
90
|
+
const existing = unique.get(key);
|
|
91
|
+
|
|
92
|
+
if (!existing) {
|
|
93
|
+
unique.set(key, track);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const oldScore = getContentQualityScore(existing);
|
|
98
|
+
const newScore = getContentQualityScore(track);
|
|
99
|
+
|
|
100
|
+
if (newScore > oldScore) {
|
|
101
|
+
unique.set(key, track);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return [...unique.values()];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// const MUSIC_KEYWORDS = ["official", "mv", "audio", "lyrics", "remix", "cover", "ft", "feat", "prod", "music video"];
|
|
51
109
|
const NON_MUSIC_KEYWORDS = ["reaction", "review", "podcast", "interview", "vlog", "live stream", "news", "tiktok"];
|
|
52
110
|
|
|
111
|
+
const OFFICIAL_KEYWORDS = ["official", "official video", "official audio", "music video", "mv", "audio", "visualizer", "lyrics"];
|
|
112
|
+
|
|
113
|
+
const MUSIC_KEYWORDS = [
|
|
114
|
+
"song",
|
|
115
|
+
"track",
|
|
116
|
+
"remix",
|
|
117
|
+
"cover",
|
|
118
|
+
"instrumental",
|
|
119
|
+
"karaoke",
|
|
120
|
+
"nightcore",
|
|
121
|
+
"sped up",
|
|
122
|
+
"slowed",
|
|
123
|
+
"feat",
|
|
124
|
+
"ft",
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const BAD_KEYWORDS = [
|
|
128
|
+
"reaction",
|
|
129
|
+
"review",
|
|
130
|
+
"podcast",
|
|
131
|
+
"interview",
|
|
132
|
+
"vlog",
|
|
133
|
+
"livestream",
|
|
134
|
+
"live stream",
|
|
135
|
+
"news",
|
|
136
|
+
"analysis",
|
|
137
|
+
"commentary",
|
|
138
|
+
"tiktok",
|
|
139
|
+
"shorts",
|
|
140
|
+
"funny",
|
|
141
|
+
"meme",
|
|
142
|
+
];
|
|
143
|
+
|
|
53
144
|
function detectContentType(title: string): number {
|
|
54
145
|
const t = title.toLowerCase();
|
|
55
146
|
let score = 0;
|
|
@@ -76,6 +167,99 @@ function scoreTrack(base: Track, candidate: Track): number {
|
|
|
76
167
|
return score;
|
|
77
168
|
}
|
|
78
169
|
|
|
170
|
+
type ExtractedMediaId = {
|
|
171
|
+
platform: "youtube" | "spotify" | "soundcloud" | "unknown";
|
|
172
|
+
id: string;
|
|
173
|
+
url: string;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export function extractMediaId(input: string): ExtractedMediaId | null {
|
|
177
|
+
try {
|
|
178
|
+
const url = new URL(input);
|
|
179
|
+
|
|
180
|
+
const host = url.hostname.replace(/^www\./, "").toLowerCase();
|
|
181
|
+
|
|
182
|
+
// =====================================================
|
|
183
|
+
// YOUTUBE
|
|
184
|
+
// =====================================================
|
|
185
|
+
if (host === "youtube.com" || host === "m.youtube.com" || host === "music.youtube.com") {
|
|
186
|
+
const videoId = url.searchParams.get("v");
|
|
187
|
+
|
|
188
|
+
if (videoId) {
|
|
189
|
+
return {
|
|
190
|
+
platform: "youtube",
|
|
191
|
+
id: videoId,
|
|
192
|
+
url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (host === "youtu.be") {
|
|
198
|
+
const id = url.pathname.slice(1);
|
|
199
|
+
|
|
200
|
+
if (id) {
|
|
201
|
+
return {
|
|
202
|
+
platform: "youtube",
|
|
203
|
+
id,
|
|
204
|
+
url: `https://www.youtube.com/watch?v=${id}`,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// =====================================================
|
|
210
|
+
// SPOTIFY
|
|
211
|
+
// =====================================================
|
|
212
|
+
if (host === "open.spotify.com") {
|
|
213
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
214
|
+
|
|
215
|
+
// track/playlist/album/episode/show
|
|
216
|
+
if (parts.length >= 2) {
|
|
217
|
+
const [, id] = parts;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
platform: "spotify",
|
|
221
|
+
id,
|
|
222
|
+
url: `https://open.spotify.com/${parts[0]}/${id}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// spotify uri
|
|
228
|
+
if (input.startsWith("spotify:")) {
|
|
229
|
+
const parts = input.split(":");
|
|
230
|
+
|
|
231
|
+
if (parts.length >= 3) {
|
|
232
|
+
return {
|
|
233
|
+
platform: "spotify",
|
|
234
|
+
id: parts[2],
|
|
235
|
+
url: `https://open.spotify.com/${parts[1]}/${parts[2]}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// =====================================================
|
|
241
|
+
// SOUNDCLOUD
|
|
242
|
+
// =====================================================
|
|
243
|
+
if (host === "soundcloud.com") {
|
|
244
|
+
const path = url.pathname.split("/").filter(Boolean);
|
|
245
|
+
|
|
246
|
+
if (path.length >= 2) {
|
|
247
|
+
const id = `${path[0]}/${path[1]}`;
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
platform: "soundcloud",
|
|
251
|
+
id,
|
|
252
|
+
url: `https://soundcloud.com/${id}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return null;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
79
263
|
interface SearchCacheEntry {
|
|
80
264
|
result: SearchResult;
|
|
81
265
|
timestamp: number;
|
|
@@ -219,11 +403,21 @@ export class PluginManager {
|
|
|
219
403
|
exactMatch: true,
|
|
220
404
|
};
|
|
221
405
|
}
|
|
406
|
+
const queryMedia = extractMediaId(query);
|
|
407
|
+
const trackMedia = extractMediaId(track.url || "");
|
|
222
408
|
|
|
409
|
+
if (queryMedia && trackMedia && queryMedia.platform === trackMedia.platform && queryMedia.id === trackMedia.id) {
|
|
410
|
+
return {
|
|
411
|
+
score: 100,
|
|
412
|
+
reason: `${queryMedia.platform} exact ID match`,
|
|
413
|
+
matchedBy: "url",
|
|
414
|
+
exactMatch: true,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
223
417
|
// 2. Evaluate title match exactly - 100%
|
|
224
418
|
if (normalizedTitle === normalizedQuery) {
|
|
225
419
|
return {
|
|
226
|
-
score:
|
|
420
|
+
score: 90 + getContentQualityScore(track),
|
|
227
421
|
reason: "Title matches exactly",
|
|
228
422
|
matchedBy: "title",
|
|
229
423
|
exactMatch: true,
|
|
@@ -232,11 +426,9 @@ export class PluginManager {
|
|
|
232
426
|
|
|
233
427
|
// 3. Evaluate title contains query or vice versa - 70-90%
|
|
234
428
|
if (normalizedTitle.includes(normalizedQuery) && normalizedQuery.length > 5) {
|
|
235
|
-
const ratio = normalizedQuery.length / normalizedTitle.length;
|
|
236
|
-
const score = 70 + Math.min(20, Math.floor(ratio * 20));
|
|
237
429
|
return {
|
|
238
|
-
score,
|
|
239
|
-
reason: `Title contains
|
|
430
|
+
score: 75 + getContentQualityScore(track),
|
|
431
|
+
reason: `Title contains query`,
|
|
240
432
|
matchedBy: "title",
|
|
241
433
|
exactMatch: false,
|
|
242
434
|
};
|
|
@@ -257,10 +449,12 @@ export class PluginManager {
|
|
|
257
449
|
const simScore = similarity(normalizedTitle, normalizedQuery);
|
|
258
450
|
const tokenScore = tokenOverlap(normalizedTitle, normalizedQuery);
|
|
259
451
|
const contentTypeBonus = detectContentType(track.title);
|
|
260
|
-
|
|
452
|
+
const qualityScore = getContentQualityScore(track);
|
|
261
453
|
// Tính điểm tổng hợp: similarity 60%, token overlap 30%, content type 10%
|
|
262
|
-
|
|
263
|
-
finalScore =
|
|
454
|
+
|
|
455
|
+
let finalScore = simScore * 35 + tokenScore * 25 + qualityScore * 1.5;
|
|
456
|
+
|
|
457
|
+
finalScore = Math.max(0, Math.min(100, Math.floor(finalScore)));
|
|
264
458
|
|
|
265
459
|
if (finalScore >= 20) {
|
|
266
460
|
let reason = `Similarity ${Math.floor(simScore * 100)}%`;
|
|
@@ -286,83 +480,6 @@ export class PluginManager {
|
|
|
286
480
|
};
|
|
287
481
|
}
|
|
288
482
|
|
|
289
|
-
/**
|
|
290
|
-
* Evaluate and rank all search results from a plugin
|
|
291
|
-
* @param tracks List of tracks to evaluate
|
|
292
|
-
* @param query Original query
|
|
293
|
-
* @param isPlaylistResult Whether this is a playlist result (skip per-track evaluation)
|
|
294
|
-
* @returns List of tracks with scores, sorted by score
|
|
295
|
-
*/
|
|
296
|
-
private evaluateAndRankResults(
|
|
297
|
-
tracks: Track[],
|
|
298
|
-
query: string,
|
|
299
|
-
isPlaylistResult: boolean = false,
|
|
300
|
-
): { track: Track; score: SearchScore }[] {
|
|
301
|
-
// 🔥 Nếu là playlist result, giữ nguyên tất cả tracks với điểm 100%
|
|
302
|
-
if (isPlaylistResult && tracks.length > 0) {
|
|
303
|
-
this.debug(`[Evaluation] Playlist detected - keeping all ${tracks.length} tracks`);
|
|
304
|
-
|
|
305
|
-
return tracks.map((track) => ({
|
|
306
|
-
track,
|
|
307
|
-
score: {
|
|
308
|
-
score: 100,
|
|
309
|
-
reason: "Part of playlist",
|
|
310
|
-
matchedBy: "playlist",
|
|
311
|
-
exactMatch: false,
|
|
312
|
-
},
|
|
313
|
-
}));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const evaluated = tracks.map((track) => ({
|
|
317
|
-
track,
|
|
318
|
-
score: this.evaluateTrackMatch(track, query),
|
|
319
|
-
}));
|
|
320
|
-
|
|
321
|
-
// Sort by score in descending order (cao xuống thấp)
|
|
322
|
-
evaluated.sort((a, b) => b.score.score - a.score.score);
|
|
323
|
-
|
|
324
|
-
// Log evaluation results
|
|
325
|
-
for (const item of evaluated.slice(0, 5)) {
|
|
326
|
-
this.debug(`[Evaluation] "${item.track.title}" -> ${item.score.score}% (${item.score.reason})`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return evaluated;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Select the best result from multiple plugins
|
|
333
|
-
* @param allResults Results from various plugins
|
|
334
|
-
* @param query Original query
|
|
335
|
-
* @returns The best result
|
|
336
|
-
*/
|
|
337
|
-
private selectBestResult(allResults: Map<string, SearchResult>, query: string): SearchResult | null {
|
|
338
|
-
let bestResult: SearchResult | null = null;
|
|
339
|
-
let bestScore = -1;
|
|
340
|
-
|
|
341
|
-
for (const [source, result] of allResults) {
|
|
342
|
-
if (!result.tracks || result.tracks.length === 0) continue;
|
|
343
|
-
|
|
344
|
-
// Evaluate the first (best) track of this result
|
|
345
|
-
const bestTrackScore = this.evaluateTrackMatch(result.tracks[0], query);
|
|
346
|
-
|
|
347
|
-
if (bestTrackScore.score > bestScore) {
|
|
348
|
-
bestScore = bestTrackScore.score;
|
|
349
|
-
bestResult = {
|
|
350
|
-
...result,
|
|
351
|
-
source,
|
|
352
|
-
score: bestTrackScore,
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
this.debug(`[Selection] Plugin ${source} -> best match: ${bestTrackScore.score}%`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (bestResult && bestResult.score) {
|
|
360
|
-
this.debug(`[Selection] Selected result from ${bestResult.source} with score ${bestResult.score.score}%`);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return bestResult;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
483
|
/**
|
|
367
484
|
* Search with deduplication and evaluation of results
|
|
368
485
|
* @param query Search query
|
|
@@ -407,202 +524,69 @@ export class PluginManager {
|
|
|
407
524
|
|
|
408
525
|
private async searchInternal(query: string, requestedBy: string): Promise<SearchResult | null> {
|
|
409
526
|
const timeoutMs = this.options.extractorTimeout ?? 15000;
|
|
410
|
-
const minScore = this.options.searchMinScore ?? 30;
|
|
411
|
-
|
|
412
|
-
// Get all plugins that support search
|
|
413
|
-
const allSearchPlugins = this.getAll()
|
|
414
|
-
.filter((p) => typeof p.search === "function")
|
|
415
|
-
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
416
|
-
|
|
417
|
-
if (allSearchPlugins.length === 0) {
|
|
418
|
-
this.debug(`[Search] No plugins support search`);
|
|
419
|
-
return null;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const priorityGroups = new Map<number, BasePlugin[]>();
|
|
423
|
-
for (const plugin of allSearchPlugins) {
|
|
424
|
-
const priority = plugin.priority ?? 0;
|
|
425
|
-
if (!priorityGroups.has(priority)) {
|
|
426
|
-
priorityGroups.set(priority, []);
|
|
427
|
-
}
|
|
428
|
-
priorityGroups.get(priority)!.push(plugin);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const sortedPriorities = Array.from(priorityGroups.keys()).sort((a, b) => b - a);
|
|
432
|
-
|
|
433
|
-
this.debug(
|
|
434
|
-
`[Search] Priority groups: ${sortedPriorities.map((p) => `${p} (${priorityGroups.get(p)!.length} plugins)`).join(", ")}`,
|
|
435
|
-
);
|
|
436
|
-
|
|
437
|
-
const allResults = new Map<string, SearchResult>();
|
|
438
|
-
const errors: Array<{ plugin: string; error: Error }> = [];
|
|
439
527
|
|
|
440
|
-
|
|
441
|
-
let bestResultOverall: SearchResult | null = null;
|
|
442
|
-
let bestScoreOverall = -1;
|
|
528
|
+
const plugins = this.getAll().filter((p) => typeof p.search === "function");
|
|
443
529
|
|
|
444
|
-
|
|
445
|
-
for (const priority of sortedPriorities) {
|
|
446
|
-
const groupPlugins = priorityGroups.get(priority)!;
|
|
447
|
-
this.debug(`[Search] Processing priority group ${priority} with ${groupPlugins.length} plugins`);
|
|
448
|
-
|
|
449
|
-
const groupResults = new Map<string, SearchResult>();
|
|
450
|
-
|
|
451
|
-
// Process all plugins in current priority group
|
|
452
|
-
for (const plugin of groupPlugins) {
|
|
453
|
-
// Skip if we already found a high-quality result (>=90%) from previous group
|
|
454
|
-
if (foundHighQualityResult) {
|
|
455
|
-
this.debug(`[Search] Skipping plugin ${plugin.name} (priority ${priority}) - already have high-quality result`);
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
530
|
+
if (!plugins.length) return null;
|
|
458
531
|
|
|
532
|
+
const settled = await Promise.allSettled(
|
|
533
|
+
plugins.map(async (plugin) => {
|
|
459
534
|
try {
|
|
460
|
-
this.debug(`[Search] Trying plugin: ${plugin.name} (priority: ${priority})`);
|
|
461
|
-
|
|
462
|
-
const startTime = Date.now();
|
|
463
535
|
const result = await withTimeout(plugin.search(query, requestedBy), timeoutMs, `Search timeout for ${plugin.name}`);
|
|
464
536
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
if (result && Array.isArray(result.tracks) && result.tracks.length > 0) {
|
|
468
|
-
// 🔥 KIỂM TRA XEM CÓ PLAYLIST HAY KHÔNG
|
|
469
|
-
const hasPlaylist = !!result.playlist;
|
|
470
|
-
const isPlaylistResult = hasPlaylist && result.tracks.length > 1;
|
|
471
|
-
|
|
472
|
-
// Evaluate and rank results from this plugin
|
|
473
|
-
const evaluatedTracks = this.evaluateAndRankResults(result.tracks, query, isPlaylistResult);
|
|
474
|
-
|
|
475
|
-
const bestScore = evaluatedTracks[0]?.score.score || 0;
|
|
476
|
-
|
|
477
|
-
// Filter tracks below minimum score (chỉ áp dụng cho non-playlist)
|
|
478
|
-
let validTracks: Track[];
|
|
479
|
-
|
|
480
|
-
if (isPlaylistResult) {
|
|
481
|
-
// 🔥 PLAYLIST: Giữ nguyên tất cả tracks, không lọc theo score
|
|
482
|
-
validTracks = evaluatedTracks.map((item) => item.track);
|
|
483
|
-
this.debug(`[Search] Plugin ${plugin.name} returned playlist with ${validTracks.length} tracks (keeping all)`);
|
|
484
|
-
} else {
|
|
485
|
-
// Single track search: filter by min score
|
|
486
|
-
validTracks = evaluatedTracks.filter((item) => item.score.score >= minScore).map((item) => item.track);
|
|
487
|
-
this.debug(
|
|
488
|
-
`[Search] Plugin ${plugin.name} returned ${validTracks.length}/${result.tracks.length} valid tracks in ${duration}ms (best: ${bestScore}%)`,
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (validTracks.length > 0) {
|
|
493
|
-
const pluginResult: SearchResult = {
|
|
494
|
-
tracks: validTracks,
|
|
495
|
-
playlist: result.playlist,
|
|
496
|
-
query,
|
|
497
|
-
score: evaluatedTracks[0].score,
|
|
498
|
-
source: plugin.name,
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
groupResults.set(plugin.name, pluginResult);
|
|
502
|
-
allResults.set(plugin.name, pluginResult);
|
|
503
|
-
|
|
504
|
-
// 🔥 Nếu là playlist hoặc kết quả chất lượng cao, dừng search
|
|
505
|
-
if (isPlaylistResult || bestScore >= 90) {
|
|
506
|
-
foundHighQualityResult = true;
|
|
507
|
-
bestResultOverall = pluginResult;
|
|
508
|
-
bestScoreOverall = bestScore;
|
|
509
|
-
this.debug(`[Search] ${isPlaylistResult ? "Playlist" : "High-quality result"} found, stopping search`);
|
|
510
|
-
break;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Track best result overall
|
|
514
|
-
if (bestScore > bestScoreOverall) {
|
|
515
|
-
bestScoreOverall = bestScore;
|
|
516
|
-
bestResultOverall = pluginResult;
|
|
517
|
-
}
|
|
518
|
-
} else {
|
|
519
|
-
this.debug(
|
|
520
|
-
`[Search] Plugin ${plugin.name} returned ${result.tracks.length} tracks but none passed min score ${minScore}`,
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
} else {
|
|
524
|
-
this.debug(`[Search] Plugin ${plugin.name} returned no tracks in ${duration}ms`);
|
|
537
|
+
if (!result?.tracks?.length) {
|
|
538
|
+
return [];
|
|
525
539
|
}
|
|
526
|
-
} catch (error) {
|
|
527
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
528
|
-
this.debug(`[Search] Plugin ${plugin.name} failed:`, err.message);
|
|
529
|
-
errors.push({ plugin: plugin.name, error: err });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
540
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
// If current group has at least one valid result, check if we should continue to lower priority groups
|
|
540
|
-
if (groupResults.size > 0) {
|
|
541
|
-
// Get best score from this group
|
|
542
|
-
const bestGroupScore = Math.max(...Array.from(groupResults.values()).map((r) => r.score?.score || 0));
|
|
541
|
+
return result.tracks.map((track) => ({
|
|
542
|
+
...track,
|
|
543
|
+
source: plugin.name,
|
|
544
|
+
}));
|
|
545
|
+
} catch (e) {
|
|
546
|
+
this.debug(`[Search] ${plugin.name} failed`, e);
|
|
543
547
|
|
|
544
|
-
|
|
545
|
-
if (bestGroupScore >= 70) {
|
|
546
|
-
this.debug(`[Search] Priority group ${priority} has results with score ${bestGroupScore}% (>=70%), stopping search`);
|
|
547
|
-
break;
|
|
548
|
+
return [];
|
|
548
549
|
}
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
549
552
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
} else {
|
|
556
|
-
this.debug(
|
|
557
|
-
`[Search] Priority group ${priority} has low quality results (${bestGroupScore}% <50%), trying lower priority groups`,
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
} else {
|
|
561
|
-
this.debug(`[Search] Priority group ${priority} produced no valid results, trying lower priority group`);
|
|
553
|
+
const allTracks: Track[] = [];
|
|
554
|
+
|
|
555
|
+
for (const result of settled) {
|
|
556
|
+
if (result.status === "fulfilled") {
|
|
557
|
+
allTracks.push(...result.value);
|
|
562
558
|
}
|
|
563
559
|
}
|
|
564
560
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
this.setCachedSearch(query, requestedBy, bestResultOverall);
|
|
561
|
+
if (!allTracks.length) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
569
564
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
this.debug(
|
|
573
|
-
`[Search] Complete - Best from ${bestResultOverall.source}: ${bestResultOverall.tracks.length} tracks (${bestResultOverall.score?.score || 0}%), Total candidates: ${allTracksCount}`,
|
|
574
|
-
);
|
|
565
|
+
// dedupe
|
|
566
|
+
const deduped = dedupeTracks(allTracks);
|
|
575
567
|
|
|
576
|
-
|
|
577
|
-
|
|
568
|
+
// score + sort
|
|
569
|
+
const ranked = deduped
|
|
570
|
+
.map((track) => ({
|
|
571
|
+
track,
|
|
572
|
+
score: this.evaluateTrackMatch(track, query),
|
|
573
|
+
}))
|
|
574
|
+
.sort((a, b) => b.score.score - a.score.score);
|
|
578
575
|
|
|
579
|
-
|
|
580
|
-
if (allResults.size === 0 && errors.length > 0) {
|
|
581
|
-
// Try to get best result from allResults even if below min score
|
|
582
|
-
let fallbackResult: SearchResult | null = null;
|
|
583
|
-
let fallbackScore = -1;
|
|
584
|
-
|
|
585
|
-
for (const [source, result] of allResults) {
|
|
586
|
-
const score = result.score?.score || 0;
|
|
587
|
-
if (score > fallbackScore) {
|
|
588
|
-
fallbackScore = score;
|
|
589
|
-
fallbackResult = result;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
576
|
+
const tracks = ranked.map((x) => x.track);
|
|
592
577
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
578
|
+
const finalResult: SearchResult = {
|
|
579
|
+
query,
|
|
580
|
+
tracks,
|
|
581
|
+
source: "multi-search",
|
|
582
|
+
score: ranked[0]?.score,
|
|
583
|
+
};
|
|
597
584
|
|
|
598
|
-
|
|
599
|
-
const lastError = errors[errors.length - 1]?.error;
|
|
600
|
-
if (lastError) throw lastError;
|
|
601
|
-
}
|
|
585
|
+
this.setCachedSearch(query, requestedBy, finalResult);
|
|
602
586
|
|
|
603
|
-
|
|
587
|
+
this.debug(`[Search] Aggregated ${tracks.length} tracks from ${plugins.length} plugins`);
|
|
588
|
+
return finalResult;
|
|
604
589
|
}
|
|
605
|
-
|
|
606
590
|
/**
|
|
607
591
|
* Get plugin priority groups info for debugging
|
|
608
592
|
*/
|
package/src/structures/Player.ts
CHANGED
|
@@ -956,9 +956,10 @@ export class Player extends EventEmitter {
|
|
|
956
956
|
await this.applyTrackMiddleware(track);
|
|
957
957
|
const trackId = track.id || track.url || track.title;
|
|
958
958
|
const existingStream = this.streamManager.getStreamByTrack(trackId);
|
|
959
|
-
|
|
959
|
+
this.playbackMode = PlaybackMode.NATIVE;
|
|
960
960
|
if (existingStream && !existingStream.destroyed) {
|
|
961
961
|
this.debug(`[Stream] Using existing stream from manager for: ${track.title}`);
|
|
962
|
+
|
|
962
963
|
return { stream: existingStream, type: "arbitrary" };
|
|
963
964
|
}
|
|
964
965
|
|
|
@@ -1123,6 +1124,7 @@ export class Player extends EventEmitter {
|
|
|
1123
1124
|
const streamId = this.streamManager.registerStream(streamInfo.stream, track, {
|
|
1124
1125
|
source: track.source || "stream",
|
|
1125
1126
|
isPreload: false,
|
|
1127
|
+
isRemote: !!streamInfo?.remote,
|
|
1126
1128
|
priority: 10,
|
|
1127
1129
|
});
|
|
1128
1130
|
|
|
@@ -1796,8 +1798,9 @@ export class Player extends EventEmitter {
|
|
|
1796
1798
|
if (typeof index === "number" && index >= 0) {
|
|
1797
1799
|
for (let i = 0; i < index; i++) this.queue.remove(0);
|
|
1798
1800
|
}
|
|
1799
|
-
// signal the remote backend to stop;
|
|
1801
|
+
// signal the remote backend to stop;
|
|
1800
1802
|
void this.remoteHandle?.stop().catch((e) => this.debug("[Player] Remote skip:", e));
|
|
1803
|
+
this.playNext();
|
|
1801
1804
|
return true;
|
|
1802
1805
|
}
|
|
1803
1806
|
|
|
@@ -2666,7 +2669,7 @@ export class Player extends EventEmitter {
|
|
|
2666
2669
|
plugins: this.pluginManager.getAll().map((plugin) => plugin.name),
|
|
2667
2670
|
};
|
|
2668
2671
|
}
|
|
2669
|
-
|
|
2672
|
+
|
|
2670
2673
|
public exitRemoteMode(): void {
|
|
2671
2674
|
if (this.playbackMode !== PlaybackMode.REMOTE) return;
|
|
2672
2675
|
this.debug("[Player] Exiting REMOTE mode, restoring native playback");
|
|
@@ -12,6 +12,7 @@ export interface ManagedStream {
|
|
|
12
12
|
metadata: {
|
|
13
13
|
source: string;
|
|
14
14
|
isPreload: boolean;
|
|
15
|
+
isRemote: boolean;
|
|
15
16
|
priority: number;
|
|
16
17
|
};
|
|
17
18
|
listeners: {
|
|
@@ -128,6 +129,7 @@ export class StreamManager extends EventEmitter {
|
|
|
128
129
|
source: track.source || "unknown",
|
|
129
130
|
isPreload: metadata.isPreload || false,
|
|
130
131
|
priority: metadata.priority || 0,
|
|
132
|
+
isRemote: metadata.isRemote || false,
|
|
131
133
|
...metadata,
|
|
132
134
|
},
|
|
133
135
|
listeners,
|