magmastream 2.10.3-alpha.5 → 2.10.3-alpha.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/structures/Enums.d.ts +2 -0
- package/dist/structures/Enums.js +2 -0
- package/dist/structures/Filters.d.ts +4 -1
- package/dist/structures/Filters.js +18 -1
- package/dist/structures/Manager.js +1 -0
- package/dist/structures/Node.d.ts +1 -0
- package/dist/structures/Node.js +131 -24
- package/dist/structures/Player.d.ts +5 -1
- package/dist/structures/Player.js +100 -11
- package/dist/structures/Rest.js +20 -2
- package/dist/structures/Types.d.ts +74 -7
- package/dist/structures/Utils.js +12 -2
- package/package.json +1 -1
package/dist/structures/Enums.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Manager } from "./Manager";
|
|
|
4
4
|
import { Player } from "./Player";
|
|
5
5
|
import { ChannelMixOptions, DistortionOptions, KaraokeOptions, LowPassOptions, ReverbOptions, RotationOptions, TimescaleOptions, TremoloOptions, VibratoOptions } from "./Types";
|
|
6
6
|
export declare class Filters {
|
|
7
|
+
private static readonly defaultPluginFilterPlugins;
|
|
7
8
|
distortion: DistortionOptions | null;
|
|
8
9
|
equalizer: Band[];
|
|
9
10
|
karaoke: KaraokeOptions | null;
|
|
@@ -46,6 +47,7 @@ export declare class Filters {
|
|
|
46
47
|
*/
|
|
47
48
|
private applyFilter;
|
|
48
49
|
private emitPlayersTasteUpdate;
|
|
50
|
+
private assertPluginFiltersSupported;
|
|
49
51
|
/**
|
|
50
52
|
* Sets the status of a specific filter.
|
|
51
53
|
*
|
|
@@ -141,9 +143,10 @@ export declare class Filters {
|
|
|
141
143
|
* Sets plugin-provided filter options on the audio.
|
|
142
144
|
*
|
|
143
145
|
* @param {Record<string, unknown>} [pluginFilters] - Plugin filter settings keyed by plugin filter name.
|
|
146
|
+
* @param {string | string[]} [requiredPluginNames] - The plugin name(s) that can handle the provided filters. Defaults to LavaDSPX.
|
|
144
147
|
* @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
|
|
145
148
|
*/
|
|
146
|
-
setPluginFilters(pluginFilters?: Record<string, unknown
|
|
149
|
+
setPluginFilters(pluginFilters?: Record<string, unknown>, requiredPluginNames?: string | string[]): Promise<this>;
|
|
147
150
|
/**
|
|
148
151
|
* Sets the own rotation options effect to the audio.
|
|
149
152
|
*
|
|
@@ -5,6 +5,7 @@ const filtersEqualizers_1 = require("../utils/filtersEqualizers");
|
|
|
5
5
|
const Enums_1 = require("./Enums");
|
|
6
6
|
const MagmastreamError_1 = require("./MagmastreamError");
|
|
7
7
|
class Filters {
|
|
8
|
+
static defaultPluginFilterPlugins = ["lavadspx-plugin", "LavaDSPX-Plugin"];
|
|
8
9
|
distortion;
|
|
9
10
|
equalizer;
|
|
10
11
|
karaoke;
|
|
@@ -113,6 +114,18 @@ class Filters {
|
|
|
113
114
|
details: { action: "change" },
|
|
114
115
|
});
|
|
115
116
|
}
|
|
117
|
+
assertPluginFiltersSupported(pluginNames) {
|
|
118
|
+
const requiredPlugins = Array.isArray(pluginNames) ? pluginNames : [pluginNames];
|
|
119
|
+
const availablePlugins = this.player.node.info?.plugins ?? [];
|
|
120
|
+
const hasRequiredPlugin = requiredPlugins.some((name) => availablePlugins.some((plugin) => plugin.name.toLowerCase() === name.toLowerCase()));
|
|
121
|
+
if (!hasRequiredPlugin) {
|
|
122
|
+
throw new MagmastreamError_1.MagmaStreamError({
|
|
123
|
+
code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
|
|
124
|
+
message: `One of the following plugins must be present in the lavalink node: ${requiredPlugins.map((name) => `"${name}"`).join(" or ")}.`,
|
|
125
|
+
context: { identifier: this.player.node.options.identifier },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
116
129
|
/**
|
|
117
130
|
* Sets the status of a specific filter.
|
|
118
131
|
*
|
|
@@ -274,11 +287,15 @@ class Filters {
|
|
|
274
287
|
* Sets plugin-provided filter options on the audio.
|
|
275
288
|
*
|
|
276
289
|
* @param {Record<string, unknown>} [pluginFilters] - Plugin filter settings keyed by plugin filter name.
|
|
290
|
+
* @param {string | string[]} [requiredPluginNames] - The plugin name(s) that can handle the provided filters. Defaults to LavaDSPX.
|
|
277
291
|
* @returns {Promise<this>} - Returns the current instance of the Filters class for method chaining.
|
|
278
292
|
*/
|
|
279
|
-
async setPluginFilters(pluginFilters) {
|
|
293
|
+
async setPluginFilters(pluginFilters, requiredPluginNames = Filters.defaultPluginFilterPlugins) {
|
|
280
294
|
const oldPlayer = { ...this };
|
|
281
295
|
const nextPluginFilters = pluginFilters ?? {};
|
|
296
|
+
if (Object.keys(nextPluginFilters).length > 0) {
|
|
297
|
+
this.assertPluginFiltersSupported(requiredPluginNames);
|
|
298
|
+
}
|
|
282
299
|
await this.applyFilter({ property: "pluginFilters", value: nextPluginFilters });
|
|
283
300
|
this.setFilterStatus(Enums_1.AvailableFilters.PluginFilters, Object.keys(nextPluginFilters).length > 0);
|
|
284
301
|
this.emitPlayersTasteUpdate(oldPlayer);
|
|
@@ -231,6 +231,7 @@ class Manager extends events_1.EventEmitter {
|
|
|
231
231
|
result = { loadType: lavalinkResponse.loadType, tracks };
|
|
232
232
|
break;
|
|
233
233
|
}
|
|
234
|
+
case Enums_1.LoadTypes.Episode:
|
|
234
235
|
case Enums_1.LoadTypes.Short:
|
|
235
236
|
case Enums_1.LoadTypes.Track: {
|
|
236
237
|
const track = Utils_1.TrackUtils.build(lavalinkResponse.data, requester);
|
|
@@ -158,6 +158,7 @@ export declare class Node {
|
|
|
158
158
|
* @private
|
|
159
159
|
*/
|
|
160
160
|
protected handleEvent(payload: PlayerEvent & PlayerEvents): Promise<void>;
|
|
161
|
+
private handleNodeLinkEvent;
|
|
161
162
|
/**
|
|
162
163
|
* Emitted when a new track starts playing.
|
|
163
164
|
* @param {Player} player The player that started playing the track.
|
package/dist/structures/Node.js
CHANGED
|
@@ -371,7 +371,10 @@ class Node {
|
|
|
371
371
|
* @param request - The incoming message.
|
|
372
372
|
*/
|
|
373
373
|
upgrade(request) {
|
|
374
|
-
|
|
374
|
+
const nodeLinkHeader = request.headers.iamnodelink ?? request.headers.isnodelink;
|
|
375
|
+
const isNodeLinkHeader = Array.isArray(nodeLinkHeader) ? nodeLinkHeader.some((value) => value === "true") : nodeLinkHeader === "true";
|
|
376
|
+
this.isNodeLink = Boolean(this.options.isNodeLink) || isNodeLinkHeader;
|
|
377
|
+
this.rest.isNodeLink = this.isNodeLink;
|
|
375
378
|
}
|
|
376
379
|
/**
|
|
377
380
|
* Handles the "open" event emitted by the WebSocket connection.
|
|
@@ -507,6 +510,8 @@ class Node {
|
|
|
507
510
|
this.sessionId = payload.sessionId;
|
|
508
511
|
await this.updateSessionId();
|
|
509
512
|
this.info = await this.fetchInfo();
|
|
513
|
+
this.isNodeLink = this.isNodeLink || Boolean(this.info.isNodelink);
|
|
514
|
+
this.rest.isNodeLink = this.isNodeLink;
|
|
510
515
|
if (payload.resumed || !hadPreviousSession) {
|
|
511
516
|
await this.manager.loadPlayerStates(this.options.identifier);
|
|
512
517
|
}
|
|
@@ -587,11 +592,71 @@ class Node {
|
|
|
587
592
|
this.lyricsLine(player, track, payload);
|
|
588
593
|
break;
|
|
589
594
|
default:
|
|
595
|
+
if (this.isNodeLink && this.handleNodeLinkEvent(player, payload))
|
|
596
|
+
return;
|
|
590
597
|
error = new Error(`Node#event unknown event '${type}'.`);
|
|
591
598
|
this.manager.emit(Enums_1.ManagerEventTypes.NodeError, this, error);
|
|
592
599
|
break;
|
|
593
600
|
}
|
|
594
601
|
}
|
|
602
|
+
handleNodeLinkEvent(player, payload) {
|
|
603
|
+
switch (payload.type) {
|
|
604
|
+
case "VolumeChangedEvent": {
|
|
605
|
+
if (typeof payload.volume === "number") {
|
|
606
|
+
const oldVolume = player.volume;
|
|
607
|
+
const oldPlayer = { ...player };
|
|
608
|
+
player.volume = payload.volume;
|
|
609
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
610
|
+
changeType: Enums_1.PlayerStateEventTypes.VolumeChange,
|
|
611
|
+
details: {
|
|
612
|
+
type: "volume",
|
|
613
|
+
action: "adjust",
|
|
614
|
+
previousVolume: oldVolume,
|
|
615
|
+
currentVolume: player.volume,
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
case "PauseEvent": {
|
|
622
|
+
if (typeof payload.paused === "boolean") {
|
|
623
|
+
const oldPaused = player.paused;
|
|
624
|
+
const oldPlayer = { ...player };
|
|
625
|
+
player.paused = payload.paused;
|
|
626
|
+
player.playing = !payload.paused;
|
|
627
|
+
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, player, {
|
|
628
|
+
changeType: Enums_1.PlayerStateEventTypes.PauseChange,
|
|
629
|
+
details: {
|
|
630
|
+
type: "pause",
|
|
631
|
+
action: payload.paused ? "pause" : "resume",
|
|
632
|
+
previousPause: oldPaused,
|
|
633
|
+
currentPause: player.paused,
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
case "SeekEvent":
|
|
640
|
+
if (typeof payload.position === "number")
|
|
641
|
+
player.position = payload.position;
|
|
642
|
+
return true;
|
|
643
|
+
case "PlayerCreatedEvent":
|
|
644
|
+
case "PlayerDestroyedEvent":
|
|
645
|
+
case "PlayerConnectedEvent":
|
|
646
|
+
case "PlayerReconnectingEvent":
|
|
647
|
+
case "ConnectionStatusEvent":
|
|
648
|
+
case "FiltersChangedEvent":
|
|
649
|
+
case "MixStartedEvent":
|
|
650
|
+
case "MixEndedEvent":
|
|
651
|
+
case "EternalBoxInfoEvent":
|
|
652
|
+
case "EternalBoxJumpEvent":
|
|
653
|
+
case "StreamMetadataEvent":
|
|
654
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[NODE] NodeLink player event for guild ${player.guildId}: ${Utils_1.JSONUtils.safe(payload, 2)}`);
|
|
655
|
+
return true;
|
|
656
|
+
default:
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
595
660
|
/**
|
|
596
661
|
* Emitted when a new track starts playing.
|
|
597
662
|
* @param {Player} player The player that started playing the track.
|
|
@@ -849,7 +914,7 @@ class Node {
|
|
|
849
914
|
});
|
|
850
915
|
}
|
|
851
916
|
if (this.isNodeLink) {
|
|
852
|
-
return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&
|
|
917
|
+
return (await this.rest.get(`/v4/loadlyrics?encodedTrack=${encodeURIComponent(track.track)}${language ? `&lang=${encodeURIComponent(language)}` : ""}`));
|
|
853
918
|
}
|
|
854
919
|
const requiredPlugins = ["lavalyrics-plugin"];
|
|
855
920
|
for (const plugin of requiredPlugins) {
|
|
@@ -886,13 +951,8 @@ class Node {
|
|
|
886
951
|
context: { identifier: this.options.identifier },
|
|
887
952
|
});
|
|
888
953
|
}
|
|
889
|
-
if (this.isNodeLink)
|
|
890
|
-
|
|
891
|
-
code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
|
|
892
|
-
message: `The node is a NodeLink, cannot subscribe to lyrics.`,
|
|
893
|
-
context: { identifier: this.options.identifier },
|
|
894
|
-
});
|
|
895
|
-
}
|
|
954
|
+
if (this.isNodeLink)
|
|
955
|
+
return await this.rest.post(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe?skipTrackSource=${skipTrackSource}`, {});
|
|
896
956
|
const requiredPlugins = ["lavalyrics-plugin"];
|
|
897
957
|
for (const plugin of requiredPlugins) {
|
|
898
958
|
if (!this.info.plugins.some((p) => p.name === plugin)) {
|
|
@@ -938,13 +998,8 @@ class Node {
|
|
|
938
998
|
context: { identifier: this.options.identifier },
|
|
939
999
|
});
|
|
940
1000
|
}
|
|
941
|
-
if (this.isNodeLink)
|
|
942
|
-
|
|
943
|
-
code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
|
|
944
|
-
message: `The node is a NodeLink, cannot unsubscribe from lyrics.`,
|
|
945
|
-
context: { identifier: this.options.identifier },
|
|
946
|
-
});
|
|
947
|
-
}
|
|
1001
|
+
if (this.isNodeLink)
|
|
1002
|
+
return await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${guildId}/lyrics/subscribe`);
|
|
948
1003
|
if (!this.info.plugins.some((plugin) => plugin.name === "java-lyrics-plugin")) {
|
|
949
1004
|
throw new MagmastreamError_1.MagmaStreamError({
|
|
950
1005
|
code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
|
|
@@ -1088,6 +1143,22 @@ class Node {
|
|
|
1088
1143
|
* @throws {RangeError} If the sponsorblock-plugin is not available in the Lavalink node.
|
|
1089
1144
|
*/
|
|
1090
1145
|
async getSponsorBlock(player) {
|
|
1146
|
+
if (this.isNodeLink) {
|
|
1147
|
+
try {
|
|
1148
|
+
const state = (await this.rest.get(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`));
|
|
1149
|
+
return state.categories.map((segment) => segment);
|
|
1150
|
+
}
|
|
1151
|
+
catch (err) {
|
|
1152
|
+
throw err instanceof MagmastreamError_1.MagmaStreamError
|
|
1153
|
+
? err
|
|
1154
|
+
: new MagmastreamError_1.MagmaStreamError({
|
|
1155
|
+
code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
|
|
1156
|
+
message: "Failed to fetch NodeLink SponsorBlock state.",
|
|
1157
|
+
cause: err instanceof Error ? err : undefined,
|
|
1158
|
+
context: { identifier: this.options.identifier, guildId: player.guildId },
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1091
1162
|
if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
|
|
1092
1163
|
throw new MagmastreamError_1.MagmaStreamError({
|
|
1093
1164
|
code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
|
|
@@ -1124,13 +1195,6 @@ class Node {
|
|
|
1124
1195
|
* ```
|
|
1125
1196
|
*/
|
|
1126
1197
|
async setSponsorBlock(player, segments = [Enums_1.SponsorBlockSegment.Sponsor, Enums_1.SponsorBlockSegment.SelfPromo]) {
|
|
1127
|
-
if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
|
|
1128
|
-
throw new MagmastreamError_1.MagmaStreamError({
|
|
1129
|
-
code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
|
|
1130
|
-
message: `The plugin "sponsorblock-plugin" must be present in the lavalink node to set SponsorBlock segments.`,
|
|
1131
|
-
context: { identifier: this.options.identifier, guildId: player.guildId },
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
1198
|
if (!segments.length) {
|
|
1135
1199
|
throw new MagmastreamError_1.MagmaStreamError({
|
|
1136
1200
|
code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
|
|
@@ -1145,8 +1209,35 @@ class Node {
|
|
|
1145
1209
|
context: { identifier: this.options.identifier, guildId: player.guildId, invalidSegments: segments },
|
|
1146
1210
|
});
|
|
1147
1211
|
}
|
|
1212
|
+
const categories = segments.map((segment) => segment.toLowerCase());
|
|
1213
|
+
if (this.isNodeLink) {
|
|
1214
|
+
try {
|
|
1215
|
+
await this.rest.patch(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`, {
|
|
1216
|
+
enabled: true,
|
|
1217
|
+
categories,
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
throw err instanceof MagmastreamError_1.MagmaStreamError
|
|
1223
|
+
? err
|
|
1224
|
+
: new MagmastreamError_1.MagmaStreamError({
|
|
1225
|
+
code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
|
|
1226
|
+
message: "Failed to set NodeLink SponsorBlock categories.",
|
|
1227
|
+
cause: err instanceof Error ? err : undefined,
|
|
1228
|
+
context: { identifier: this.options.identifier, guildId: player.guildId, segments },
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
|
|
1233
|
+
throw new MagmastreamError_1.MagmaStreamError({
|
|
1234
|
+
code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
|
|
1235
|
+
message: `The plugin "sponsorblock-plugin" must be present in the lavalink node to set SponsorBlock segments.`,
|
|
1236
|
+
context: { identifier: this.options.identifier, guildId: player.guildId },
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1148
1239
|
try {
|
|
1149
|
-
await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`,
|
|
1240
|
+
await this.rest.put(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock/categories`, categories);
|
|
1150
1241
|
}
|
|
1151
1242
|
catch (err) {
|
|
1152
1243
|
throw err instanceof MagmastreamError_1.MagmaStreamError
|
|
@@ -1166,6 +1257,22 @@ class Node {
|
|
|
1166
1257
|
* @throws {RangeError} If the sponsorblock-plugin is not available in the Lavalink node.
|
|
1167
1258
|
*/
|
|
1168
1259
|
async deleteSponsorBlock(player) {
|
|
1260
|
+
if (this.isNodeLink) {
|
|
1261
|
+
try {
|
|
1262
|
+
await this.rest.delete(`/v4/sessions/${this.sessionId}/players/${player.guildId}/sponsorblock`);
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
catch (err) {
|
|
1266
|
+
throw err instanceof MagmastreamError_1.MagmaStreamError
|
|
1267
|
+
? err
|
|
1268
|
+
: new MagmastreamError_1.MagmaStreamError({
|
|
1269
|
+
code: Enums_1.MagmaStreamErrorCode.NODE_PROTOCOL_ERROR,
|
|
1270
|
+
message: "Failed to delete NodeLink SponsorBlock state.",
|
|
1271
|
+
cause: err instanceof Error ? err : undefined,
|
|
1272
|
+
context: { identifier: this.options.identifier, guildId: player.guildId },
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1169
1276
|
if (!this.info.plugins.some((plugin) => plugin.name === "sponsorblock-plugin")) {
|
|
1170
1277
|
throw new MagmastreamError_1.MagmaStreamError({
|
|
1171
1278
|
code: Enums_1.MagmaStreamErrorCode.NODE_PLUGIN_ERROR,
|
|
@@ -58,6 +58,7 @@ export declare class Player {
|
|
|
58
58
|
protected voiceReceiverReconnectTimeout?: NodeJS.Timeout;
|
|
59
59
|
protected voiceReceiverAttempt: number;
|
|
60
60
|
protected voiceReceiverReconnectTries: number;
|
|
61
|
+
private readonly voiceReceiverStreams;
|
|
61
62
|
/**
|
|
62
63
|
* Creates a new player, returns one if it already exists.
|
|
63
64
|
* @param options The player options.
|
|
@@ -329,10 +330,13 @@ export declare class Player {
|
|
|
329
330
|
private openVoiceReceiver;
|
|
330
331
|
/**
|
|
331
332
|
* Handles a voice receiver message.
|
|
332
|
-
* @param {
|
|
333
|
+
* @param {RawData} payload - The payload to handle.
|
|
333
334
|
* @returns {Promise<void>} - A promise that resolves when the voice receiver message is handled.
|
|
334
335
|
*/
|
|
335
336
|
private onVoiceReceiverMessage;
|
|
337
|
+
private toVoiceReceiverBuffer;
|
|
338
|
+
private parseNodeLinkVoiceFrame;
|
|
339
|
+
private handleNodeLinkVoiceFrame;
|
|
336
340
|
/**
|
|
337
341
|
* Handles a voice receiver error.
|
|
338
342
|
* @param {Error} error - The error to handle.
|
|
@@ -61,11 +61,12 @@ class Player {
|
|
|
61
61
|
dynamicRepeatIntervalMs = null;
|
|
62
62
|
static _manager;
|
|
63
63
|
/** Should only be used when the node is a NodeLink */
|
|
64
|
-
voiceReceiverWsClient;
|
|
65
|
-
isConnectToVoiceReceiver;
|
|
64
|
+
voiceReceiverWsClient = null;
|
|
65
|
+
isConnectToVoiceReceiver = false;
|
|
66
66
|
voiceReceiverReconnectTimeout;
|
|
67
|
-
voiceReceiverAttempt;
|
|
68
|
-
voiceReceiverReconnectTries;
|
|
67
|
+
voiceReceiverAttempt = 1;
|
|
68
|
+
voiceReceiverReconnectTries = 30;
|
|
69
|
+
voiceReceiverStreams = new Map();
|
|
69
70
|
/**
|
|
70
71
|
* Creates a new player, returns one if it already exists.
|
|
71
72
|
* @param options The player options.
|
|
@@ -270,6 +271,7 @@ class Player {
|
|
|
270
271
|
this.voiceReceiverWsClient.close();
|
|
271
272
|
this.voiceReceiverWsClient = null;
|
|
272
273
|
}
|
|
274
|
+
this.voiceReceiverStreams.clear();
|
|
273
275
|
if (disconnect) {
|
|
274
276
|
await this.disconnect().catch(() => { });
|
|
275
277
|
}
|
|
@@ -1040,10 +1042,11 @@ class Player {
|
|
|
1040
1042
|
"Client-Name": this.manager.options.clientName,
|
|
1041
1043
|
};
|
|
1042
1044
|
const { host, useSSL, port } = this.node.options;
|
|
1043
|
-
this.
|
|
1045
|
+
this.voiceReceiverReconnectTries = this.node.options.maxRetryAttempts ?? this.voiceReceiverReconnectTries;
|
|
1046
|
+
this.voiceReceiverWsClient = new ws_1.WebSocket(`${useSSL ? "wss" : "ws"}://${host}:${port}/v4/websocket/voice/${this.guildId}`, { headers });
|
|
1044
1047
|
this.voiceReceiverWsClient.on("open", () => this.openVoiceReceiver());
|
|
1045
1048
|
this.voiceReceiverWsClient.on("error", (err) => this.onVoiceReceiverError(err));
|
|
1046
|
-
this.voiceReceiverWsClient.on("message", (rawMessage) => this.onVoiceReceiverMessage(rawMessage
|
|
1049
|
+
this.voiceReceiverWsClient.on("message", (rawMessage) => this.onVoiceReceiverMessage(rawMessage));
|
|
1047
1050
|
this.voiceReceiverWsClient.on("close", (code, reason) => this.closeVoiceReceiver(code, reason.toString()));
|
|
1048
1051
|
}
|
|
1049
1052
|
/**
|
|
@@ -1065,6 +1068,7 @@ class Player {
|
|
|
1065
1068
|
this.voiceReceiverWsClient = null;
|
|
1066
1069
|
}
|
|
1067
1070
|
this.isConnectToVoiceReceiver = false;
|
|
1071
|
+
this.voiceReceiverStreams.clear();
|
|
1068
1072
|
}
|
|
1069
1073
|
/**
|
|
1070
1074
|
* Closes the voice receiver for the player.
|
|
@@ -1108,6 +1112,8 @@ class Player {
|
|
|
1108
1112
|
this.voiceReceiverWsClient?.close(1000, "destroy");
|
|
1109
1113
|
this.voiceReceiverWsClient?.removeAllListeners();
|
|
1110
1114
|
this.voiceReceiverWsClient = null;
|
|
1115
|
+
this.voiceReceiverStreams.clear();
|
|
1116
|
+
this.isConnectToVoiceReceiver = false;
|
|
1111
1117
|
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[PLAYER] Disconnected from voice receiver for player ${this.guildId}`);
|
|
1112
1118
|
this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverDisconnect, this);
|
|
1113
1119
|
}
|
|
@@ -1125,14 +1131,28 @@ class Player {
|
|
|
1125
1131
|
}
|
|
1126
1132
|
/**
|
|
1127
1133
|
* Handles a voice receiver message.
|
|
1128
|
-
* @param {
|
|
1134
|
+
* @param {RawData} payload - The payload to handle.
|
|
1129
1135
|
* @returns {Promise<void>} - A promise that resolves when the voice receiver message is handled.
|
|
1130
1136
|
*/
|
|
1131
1137
|
async onVoiceReceiverMessage(payload) {
|
|
1132
|
-
const
|
|
1138
|
+
const buffer = this.toVoiceReceiverBuffer(payload);
|
|
1139
|
+
const parsedFrame = this.parseNodeLinkVoiceFrame(buffer);
|
|
1140
|
+
if (parsedFrame) {
|
|
1141
|
+
this.handleNodeLinkVoiceFrame(parsedFrame);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const textPayload = buffer.toString();
|
|
1145
|
+
let packet;
|
|
1146
|
+
try {
|
|
1147
|
+
packet = JSON.parse(textPayload);
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown binary payload for player ${this.guildId}`);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1133
1153
|
if (!packet?.op)
|
|
1134
1154
|
return;
|
|
1135
|
-
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(
|
|
1155
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved a payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
|
|
1136
1156
|
switch (packet.type) {
|
|
1137
1157
|
case "startSpeakingEvent": {
|
|
1138
1158
|
this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, packet.data);
|
|
@@ -1141,17 +1161,86 @@ class Player {
|
|
|
1141
1161
|
case "endSpeakingEvent": {
|
|
1142
1162
|
const data = {
|
|
1143
1163
|
...packet.data,
|
|
1144
|
-
data: Buffer.from(packet.data.data, "base64"),
|
|
1164
|
+
data: Buffer.isBuffer(packet.data.data) ? packet.data.data : Buffer.from(packet.data.data, "base64"),
|
|
1145
1165
|
};
|
|
1146
1166
|
this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverEndSpeaking, this, data);
|
|
1147
1167
|
break;
|
|
1148
1168
|
}
|
|
1149
1169
|
default: {
|
|
1150
|
-
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(
|
|
1170
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `VoiceReceiver recieved an unknown payload: ${Utils_1.JSONUtils.safe(textPayload, 2)}`);
|
|
1151
1171
|
break;
|
|
1152
1172
|
}
|
|
1153
1173
|
}
|
|
1154
1174
|
}
|
|
1175
|
+
toVoiceReceiverBuffer(payload) {
|
|
1176
|
+
if (Buffer.isBuffer(payload))
|
|
1177
|
+
return payload;
|
|
1178
|
+
if (payload instanceof ArrayBuffer)
|
|
1179
|
+
return Buffer.from(payload);
|
|
1180
|
+
return Buffer.concat(payload);
|
|
1181
|
+
}
|
|
1182
|
+
parseNodeLinkVoiceFrame(payload) {
|
|
1183
|
+
if (payload.length < 12)
|
|
1184
|
+
return null;
|
|
1185
|
+
let offset = 0;
|
|
1186
|
+
const op = payload.readUInt8(offset++);
|
|
1187
|
+
if (op < 1 || op > 3)
|
|
1188
|
+
return null;
|
|
1189
|
+
const format = payload.readUInt8(offset++);
|
|
1190
|
+
const guildLength = payload.readUInt8(offset++);
|
|
1191
|
+
if (offset + guildLength > payload.length)
|
|
1192
|
+
return null;
|
|
1193
|
+
const guildId = payload.toString("utf8", offset, offset + guildLength);
|
|
1194
|
+
offset += guildLength;
|
|
1195
|
+
if (offset >= payload.length)
|
|
1196
|
+
return null;
|
|
1197
|
+
const userLength = payload.readUInt8(offset++);
|
|
1198
|
+
if (offset + userLength + 8 > payload.length)
|
|
1199
|
+
return null;
|
|
1200
|
+
const userId = payload.toString("utf8", offset, offset + userLength);
|
|
1201
|
+
offset += userLength;
|
|
1202
|
+
const ssrc = payload.readUInt32BE(offset);
|
|
1203
|
+
offset += 4;
|
|
1204
|
+
const timestamp = payload.readUInt32BE(offset);
|
|
1205
|
+
offset += 4;
|
|
1206
|
+
const type = format === 2 ? "pcm" : format === 1 ? "ogg" : "opus";
|
|
1207
|
+
return { op, type, guildId, userId, ssrc, timestamp, data: payload.subarray(offset) };
|
|
1208
|
+
}
|
|
1209
|
+
handleNodeLinkVoiceFrame(frame) {
|
|
1210
|
+
const key = `${frame.guildId}:${frame.ssrc}`;
|
|
1211
|
+
if (frame.op === 1) {
|
|
1212
|
+
this.voiceReceiverStreams.set(key, { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] });
|
|
1213
|
+
this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverStartSpeaking, this, {
|
|
1214
|
+
userId: frame.userId,
|
|
1215
|
+
guildId: frame.guildId,
|
|
1216
|
+
ssrc: frame.ssrc,
|
|
1217
|
+
type: frame.type,
|
|
1218
|
+
timestamp: frame.timestamp,
|
|
1219
|
+
});
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (frame.op === 3) {
|
|
1223
|
+
const stream = this.voiceReceiverStreams.get(key) ?? { guildId: frame.guildId, userId: frame.userId, type: frame.type, chunks: [] };
|
|
1224
|
+
stream.chunks.push(frame.data);
|
|
1225
|
+
this.voiceReceiverStreams.set(key, stream);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
if (frame.op === 2) {
|
|
1229
|
+
const stream = this.voiceReceiverStreams.get(key);
|
|
1230
|
+
const chunks = stream?.chunks ?? [];
|
|
1231
|
+
if (frame.data.length > 0)
|
|
1232
|
+
chunks.push(frame.data);
|
|
1233
|
+
this.voiceReceiverStreams.delete(key);
|
|
1234
|
+
this.manager.emit(Enums_1.ManagerEventTypes.VoiceReceiverEndSpeaking, this, {
|
|
1235
|
+
userId: stream?.userId ?? frame.userId,
|
|
1236
|
+
guildId: stream?.guildId ?? frame.guildId,
|
|
1237
|
+
data: Buffer.concat(chunks),
|
|
1238
|
+
type: stream?.type ?? frame.type,
|
|
1239
|
+
ssrc: frame.ssrc,
|
|
1240
|
+
timestamp: frame.timestamp,
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1155
1244
|
/**
|
|
1156
1245
|
* Handles a voice receiver error.
|
|
1157
1246
|
* @param {Error} error - The error to handle.
|
package/dist/structures/Rest.js
CHANGED
|
@@ -64,8 +64,26 @@ class Rest {
|
|
|
64
64
|
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[REST] Skipping getPlayers - no session ID on node ${this.node.options.identifier}`);
|
|
65
65
|
return [];
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
try {
|
|
68
|
+
const result = await this.get(`/v4/sessions/${this.sessionId}/players`);
|
|
69
|
+
return Array.isArray(result) ? result : (result?.players ?? []);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const shouldFallback = err instanceof MagmastreamError_1.MagmaStreamError && err.code === Enums_1.MagmaStreamErrorCode.REST_REQUEST_FAILED && err.message.includes("(400)");
|
|
73
|
+
if (!shouldFallback)
|
|
74
|
+
throw err;
|
|
75
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[REST] getPlayers bulk endpoint failed on node ${this.node.options.identifier}; falling back to individual player fetches.`);
|
|
76
|
+
const players = this.manager.players.filter((player) => player.node.options.identifier === this.node.options.identifier);
|
|
77
|
+
const results = await Promise.all(players.map(async (player) => {
|
|
78
|
+
try {
|
|
79
|
+
return await this.getPlayer(player.guildId);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
return results.filter((player) => player !== null);
|
|
86
|
+
}
|
|
69
87
|
}
|
|
70
88
|
/**
|
|
71
89
|
* Sends a PATCH request to update player related data.
|
|
@@ -447,7 +447,7 @@ export interface TrackData {
|
|
|
447
447
|
/** Additional track info provided by plugins. */
|
|
448
448
|
pluginInfo: Record<string, unknown>;
|
|
449
449
|
/** Additional track data sent back from Lavalink. */
|
|
450
|
-
userData
|
|
450
|
+
userData?: Record<string, unknown>;
|
|
451
451
|
}
|
|
452
452
|
/**
|
|
453
453
|
* Playlist Raw Data
|
|
@@ -516,6 +516,15 @@ export interface TrackSearchResult {
|
|
|
516
516
|
/** The track obtained */
|
|
517
517
|
tracks: [Track];
|
|
518
518
|
}
|
|
519
|
+
/**
|
|
520
|
+
* Episode Search Result
|
|
521
|
+
*/
|
|
522
|
+
export interface EpisodeSearchResult {
|
|
523
|
+
/** The load type is always 'episode' */
|
|
524
|
+
loadType: LoadTypes.Episode;
|
|
525
|
+
/** The episode obtained */
|
|
526
|
+
tracks: [Track];
|
|
527
|
+
}
|
|
519
528
|
/**
|
|
520
529
|
* Search Result
|
|
521
530
|
*/
|
|
@@ -815,6 +824,31 @@ export interface SponsorBlockChapterStarted extends PlayerEvent {
|
|
|
815
824
|
duration: number;
|
|
816
825
|
};
|
|
817
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* NodeLink SponsorBlock segment data.
|
|
829
|
+
*/
|
|
830
|
+
export interface NodeLinkSponsorBlockSegment {
|
|
831
|
+
uuid: string;
|
|
832
|
+
start: number;
|
|
833
|
+
end: number;
|
|
834
|
+
category: string;
|
|
835
|
+
actionType: string;
|
|
836
|
+
votes: number;
|
|
837
|
+
locked: boolean;
|
|
838
|
+
videoDuration: number;
|
|
839
|
+
description: string;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* NodeLink SponsorBlock state.
|
|
843
|
+
*/
|
|
844
|
+
export interface NodeLinkSponsorBlockState {
|
|
845
|
+
enabled: boolean;
|
|
846
|
+
categories: string[];
|
|
847
|
+
actionTypes: string[];
|
|
848
|
+
segments: NodeLinkSponsorBlockSegment[];
|
|
849
|
+
lastSkippedUuid: string | null;
|
|
850
|
+
skipMarginMs: number;
|
|
851
|
+
}
|
|
818
852
|
/**
|
|
819
853
|
* SponsorBlockChaptersLoaded interface
|
|
820
854
|
*/
|
|
@@ -898,13 +932,21 @@ export interface LavalinkInfo {
|
|
|
898
932
|
commit: string;
|
|
899
933
|
commitTime: number;
|
|
900
934
|
};
|
|
901
|
-
jvm
|
|
902
|
-
lavaplayer
|
|
935
|
+
jvm?: string;
|
|
936
|
+
lavaplayer?: string;
|
|
937
|
+
node?: string;
|
|
938
|
+
voice?: {
|
|
939
|
+
name: string;
|
|
940
|
+
version: string;
|
|
941
|
+
};
|
|
942
|
+
isNodelink?: boolean;
|
|
903
943
|
sourceManagers: string[];
|
|
904
944
|
filters: string[];
|
|
905
945
|
plugins: {
|
|
906
946
|
name: string;
|
|
907
947
|
version: string;
|
|
948
|
+
author?: string | null;
|
|
949
|
+
path?: string | null;
|
|
908
950
|
}[];
|
|
909
951
|
}
|
|
910
952
|
/**
|
|
@@ -958,6 +1000,21 @@ export interface NodeLinkGetLyricsMultiple {
|
|
|
958
1000
|
loadType: "lyricsMultiple";
|
|
959
1001
|
data: NodeLinkGetLyricsData[];
|
|
960
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* NodeLink Get Lyrics interface
|
|
1005
|
+
*/
|
|
1006
|
+
export interface NodeLinkGetLyricsLyrics {
|
|
1007
|
+
loadType: "lyrics";
|
|
1008
|
+
data: {
|
|
1009
|
+
provider: string;
|
|
1010
|
+
lines: {
|
|
1011
|
+
time: number;
|
|
1012
|
+
duration?: number;
|
|
1013
|
+
text: string;
|
|
1014
|
+
words?: Record<string, unknown>[];
|
|
1015
|
+
}[];
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
961
1018
|
/**
|
|
962
1019
|
* NodeLink Get Lyrics Empty interface
|
|
963
1020
|
*/
|
|
@@ -1009,6 +1066,12 @@ export interface StartSpeakingEventVoiceReceiverData {
|
|
|
1009
1066
|
* The guild ID of the guild where the user started speaking.
|
|
1010
1067
|
*/
|
|
1011
1068
|
guildId: string;
|
|
1069
|
+
/** The SSRC identifier of the speaking user. */
|
|
1070
|
+
ssrc?: number;
|
|
1071
|
+
/** The audio frame type sent by NodeLink. */
|
|
1072
|
+
type?: "opus" | "pcm" | "ogg";
|
|
1073
|
+
/** The timestamp sent by NodeLink for the start frame. */
|
|
1074
|
+
timestamp?: number;
|
|
1012
1075
|
}
|
|
1013
1076
|
/**
|
|
1014
1077
|
* End Speaking Event Voice Receiver Data interface
|
|
@@ -1025,11 +1088,15 @@ export interface EndSpeakingEventVoiceReceiverData {
|
|
|
1025
1088
|
/**
|
|
1026
1089
|
* The audio data received from the user in base64.
|
|
1027
1090
|
*/
|
|
1028
|
-
data: string;
|
|
1091
|
+
data: string | Buffer;
|
|
1029
1092
|
/**
|
|
1030
1093
|
* The type of the audio data. Can be either opus or pcm. Older versions may include ogg/opus.
|
|
1031
1094
|
*/
|
|
1032
|
-
type: "opus" | "pcm";
|
|
1095
|
+
type: "opus" | "pcm" | "ogg";
|
|
1096
|
+
/** The SSRC identifier of the speaking user. */
|
|
1097
|
+
ssrc?: number;
|
|
1098
|
+
/** The timestamp sent by NodeLink for the final frame. */
|
|
1099
|
+
timestamp?: number;
|
|
1033
1100
|
}
|
|
1034
1101
|
/**
|
|
1035
1102
|
* Base Voice Receiver Event interface
|
|
@@ -1405,7 +1472,7 @@ export type LoadType = keyof typeof LoadTypes;
|
|
|
1405
1472
|
/**
|
|
1406
1473
|
* NodeLink Get Lyrics Enum type
|
|
1407
1474
|
*/
|
|
1408
|
-
export type NodeLinkGetLyrics = NodeLinkGetLyricsSingle | NodeLinkGetLyricsMultiple | NodeLinkGetLyricsEmpty | NodeLinkGetLyricsError;
|
|
1475
|
+
export type NodeLinkGetLyrics = NodeLinkGetLyricsLyrics | NodeLinkGetLyricsSingle | NodeLinkGetLyricsMultiple | NodeLinkGetLyricsEmpty | NodeLinkGetLyricsError;
|
|
1409
1476
|
/**
|
|
1410
1477
|
* Voice Receiver Event Enum type
|
|
1411
1478
|
*/
|
|
@@ -1413,7 +1480,7 @@ export type VoiceReceiverEvent = StartSpeakingEventVoiceReceiver | EndSpeakingEv
|
|
|
1413
1480
|
/**
|
|
1414
1481
|
* Search Result Enum type
|
|
1415
1482
|
*/
|
|
1416
|
-
export type SearchResult = TrackSearchResult | SearchSearchResult | PlaylistSearchResult | ErrorOrEmptySearchResult | AlbumSearchResult | ArtistSearchResult | StationSearchResult | PodcastSearchResult | ShowSearchResult | ShortSearchResult;
|
|
1483
|
+
export type SearchResult = TrackSearchResult | EpisodeSearchResult | SearchSearchResult | PlaylistSearchResult | ErrorOrEmptySearchResult | AlbumSearchResult | ArtistSearchResult | StationSearchResult | PodcastSearchResult | ShowSearchResult | ShortSearchResult;
|
|
1417
1484
|
/**
|
|
1418
1485
|
* Lyrics Event Enum type
|
|
1419
1486
|
*/
|
package/dist/structures/Utils.js
CHANGED
|
@@ -228,7 +228,14 @@ class AutoPlayUtils {
|
|
|
228
228
|
// Iterate over autoplay platforms in order of priority
|
|
229
229
|
for (const platform of autoPlaySearchPlatforms) {
|
|
230
230
|
if (enabledSources.includes(platform)) {
|
|
231
|
-
|
|
231
|
+
let recommendedTracks;
|
|
232
|
+
try {
|
|
233
|
+
recommendedTracks = await this.getRecommendedTracksFromSource(track, platform);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[AUTOPLAY] Failed to fetch recommendations from ${platform}: ${error instanceof Error ? error.message : String(error)}`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
232
239
|
// If tracks are found, return them immediately
|
|
233
240
|
if (recommendedTracks.length > 0) {
|
|
234
241
|
return recommendedTracks;
|
|
@@ -336,7 +343,7 @@ class AutoPlayUtils {
|
|
|
336
343
|
// return match ? match[1] : null;
|
|
337
344
|
// };
|
|
338
345
|
// const identifier = `sprec:seed_artists=${extractSpotifyArtistID(track.pluginInfo.artistUrl)}&seed_tracks=${track.identifier}`;
|
|
339
|
-
const identifier = `sprec:mix:track:${track.identifier}`;
|
|
346
|
+
const identifier = this.manager.useableNode.isNodeLink ? `sprec:seed_tracks=${track.identifier}` : `sprec:mix:track:${track.identifier}`;
|
|
340
347
|
const recommendedResult = (await this.manager.useableNode.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(identifier)}`));
|
|
341
348
|
const tracks = this.buildTracksFromResponse(recommendedResult, requester);
|
|
342
349
|
return tracks;
|
|
@@ -515,6 +522,7 @@ class AutoPlayUtils {
|
|
|
515
522
|
case Enums_1.LoadTypes.Playlist:
|
|
516
523
|
return searchResult.playlist.tracks;
|
|
517
524
|
case Enums_1.LoadTypes.Track:
|
|
525
|
+
case Enums_1.LoadTypes.Episode:
|
|
518
526
|
case Enums_1.LoadTypes.Search:
|
|
519
527
|
case Enums_1.LoadTypes.Short:
|
|
520
528
|
return searchResult.tracks;
|
|
@@ -550,6 +558,7 @@ class AutoPlayUtils {
|
|
|
550
558
|
case Enums_1.LoadTypes.Playlist:
|
|
551
559
|
return searchResult.playlist.tracks[0] || null;
|
|
552
560
|
case Enums_1.LoadTypes.Track:
|
|
561
|
+
case Enums_1.LoadTypes.Episode:
|
|
553
562
|
case Enums_1.LoadTypes.Search:
|
|
554
563
|
case Enums_1.LoadTypes.Short:
|
|
555
564
|
return searchResult.tracks[0] || null;
|
|
@@ -577,6 +586,7 @@ class AutoPlayUtils {
|
|
|
577
586
|
if (TrackUtils.isErrorOrEmptySearchResult(recommendedResult))
|
|
578
587
|
return [];
|
|
579
588
|
switch (recommendedResult.loadType) {
|
|
589
|
+
case Enums_1.LoadTypes.Episode:
|
|
580
590
|
case Enums_1.LoadTypes.Track: {
|
|
581
591
|
const data = recommendedResult.data;
|
|
582
592
|
if (!this.isTrackData(data)) {
|