@ziplayer/plugin 0.1.2 → 0.1.40
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/README.md +180 -180
- package/YTSR_README.md +310 -310
- package/dist/TTSPlugin.js +2 -0
- package/dist/TTSPlugin.js.map +1 -1
- package/dist/YTSRPlugin.d.ts.map +1 -1
- package/dist/YTSRPlugin.js +17 -1
- package/dist/YTSRPlugin.js.map +1 -1
- package/dist/YouTubePlugin.d.ts +15 -6
- package/dist/YouTubePlugin.d.ts.map +1 -1
- package/dist/YouTubePlugin.js +122 -47
- package/dist/YouTubePlugin.js.map +1 -1
- package/dist/utils/progress-bar.d.ts +8 -0
- package/dist/utils/progress-bar.d.ts.map +1 -0
- package/dist/utils/progress-bar.js +24 -0
- package/dist/utils/progress-bar.js.map +1 -0
- package/dist/utils/sabr-stream-factory.d.ts +25 -0
- package/dist/utils/sabr-stream-factory.d.ts.map +1 -0
- package/dist/utils/sabr-stream-factory.js +83 -0
- package/dist/utils/sabr-stream-factory.js.map +1 -0
- package/dist/utils/stream-converter.d.ts +10 -0
- package/dist/utils/stream-converter.d.ts.map +1 -0
- package/dist/utils/stream-converter.js +78 -0
- package/dist/utils/stream-converter.js.map +1 -0
- package/package.json +5 -3
- package/src/SpotifyPlugin.ts +312 -312
- package/src/TTSPlugin.ts +361 -361
- package/src/YTSRPlugin.ts +596 -583
- package/src/YouTubePlugin.ts +620 -528
- package/src/types/googlevideo.d.ts +45 -0
- package/src/utils/sabr-stream-factory.ts +96 -0
- package/src/utils/stream-converter.ts +87 -0
- package/tsconfig.json +1 -1
package/src/YTSRPlugin.ts
CHANGED
|
@@ -1,583 +1,596 @@
|
|
|
1
|
-
import { BasePlugin, Track, SearchResult } from "ziplayer";
|
|
2
|
-
import YouTube, { Video, Playlist, Channel } from "youtube-sr";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Plugin YTSR để tìm kiếm nâng cao trên YouTube mà không cần tạo stream.
|
|
6
|
-
*
|
|
7
|
-
* Plugin này cung cấp các tính năng tìm kiếm nâng cao cho YouTube bao gồm:
|
|
8
|
-
* - Tìm kiếm video với nhiều tùy chọn lọc
|
|
9
|
-
* - Tìm kiếm playlist và channel
|
|
10
|
-
* - Hỗ trợ các loại tìm kiếm khác nhau (video, playlist, channel)
|
|
11
|
-
* - Không tạo stream, chỉ trả về metadata
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* const ytsrPlugin = new YTSRPlugin();
|
|
15
|
-
*
|
|
16
|
-
* // Tìm kiếm video
|
|
17
|
-
* const videoResult = await ytsrPlugin.search("Never Gonna Give You Up", "user123");
|
|
18
|
-
*
|
|
19
|
-
* // Tìm kiếm playlist
|
|
20
|
-
* const playlistResult = await ytsrPlugin.searchPlaylist("chill music playlist", "user123");
|
|
21
|
-
*
|
|
22
|
-
* // Tìm kiếm channel
|
|
23
|
-
* const channelResult = await ytsrPlugin.searchChannel("PewDiePie", "user123");
|
|
24
|
-
*
|
|
25
|
-
* @since 1.0.0
|
|
26
|
-
*/
|
|
27
|
-
export class YTSRPlugin extends BasePlugin {
|
|
28
|
-
name = "ytsr";
|
|
29
|
-
version = "1.0.0";
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Tạo một instance mới của YTSRPlugin.
|
|
33
|
-
*
|
|
34
|
-
* Plugin này không cần khởi tạo bất kỳ client nào vì sử dụng youtube-sr
|
|
35
|
-
* để tìm kiếm thông tin từ YouTube.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* const plugin = new YTSRPlugin();
|
|
39
|
-
* // Plugin sẵn sàng sử dụng ngay lập tức
|
|
40
|
-
*/
|
|
41
|
-
constructor() {
|
|
42
|
-
super();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Xác định xem plugin có thể xử lý query này không.
|
|
47
|
-
*
|
|
48
|
-
* @param query - Query tìm kiếm hoặc URL để kiểm tra
|
|
49
|
-
* @returns `true` nếu plugin có thể xử lý query, `false` nếu không
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* plugin.canHandle("Never Gonna Give You Up"); // true
|
|
53
|
-
* plugin.canHandle("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
|
|
54
|
-
* plugin.canHandle("spotify:track:123"); // false
|
|
55
|
-
*/
|
|
56
|
-
canHandle(query: string): boolean {
|
|
57
|
-
const q = (query || "").trim().toLowerCase();
|
|
58
|
-
|
|
59
|
-
// Tránh xử lý các pattern rõ ràng cho các extractor khác
|
|
60
|
-
if (q.startsWith("tts:") || q.startsWith("say ")) return false;
|
|
61
|
-
if (q.startsWith("spotify:")
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (type === "
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* @
|
|
228
|
-
*
|
|
229
|
-
* @
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
* @
|
|
244
|
-
*
|
|
245
|
-
* @
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
*
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
* @
|
|
503
|
-
*
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
1
|
+
import { BasePlugin, Track, SearchResult } from "ziplayer";
|
|
2
|
+
import YouTube, { Video, Playlist, Channel } from "youtube-sr";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin YTSR để tìm kiếm nâng cao trên YouTube mà không cần tạo stream.
|
|
6
|
+
*
|
|
7
|
+
* Plugin này cung cấp các tính năng tìm kiếm nâng cao cho YouTube bao gồm:
|
|
8
|
+
* - Tìm kiếm video với nhiều tùy chọn lọc
|
|
9
|
+
* - Tìm kiếm playlist và channel
|
|
10
|
+
* - Hỗ trợ các loại tìm kiếm khác nhau (video, playlist, channel)
|
|
11
|
+
* - Không tạo stream, chỉ trả về metadata
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const ytsrPlugin = new YTSRPlugin();
|
|
15
|
+
*
|
|
16
|
+
* // Tìm kiếm video
|
|
17
|
+
* const videoResult = await ytsrPlugin.search("Never Gonna Give You Up", "user123");
|
|
18
|
+
*
|
|
19
|
+
* // Tìm kiếm playlist
|
|
20
|
+
* const playlistResult = await ytsrPlugin.searchPlaylist("chill music playlist", "user123");
|
|
21
|
+
*
|
|
22
|
+
* // Tìm kiếm channel
|
|
23
|
+
* const channelResult = await ytsrPlugin.searchChannel("PewDiePie", "user123");
|
|
24
|
+
*
|
|
25
|
+
* @since 1.0.0
|
|
26
|
+
*/
|
|
27
|
+
export class YTSRPlugin extends BasePlugin {
|
|
28
|
+
name = "ytsr";
|
|
29
|
+
version = "1.0.0";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tạo một instance mới của YTSRPlugin.
|
|
33
|
+
*
|
|
34
|
+
* Plugin này không cần khởi tạo bất kỳ client nào vì sử dụng youtube-sr
|
|
35
|
+
* để tìm kiếm thông tin từ YouTube.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const plugin = new YTSRPlugin();
|
|
39
|
+
* // Plugin sẵn sàng sử dụng ngay lập tức
|
|
40
|
+
*/
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Xác định xem plugin có thể xử lý query này không.
|
|
47
|
+
*
|
|
48
|
+
* @param query - Query tìm kiếm hoặc URL để kiểm tra
|
|
49
|
+
* @returns `true` nếu plugin có thể xử lý query, `false` nếu không
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* plugin.canHandle("Never Gonna Give You Up"); // true
|
|
53
|
+
* plugin.canHandle("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
|
|
54
|
+
* plugin.canHandle("spotify:track:123"); // false
|
|
55
|
+
*/
|
|
56
|
+
canHandle(query: string): boolean {
|
|
57
|
+
const q = (query || "").trim().toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Tránh xử lý các pattern rõ ràng cho các extractor khác
|
|
60
|
+
if (q.startsWith("tts:") || q.startsWith("say ")) return false;
|
|
61
|
+
if (q.startsWith("spotify:")) return false;
|
|
62
|
+
// If the query is a URL, check its hostname for Spotify domains
|
|
63
|
+
if (q.startsWith("http://") || q.startsWith("https://")) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(query);
|
|
66
|
+
const spotifyHosts = ["open.spotify.com", "play.spotify.com", "www.spotify.com"];
|
|
67
|
+
if (spotifyHosts.includes(parsed.hostname.toLowerCase())) return false;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
/* ignore */
|
|
70
|
+
}
|
|
71
|
+
} else if (q.includes("open.spotify.com")) {
|
|
72
|
+
// fallback for non-URL text queries
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (q.includes("soundcloud")) return false;
|
|
76
|
+
|
|
77
|
+
// Xử lý URL YouTube
|
|
78
|
+
if (q.startsWith("http://") || q.startsWith("https://")) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = new URL(query);
|
|
81
|
+
const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be"];
|
|
82
|
+
return allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Xử lý tất cả text khác như tìm kiếm YouTube
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Xác thực xem URL có phải là URL YouTube hợp lệ không.
|
|
94
|
+
*
|
|
95
|
+
* @param url - URL để xác thực
|
|
96
|
+
* @returns `true` nếu URL hợp lệ, `false` nếu không
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* plugin.validate("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); // true
|
|
100
|
+
* plugin.validate("https://youtu.be/dQw4w9WgXcQ"); // true
|
|
101
|
+
* plugin.validate("https://spotify.com/track/123"); // false
|
|
102
|
+
*/
|
|
103
|
+
validate(url: string): boolean {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(url);
|
|
106
|
+
const allowedHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "youtu.be", "www.youtu.be", "m.youtube.com"];
|
|
107
|
+
return allowedHosts.includes(parsed.hostname.toLowerCase());
|
|
108
|
+
} catch (e) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tìm kiếm video trên YouTube với các tùy chọn nâng cao.
|
|
115
|
+
*
|
|
116
|
+
* @param query - Query tìm kiếm
|
|
117
|
+
* @param requestedBy - ID của user yêu cầu tìm kiếm
|
|
118
|
+
* @param options - Tùy chọn tìm kiếm nâng cao
|
|
119
|
+
* @param options.limit - Số lượng kết quả tối đa (mặc định: 10)
|
|
120
|
+
* @param options.type - Loại tìm kiếm: "video", "playlist", "channel", "all" (mặc định: "video")
|
|
121
|
+
* @param options.duration - Lọc theo thời lượng: "short", "medium", "long", "all" (mặc định: "all")
|
|
122
|
+
* @param options.sortBy - Sắp xếp theo: "relevance", "uploadDate", "viewCount", "rating" (mặc định: "relevance")
|
|
123
|
+
* @param options.uploadDate - Lọc theo ngày upload: "hour", "today", "week", "month", "year", "all" (mặc định: "all")
|
|
124
|
+
* @returns SearchResult chứa các track được tìm thấy
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Tìm kiếm video cơ bản
|
|
128
|
+
* const result = await plugin.search("Never Gonna Give You Up", "user123");
|
|
129
|
+
*
|
|
130
|
+
* // Tìm kiếm với tùy chọn nâng cao
|
|
131
|
+
* const advancedResult = await plugin.search("chill music", "user123", {
|
|
132
|
+
* limit: 5,
|
|
133
|
+
* duration: "medium",
|
|
134
|
+
* sortBy: "viewCount",
|
|
135
|
+
* uploadDate: "month"
|
|
136
|
+
* });
|
|
137
|
+
*/
|
|
138
|
+
async search(
|
|
139
|
+
query: string,
|
|
140
|
+
requestedBy: string,
|
|
141
|
+
options: {
|
|
142
|
+
limit?: number;
|
|
143
|
+
type?: "video" | "playlist" | "channel" | "all";
|
|
144
|
+
duration?: "short" | "medium" | "long" | "all";
|
|
145
|
+
sortBy?: "relevance" | "uploadDate" | "viewCount" | "rating";
|
|
146
|
+
uploadDate?: "hour" | "today" | "week" | "month" | "year" | "all";
|
|
147
|
+
} = {},
|
|
148
|
+
): Promise<SearchResult> {
|
|
149
|
+
const { limit = 10, type = "video", duration = "all", sortBy = "relevance", uploadDate = "all" } = options;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Nếu là URL YouTube, xử lý như video đơn lẻ hoặc playlist Mix
|
|
153
|
+
if (this.validate(query)) {
|
|
154
|
+
const listId = this.extractListId(query);
|
|
155
|
+
if (listId && this.isMixListId(listId)) {
|
|
156
|
+
// Xử lý playlist Mix (RD)
|
|
157
|
+
return await this.handleMixPlaylistInternal(query, requestedBy, limit);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const videoId = this.extractVideoId(query);
|
|
161
|
+
if (videoId) {
|
|
162
|
+
const video = await this.getVideoByIdInternal(videoId);
|
|
163
|
+
if (video) {
|
|
164
|
+
return {
|
|
165
|
+
tracks: [this.buildTrackFromVideo(video, requestedBy)],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Tìm kiếm với youtube-sr
|
|
172
|
+
const searchOptions: any = {
|
|
173
|
+
limit,
|
|
174
|
+
type: type === "all" ? "all" : type,
|
|
175
|
+
safeSearch: false,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Thêm các filter nâng cao
|
|
179
|
+
if (duration !== "all") {
|
|
180
|
+
searchOptions.duration = duration;
|
|
181
|
+
}
|
|
182
|
+
if (sortBy !== "relevance") {
|
|
183
|
+
searchOptions.sortBy = sortBy;
|
|
184
|
+
}
|
|
185
|
+
if (uploadDate !== "all") {
|
|
186
|
+
searchOptions.uploadDate = uploadDate;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const results = await YouTube.search(query, searchOptions);
|
|
190
|
+
|
|
191
|
+
const tracks: Track[] = [];
|
|
192
|
+
|
|
193
|
+
// Xử lý kết quả dựa trên loại tìm kiếm
|
|
194
|
+
if (type === "video" || type === "all") {
|
|
195
|
+
const videos = results.filter((item: any): item is Video => item instanceof Video);
|
|
196
|
+
tracks.push(...videos.slice(0, limit).map((video: Video) => this.buildTrackFromVideo(video, requestedBy)));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (type === "playlist" || type === "all") {
|
|
200
|
+
const playlists = results.filter((item: any): item is Playlist => item instanceof Playlist);
|
|
201
|
+
// Chuyển đổi playlist thành tracks
|
|
202
|
+
playlists.slice(0, Math.floor(limit / 2)).forEach((playlist: any) => {
|
|
203
|
+
tracks.push(this.buildTrackFromPlaylist(playlist, requestedBy));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (type === "channel" || type === "all") {
|
|
208
|
+
const channels = results.filter((item: any): item is Channel => item instanceof Channel);
|
|
209
|
+
// Chuyển đổi channel thành tracks (lấy video mới nhất)
|
|
210
|
+
channels.slice(0, Math.floor(limit / 3)).forEach((channel: any) => {
|
|
211
|
+
tracks.push(this.buildTrackFromChannel(channel, requestedBy));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { tracks: tracks.slice(0, limit) };
|
|
216
|
+
} catch (error: any) {
|
|
217
|
+
throw new Error(`YTSR search failed: ${error?.message || error}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Tìm kiếm playlist trên YouTube.
|
|
223
|
+
*
|
|
224
|
+
* @param query - Query tìm kiếm playlist
|
|
225
|
+
* @param requestedBy - ID của user yêu cầu tìm kiếm
|
|
226
|
+
* @param limit - Số lượng playlist tối đa (mặc định: 5)
|
|
227
|
+
* @returns SearchResult chứa các playlist được tìm thấy
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* const playlists = await plugin.searchPlaylist("chill music playlist", "user123", 3);
|
|
231
|
+
* console.log(`Tìm thấy ${playlists.tracks.length} playlist`);
|
|
232
|
+
*/
|
|
233
|
+
async searchPlaylist(query: string, requestedBy: string, limit: number = 5): Promise<SearchResult> {
|
|
234
|
+
return this.search(query, requestedBy, { type: "playlist", limit });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Tìm kiếm channel trên YouTube.
|
|
239
|
+
*
|
|
240
|
+
* @param query - Query tìm kiếm channel
|
|
241
|
+
* @param requestedBy - ID của user yêu cầu tìm kiếm
|
|
242
|
+
* @param limit - Số lượng channel tối đa (mặc định: 5)
|
|
243
|
+
* @returns SearchResult chứa các channel được tìm thấy
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* const channels = await plugin.searchChannel("PewDiePie", "user123", 3);
|
|
247
|
+
* console.log(`Tìm thấy ${channels.tracks.length} channel`);
|
|
248
|
+
*/
|
|
249
|
+
async searchChannel(query: string, requestedBy: string, limit: number = 5): Promise<SearchResult> {
|
|
250
|
+
return this.search(query, requestedBy, { type: "channel", limit });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Tìm kiếm video theo ID cụ thể.
|
|
255
|
+
*
|
|
256
|
+
* @param videoId - ID của video YouTube
|
|
257
|
+
* @param requestedBy - ID của user yêu cầu
|
|
258
|
+
* @returns Track object của video
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const video = await plugin.getVideoById("dQw4w9WgXcQ", "user123");
|
|
262
|
+
* console.log(video.title);
|
|
263
|
+
*/
|
|
264
|
+
async getVideoById(videoId: string, requestedBy: string): Promise<Track | null> {
|
|
265
|
+
try {
|
|
266
|
+
const video = await this.getVideoByIdInternal(videoId);
|
|
267
|
+
return video ? this.buildTrackFromVideo(video, requestedBy) : null;
|
|
268
|
+
} catch (error: any) {
|
|
269
|
+
throw new Error(`Failed to get video by ID: ${error?.message || error}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Lấy thông tin chi tiết của video từ ID.
|
|
275
|
+
*
|
|
276
|
+
* @param videoId - ID của video YouTube
|
|
277
|
+
* @returns Video object hoặc null nếu không tìm thấy
|
|
278
|
+
*/
|
|
279
|
+
private async getVideoByIdInternal(videoId: string): Promise<Video | null> {
|
|
280
|
+
try {
|
|
281
|
+
const results = await YouTube.search(`https://www.youtube.com/watch?v=${videoId}`, { limit: 1, type: "video" });
|
|
282
|
+
const video = results.find((item: any): item is Video => item instanceof Video);
|
|
283
|
+
return video || null;
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Xây dựng Track object từ Video object của youtube-sr.
|
|
291
|
+
*
|
|
292
|
+
* @param video - Video object từ youtube-sr
|
|
293
|
+
* @param requestedBy - ID của user yêu cầu
|
|
294
|
+
* @returns Track object
|
|
295
|
+
*/
|
|
296
|
+
private buildTrackFromVideo(video: Video, requestedBy: string): Track {
|
|
297
|
+
// Xử lý duration một cách an toàn
|
|
298
|
+
let duration = 0;
|
|
299
|
+
if (video.duration) {
|
|
300
|
+
if (typeof video.duration === "number") {
|
|
301
|
+
duration = video.duration;
|
|
302
|
+
} else if (typeof video.duration === "object" && video.duration !== null) {
|
|
303
|
+
// Nếu duration là object, thử lấy seconds
|
|
304
|
+
duration = (video.duration as any)?.seconds || (video.duration as any)?.totalSeconds || 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// youtube-sr trả về duration theo milliseconds, chuyển thành seconds
|
|
309
|
+
if (duration > 0) {
|
|
310
|
+
// Nếu duration lớn hơn 1000, có thể là milliseconds
|
|
311
|
+
if (duration > 1000) {
|
|
312
|
+
duration = Math.floor(duration / 1000);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
id: video.id,
|
|
318
|
+
title: video.title,
|
|
319
|
+
url: video.url,
|
|
320
|
+
duration: Math.max(0, duration), // Đảm bảo duration không âm
|
|
321
|
+
thumbnail: video.thumbnail?.url || (video as any).thumbnails?.[0]?.url,
|
|
322
|
+
requestedBy,
|
|
323
|
+
source: this.name,
|
|
324
|
+
metadata: {
|
|
325
|
+
author: video.channel?.name,
|
|
326
|
+
views: video.views,
|
|
327
|
+
description: video.description,
|
|
328
|
+
publishedAt: video.uploadedAt,
|
|
329
|
+
channelUrl: video.channel?.url,
|
|
330
|
+
channelId: video.channel?.id,
|
|
331
|
+
},
|
|
332
|
+
} as Track;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Xây dựng Track object từ Playlist object của youtube-sr.
|
|
337
|
+
*
|
|
338
|
+
* @param playlist - Playlist object từ youtube-sr
|
|
339
|
+
* @param requestedBy - ID của user yêu cầu
|
|
340
|
+
* @returns Track object
|
|
341
|
+
*/
|
|
342
|
+
private buildTrackFromPlaylist(playlist: Playlist, requestedBy: string): Track {
|
|
343
|
+
return {
|
|
344
|
+
id: playlist.id,
|
|
345
|
+
title: playlist.title,
|
|
346
|
+
url: playlist.url,
|
|
347
|
+
duration: 0, // Playlist không có duration cố định
|
|
348
|
+
thumbnail: playlist.thumbnail?.url || (playlist as any).thumbnails?.[0]?.url,
|
|
349
|
+
requestedBy,
|
|
350
|
+
source: this.name,
|
|
351
|
+
metadata: {
|
|
352
|
+
author: playlist.channel?.name,
|
|
353
|
+
views: playlist.views,
|
|
354
|
+
description: (playlist as any).description,
|
|
355
|
+
publishedAt: (playlist as any).uploadedAt,
|
|
356
|
+
channelUrl: playlist.channel?.url,
|
|
357
|
+
channelId: playlist.channel?.id,
|
|
358
|
+
videoCount: playlist.videoCount,
|
|
359
|
+
type: "playlist",
|
|
360
|
+
},
|
|
361
|
+
} as Track;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Xây dựng Track object từ Channel object của youtube-sr.
|
|
366
|
+
*
|
|
367
|
+
* @param channel - Channel object từ youtube-sr
|
|
368
|
+
* @param requestedBy - ID của user yêu cầu
|
|
369
|
+
* @returns Track object
|
|
370
|
+
*/
|
|
371
|
+
private buildTrackFromChannel(channel: Channel, requestedBy: string): Track {
|
|
372
|
+
return {
|
|
373
|
+
id: channel.id,
|
|
374
|
+
title: channel.name,
|
|
375
|
+
url: channel.url,
|
|
376
|
+
duration: 0, // Channel không có duration
|
|
377
|
+
thumbnail: (channel as any).thumbnail?.url || (channel as any).thumbnails?.[0]?.url,
|
|
378
|
+
requestedBy,
|
|
379
|
+
source: this.name,
|
|
380
|
+
metadata: {
|
|
381
|
+
author: channel.name,
|
|
382
|
+
views: channel.subscribers,
|
|
383
|
+
description: (channel as any).description,
|
|
384
|
+
publishedAt: (channel as any).joinedAt,
|
|
385
|
+
channelUrl: channel.url,
|
|
386
|
+
channelId: channel.id,
|
|
387
|
+
subscriberCount: channel.subscribers,
|
|
388
|
+
type: "channel",
|
|
389
|
+
},
|
|
390
|
+
} as Track;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Xử lý playlist Mix (RD) của YouTube (internal).
|
|
395
|
+
*
|
|
396
|
+
* @param url - URL của playlist Mix
|
|
397
|
+
* @param requestedBy - ID của user yêu cầu
|
|
398
|
+
* @param limit - Số lượng track tối đa
|
|
399
|
+
* @returns SearchResult chứa các track từ Mix
|
|
400
|
+
*/
|
|
401
|
+
private async handleMixPlaylistInternal(url: string, requestedBy: string, limit: number): Promise<SearchResult> {
|
|
402
|
+
try {
|
|
403
|
+
const videoId = this.extractVideoId(url);
|
|
404
|
+
if (!videoId) {
|
|
405
|
+
throw new Error("Không thể trích xuất video ID từ URL Mix");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Lấy thông tin video gốc
|
|
409
|
+
const anchorVideo = await this.getVideoByIdInternal(videoId);
|
|
410
|
+
if (!anchorVideo) {
|
|
411
|
+
throw new Error("Không thể lấy thông tin video gốc");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Tìm kiếm các video liên quan để tạo Mix
|
|
415
|
+
const searchQuery = `${anchorVideo.title} ${anchorVideo.channel?.name || ""}`;
|
|
416
|
+
const searchResult = await this.search(searchQuery, requestedBy, {
|
|
417
|
+
limit: limit + 5, // Lấy thêm để có thể lọc
|
|
418
|
+
type: "video",
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Lọc và sắp xếp kết quả để tạo Mix
|
|
422
|
+
const mixTracks = searchResult.tracks
|
|
423
|
+
.filter((track) => track.id !== videoId) // Loại bỏ video gốc
|
|
424
|
+
.slice(0, limit);
|
|
425
|
+
|
|
426
|
+
// Thêm video gốc vào đầu Mix
|
|
427
|
+
const anchorTrack = this.buildTrackFromVideo(anchorVideo, requestedBy);
|
|
428
|
+
mixTracks.unshift(anchorTrack);
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
tracks: mixTracks.slice(0, limit),
|
|
432
|
+
playlist: {
|
|
433
|
+
name: `YouTube Mix - ${anchorVideo.title}`,
|
|
434
|
+
url: url,
|
|
435
|
+
thumbnail: anchorVideo.thumbnail?.url || (anchorVideo as any).thumbnails?.[0]?.url,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
} catch (error: any) {
|
|
439
|
+
throw new Error(`Failed to handle Mix playlist: ${error?.message || error}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Kiểm tra xem listId có phải là Mix playlist không.
|
|
445
|
+
*
|
|
446
|
+
* @param listId - ID của playlist
|
|
447
|
+
* @returns true nếu là Mix playlist
|
|
448
|
+
*/
|
|
449
|
+
private isMixListId(listId: string): boolean {
|
|
450
|
+
// YouTube Mix playlists thường bắt đầu với 'RD'
|
|
451
|
+
return typeof listId === "string" && listId.toUpperCase().startsWith("RD");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Trích xuất playlist ID từ URL YouTube.
|
|
456
|
+
*
|
|
457
|
+
* @param input - URL chứa playlist ID
|
|
458
|
+
* @returns Playlist ID hoặc null nếu không tìm thấy
|
|
459
|
+
*/
|
|
460
|
+
private extractListId(input: string): string | null {
|
|
461
|
+
try {
|
|
462
|
+
const u = new URL(input);
|
|
463
|
+
return u.searchParams.get("list");
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Trích xuất video ID từ URL YouTube.
|
|
471
|
+
*
|
|
472
|
+
* @param input - URL hoặc string chứa video ID
|
|
473
|
+
* @returns Video ID hoặc null nếu không tìm thấy
|
|
474
|
+
*/
|
|
475
|
+
private extractVideoId(input: string): string | null {
|
|
476
|
+
try {
|
|
477
|
+
const u = new URL(input);
|
|
478
|
+
const allowedShortHosts = ["youtu.be"];
|
|
479
|
+
const allowedLongHosts = ["youtube.com", "www.youtube.com", "music.youtube.com", "m.youtube.com"];
|
|
480
|
+
|
|
481
|
+
if (allowedShortHosts.includes(u.hostname)) {
|
|
482
|
+
return u.pathname.split("/").filter(Boolean)[0] || null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (allowedLongHosts.includes(u.hostname)) {
|
|
486
|
+
// watch?v=, shorts/, embed/
|
|
487
|
+
if (u.searchParams.get("v")) return u.searchParams.get("v");
|
|
488
|
+
const path = u.pathname;
|
|
489
|
+
if (path.startsWith("/shorts/")) return path.replace("/shorts/", "");
|
|
490
|
+
if (path.startsWith("/embed/")) return path.replace("/embed/", "");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return null;
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Plugin này không hỗ trợ tạo stream, chỉ dành cho tìm kiếm metadata.
|
|
501
|
+
*
|
|
502
|
+
* @param track - Track object
|
|
503
|
+
* @throws Error vì plugin này không hỗ trợ streaming
|
|
504
|
+
*/
|
|
505
|
+
async getStream(track: Track): Promise<any> {
|
|
506
|
+
throw new Error("YTSRPlugin không hỗ trợ streaming. Plugin này chỉ dành cho tìm kiếm metadata.");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Xử lý playlist Mix (RD) của YouTube.
|
|
511
|
+
*
|
|
512
|
+
* @param mixUrl - URL của playlist Mix YouTube
|
|
513
|
+
* @param requestedBy - ID của user yêu cầu
|
|
514
|
+
* @param limit - Số lượng track tối đa (mặc định: 10)
|
|
515
|
+
* @returns SearchResult chứa các track từ Mix playlist
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* const mixResult = await plugin.handleMixPlaylist(
|
|
519
|
+
* "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=RDMWGnHCaqxdU&start_radio=1",
|
|
520
|
+
* "user123",
|
|
521
|
+
* 15
|
|
522
|
+
* );
|
|
523
|
+
* console.log(`Mix playlist: ${mixResult.playlist?.name}`);
|
|
524
|
+
* console.log(`Tìm thấy ${mixResult.tracks.length} track trong Mix`);
|
|
525
|
+
*/
|
|
526
|
+
async handleMixPlaylist(mixUrl: string, requestedBy: string, limit: number = 10): Promise<SearchResult> {
|
|
527
|
+
return this.handleMixPlaylistInternal(mixUrl, requestedBy, limit);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Lấy các video liên quan cho một video YouTube cụ thể.
|
|
532
|
+
*
|
|
533
|
+
* @param trackURL - URL của video YouTube để lấy video liên quan
|
|
534
|
+
* @param opts - Tùy chọn cho việc lọc và giới hạn kết quả
|
|
535
|
+
* @param opts.limit - Số lượng video liên quan tối đa (mặc định: 5)
|
|
536
|
+
* @param opts.offset - Số lượng video bỏ qua từ đầu (mặc định: 0)
|
|
537
|
+
* @param opts.history - Mảng các track để loại trừ khỏi kết quả
|
|
538
|
+
* @returns Mảng các Track object liên quan
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* const related = await plugin.getRelatedTracks(
|
|
542
|
+
* "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
543
|
+
* { limit: 3, history: [currentTrack] }
|
|
544
|
+
* );
|
|
545
|
+
* console.log(`Tìm thấy ${related.length} video liên quan`);
|
|
546
|
+
*/
|
|
547
|
+
async getRelatedTracks(trackURL: string, opts: { limit?: number; offset?: number; history?: Track[] } = {}): Promise<Track[]> {
|
|
548
|
+
const { limit = 5, offset = 0, history = [] } = opts;
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
const videoId = this.extractVideoId(trackURL);
|
|
552
|
+
if (!videoId) {
|
|
553
|
+
throw new Error("Không thể trích xuất video ID từ URL");
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Tìm kiếm video liên quan bằng cách tìm kiếm với title của video hiện tại
|
|
557
|
+
const currentVideo = await this.getVideoByIdInternal(videoId);
|
|
558
|
+
if (!currentVideo) {
|
|
559
|
+
throw new Error("Không thể lấy thông tin video hiện tại");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Tìm kiếm các video liên quan dựa trên title và channel
|
|
563
|
+
const searchQuery = `${currentVideo.title} ${currentVideo.channel?.name || ""}`;
|
|
564
|
+
const searchResult = await this.search(searchQuery, "auto", {
|
|
565
|
+
limit: limit + offset + history.length,
|
|
566
|
+
type: "video",
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Lọc ra video hiện tại và các video trong history
|
|
570
|
+
const filteredTracks = searchResult.tracks.filter((track) => {
|
|
571
|
+
// Loại bỏ video hiện tại
|
|
572
|
+
if (track.id === videoId) return false;
|
|
573
|
+
|
|
574
|
+
// Loại bỏ các video trong history
|
|
575
|
+
if (history.some((h) => h.id === track.id || h.url === track.url)) return false;
|
|
576
|
+
|
|
577
|
+
return true;
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Áp dụng offset và limit
|
|
581
|
+
return filteredTracks.slice(offset, offset + limit);
|
|
582
|
+
} catch (error: any) {
|
|
583
|
+
throw new Error(`Failed to get related tracks: ${error?.message || error}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Plugin này không hỗ trợ fallback stream.
|
|
589
|
+
*
|
|
590
|
+
* @param track - Track object
|
|
591
|
+
* @throws Error vì plugin này không hỗ trợ streaming
|
|
592
|
+
*/
|
|
593
|
+
async getFallback(track: Track): Promise<any> {
|
|
594
|
+
throw new Error("YTSRPlugin không hỗ trợ fallback streaming. Plugin này chỉ dành cho tìm kiếm metadata.");
|
|
595
|
+
}
|
|
596
|
+
}
|