@stella_project/stellalib 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +213 -0
- package/README.md +285 -0
- package/THIRD-PARTY-NOTICES.md +84 -0
- package/dist/Structures/Filters.d.ts +140 -0
- package/dist/Structures/Filters.d.ts.map +1 -0
- package/dist/Structures/Filters.js +315 -0
- package/dist/Structures/Filters.js.map +1 -0
- package/dist/Structures/LRUCache.d.ts +36 -0
- package/dist/Structures/LRUCache.d.ts.map +1 -0
- package/dist/Structures/LRUCache.js +94 -0
- package/dist/Structures/LRUCache.js.map +1 -0
- package/dist/Structures/Manager.d.ts +146 -0
- package/dist/Structures/Manager.d.ts.map +1 -0
- package/dist/Structures/Manager.js +503 -0
- package/dist/Structures/Manager.js.map +1 -0
- package/dist/Structures/Node.d.ts +118 -0
- package/dist/Structures/Node.d.ts.map +1 -0
- package/dist/Structures/Node.js +927 -0
- package/dist/Structures/Node.js.map +1 -0
- package/dist/Structures/Player.d.ts +193 -0
- package/dist/Structures/Player.d.ts.map +1 -0
- package/dist/Structures/Player.js +598 -0
- package/dist/Structures/Player.js.map +1 -0
- package/dist/Structures/Queue.d.ts +48 -0
- package/dist/Structures/Queue.d.ts.map +1 -0
- package/dist/Structures/Queue.js +114 -0
- package/dist/Structures/Queue.js.map +1 -0
- package/dist/Structures/Rest.d.ts +105 -0
- package/dist/Structures/Rest.d.ts.map +1 -0
- package/dist/Structures/Rest.js +343 -0
- package/dist/Structures/Rest.js.map +1 -0
- package/dist/Structures/SessionStore.d.ts +42 -0
- package/dist/Structures/SessionStore.d.ts.map +1 -0
- package/dist/Structures/SessionStore.js +94 -0
- package/dist/Structures/SessionStore.js.map +1 -0
- package/dist/Structures/Types.d.ts +450 -0
- package/dist/Structures/Types.d.ts.map +1 -0
- package/dist/Structures/Types.js +13 -0
- package/dist/Structures/Types.js.map +1 -0
- package/dist/Structures/Utils.d.ts +61 -0
- package/dist/Structures/Utils.d.ts.map +1 -0
- package/dist/Structures/Utils.js +204 -0
- package/dist/Structures/Utils.js.map +1 -0
- package/dist/Utils/FiltersEqualizers.d.ts +20 -0
- package/dist/Utils/FiltersEqualizers.d.ts.map +1 -0
- package/dist/Utils/FiltersEqualizers.js +96 -0
- package/dist/Utils/FiltersEqualizers.js.map +1 -0
- package/dist/Utils/ManagerCheck.d.ts +9 -0
- package/dist/Utils/ManagerCheck.d.ts.map +1 -0
- package/dist/Utils/ManagerCheck.js +45 -0
- package/dist/Utils/ManagerCheck.js.map +1 -0
- package/dist/Utils/NodeCheck.d.ts +9 -0
- package/dist/Utils/NodeCheck.d.ts.map +1 -0
- package/dist/Utils/NodeCheck.js +31 -0
- package/dist/Utils/NodeCheck.js.map +1 -0
- package/dist/Utils/PlayerCheck.d.ts +9 -0
- package/dist/Utils/PlayerCheck.d.ts.map +1 -0
- package/dist/Utils/PlayerCheck.js +23 -0
- package/dist/Utils/PlayerCheck.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* StellaLib — Copyright (c) 2026 AntonyZ, x2sadddDM, SynX, Astel (OSL-3.0)
|
|
4
|
+
* Derived from LithiumX — Copyright (c) 2025 Anantix Network (MIT)
|
|
5
|
+
* See LICENSE and THIRD-PARTY-NOTICES.md for full license details.
|
|
6
|
+
*/
|
|
7
|
+
import type { NodeOptions, PlayerOptions, TrackData, VoicePacket, VoiceServer, VoiceStateUpdate, SearchPlatform, SearchQuery, SearchResult, ManagerOptions, ManagerEvents } from "./Types";
|
|
8
|
+
import type { StellaNode } from "./Node";
|
|
9
|
+
import type { StellaPlayer } from "./Player";
|
|
10
|
+
import { LRUCache } from "./LRUCache";
|
|
11
|
+
import { TypedEmitter } from "tiny-typed-emitter";
|
|
12
|
+
/**
|
|
13
|
+
* The main hub for interacting with Lavalink using StellaLib.
|
|
14
|
+
*/
|
|
15
|
+
declare class StellaManager extends TypedEmitter<ManagerEvents> {
|
|
16
|
+
static readonly DEFAULT_SOURCES: Record<SearchPlatform, string>;
|
|
17
|
+
/** The map of players. */
|
|
18
|
+
readonly players: Map<string, StellaPlayer>;
|
|
19
|
+
/** The map of nodes. */
|
|
20
|
+
readonly nodes: Map<string, StellaNode>;
|
|
21
|
+
/** The options that were set. */
|
|
22
|
+
readonly options: ManagerOptions;
|
|
23
|
+
private initiated;
|
|
24
|
+
/** The search result LRU cache (bounded, TTL-based). */
|
|
25
|
+
caches: LRUCache<string, SearchResult>;
|
|
26
|
+
/** Whether the manager is shutting down. */
|
|
27
|
+
private shuttingDown;
|
|
28
|
+
/** Returns the nodes sorted by least CPU load. */
|
|
29
|
+
get leastLoadNode(): Map<string, StellaNode>;
|
|
30
|
+
/** Returns the nodes sorted by least amount of players. */
|
|
31
|
+
private get leastPlayersNode();
|
|
32
|
+
/** Returns the node with the lowest penalty score (best performance). */
|
|
33
|
+
get leastPenaltyNode(): StellaNode | undefined;
|
|
34
|
+
/** Returns a node based on priority. */
|
|
35
|
+
private get priorityNode();
|
|
36
|
+
/** Returns the best node to use based on configuration. */
|
|
37
|
+
get useableNodes(): StellaNode;
|
|
38
|
+
/**
|
|
39
|
+
* Initiates the Manager class.
|
|
40
|
+
* @param options
|
|
41
|
+
*/
|
|
42
|
+
constructor(options: ManagerOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Initiates the Manager.
|
|
45
|
+
* @param clientId
|
|
46
|
+
*/
|
|
47
|
+
init(clientId?: string): this;
|
|
48
|
+
/**
|
|
49
|
+
* Searches the enabled sources based off the URL or the `source` property.
|
|
50
|
+
* @param query
|
|
51
|
+
* @param requester The user who requested the search.
|
|
52
|
+
*/
|
|
53
|
+
search(query: string | SearchQuery, requester?: string): Promise<SearchResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Returns the available source managers and plugins on a connected node.
|
|
56
|
+
* Useful for checking which search platforms the Lavalink server supports.
|
|
57
|
+
*/
|
|
58
|
+
getAvailableSources(): Promise<{
|
|
59
|
+
sourceManagers: string[];
|
|
60
|
+
plugins: {
|
|
61
|
+
name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
}[];
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Decodes the base64 encoded tracks and returns a TrackData array.
|
|
67
|
+
* @param tracks
|
|
68
|
+
*/
|
|
69
|
+
decodeTracks(tracks: string[]): Promise<TrackData[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Decodes the base64 encoded track and returns a TrackData.
|
|
72
|
+
* @param track
|
|
73
|
+
*/
|
|
74
|
+
decodeTrack(track: string): Promise<TrackData>;
|
|
75
|
+
/**
|
|
76
|
+
* Creates a player or returns one if it already exists.
|
|
77
|
+
* @param options
|
|
78
|
+
*/
|
|
79
|
+
create(options: PlayerOptions): StellaPlayer;
|
|
80
|
+
/**
|
|
81
|
+
* Returns a player or undefined if it does not exist.
|
|
82
|
+
* @param guild
|
|
83
|
+
*/
|
|
84
|
+
get(guild: string): StellaPlayer | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Destroys a player if it exists.
|
|
87
|
+
* @param guild
|
|
88
|
+
*/
|
|
89
|
+
destroy(guild: string): void;
|
|
90
|
+
/**
|
|
91
|
+
* Creates a node or returns one if it already exists.
|
|
92
|
+
* @param options
|
|
93
|
+
*/
|
|
94
|
+
createNode(options: NodeOptions): StellaNode;
|
|
95
|
+
/**
|
|
96
|
+
* Destroys a node if it exists.
|
|
97
|
+
* @param identifier
|
|
98
|
+
*/
|
|
99
|
+
destroyNode(identifier: string): void;
|
|
100
|
+
/**
|
|
101
|
+
* Sends voice data to the Lavalink server.
|
|
102
|
+
* Handles both VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE from Discord.
|
|
103
|
+
* Includes channelId in voice state to satisfy Lavalink v4 requirements.
|
|
104
|
+
* @param data
|
|
105
|
+
*/
|
|
106
|
+
updateVoiceState(data: VoicePacket | VoiceServer | VoiceStateUpdate): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Gracefully shuts down the Manager: persists sessions, closes all nodes, and cleans up.
|
|
109
|
+
* Call this before your bot exits to enable seamless session resume on restart.
|
|
110
|
+
*
|
|
111
|
+
* Usage:
|
|
112
|
+
* ```ts
|
|
113
|
+
* process.on("SIGINT", async () => {
|
|
114
|
+
* await manager.shutdown();
|
|
115
|
+
* process.exit(0);
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
shutdown(): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Returns memory and performance statistics for monitoring.
|
|
122
|
+
*/
|
|
123
|
+
getStats(): {
|
|
124
|
+
nodes: {
|
|
125
|
+
identifier: string;
|
|
126
|
+
connected: boolean;
|
|
127
|
+
players: number;
|
|
128
|
+
playingPlayers: number;
|
|
129
|
+
penalties: number;
|
|
130
|
+
uptime: number;
|
|
131
|
+
memory: {
|
|
132
|
+
used: number;
|
|
133
|
+
free: number;
|
|
134
|
+
allocated: number;
|
|
135
|
+
};
|
|
136
|
+
restRequests: number;
|
|
137
|
+
restFailed: number;
|
|
138
|
+
}[];
|
|
139
|
+
totalPlayers: number;
|
|
140
|
+
totalPlayingPlayers: number;
|
|
141
|
+
cacheSize: number;
|
|
142
|
+
cacheMemoryEstimate: number;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export { StellaManager };
|
|
146
|
+
//# sourceMappingURL=Manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Manager.d.ts","sourceRoot":"","sources":["../../src/Structures/Manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.StellaManager = void 0;
|
|
7
|
+
const Utils_1 = require("./Utils");
|
|
8
|
+
const LRUCache_1 = require("./LRUCache");
|
|
9
|
+
const tiny_typed_emitter_1 = require("tiny-typed-emitter");
|
|
10
|
+
const ManagerCheck_1 = __importDefault(require("../Utils/ManagerCheck"));
|
|
11
|
+
/**
|
|
12
|
+
* The main hub for interacting with Lavalink using StellaLib.
|
|
13
|
+
*/
|
|
14
|
+
class StellaManager extends tiny_typed_emitter_1.TypedEmitter {
|
|
15
|
+
static DEFAULT_SOURCES = {
|
|
16
|
+
"youtube music": "ytmsearch",
|
|
17
|
+
youtube: "ytsearch",
|
|
18
|
+
spotify: "spsearch",
|
|
19
|
+
jiosaavn: "jssearch",
|
|
20
|
+
soundcloud: "scsearch",
|
|
21
|
+
deezer: "dzsearch",
|
|
22
|
+
tidal: "tdsearch",
|
|
23
|
+
applemusic: "amsearch",
|
|
24
|
+
bandcamp: "bcsearch",
|
|
25
|
+
};
|
|
26
|
+
/** The map of players. */
|
|
27
|
+
players = new Map();
|
|
28
|
+
/** The map of nodes. */
|
|
29
|
+
nodes = new Map();
|
|
30
|
+
/** The options that were set. */
|
|
31
|
+
options;
|
|
32
|
+
initiated = false;
|
|
33
|
+
/** The search result LRU cache (bounded, TTL-based). */
|
|
34
|
+
caches;
|
|
35
|
+
/** Whether the manager is shutting down. */
|
|
36
|
+
shuttingDown = false;
|
|
37
|
+
/** Returns the nodes sorted by least CPU load. */
|
|
38
|
+
get leastLoadNode() {
|
|
39
|
+
const sorted = [...this.nodes.entries()]
|
|
40
|
+
.filter(([, node]) => node.connected)
|
|
41
|
+
.sort(([, a], [, b]) => {
|
|
42
|
+
const aload = a.stats.cpu
|
|
43
|
+
? (a.stats.cpu.lavalinkLoad / a.stats.cpu.cores) * 100
|
|
44
|
+
: 0;
|
|
45
|
+
const bload = b.stats.cpu
|
|
46
|
+
? (b.stats.cpu.lavalinkLoad / b.stats.cpu.cores) * 100
|
|
47
|
+
: 0;
|
|
48
|
+
return aload - bload;
|
|
49
|
+
});
|
|
50
|
+
return new Map(sorted);
|
|
51
|
+
}
|
|
52
|
+
/** Returns the nodes sorted by least amount of players. */
|
|
53
|
+
get leastPlayersNode() {
|
|
54
|
+
const sorted = [...this.nodes.entries()]
|
|
55
|
+
.filter(([, node]) => node.connected)
|
|
56
|
+
.sort(([, a], [, b]) => a.stats.players - b.stats.players);
|
|
57
|
+
return new Map(sorted);
|
|
58
|
+
}
|
|
59
|
+
/** Returns the node with the lowest penalty score (best performance). */
|
|
60
|
+
get leastPenaltyNode() {
|
|
61
|
+
const connected = [...this.nodes.values()].filter((n) => n.connected);
|
|
62
|
+
if (!connected.length)
|
|
63
|
+
return undefined;
|
|
64
|
+
return connected.reduce((best, node) => node.penalties < best.penalties ? node : best);
|
|
65
|
+
}
|
|
66
|
+
/** Returns a node based on priority. */
|
|
67
|
+
get priorityNode() {
|
|
68
|
+
const filteredNodes = [...this.nodes.values()].filter((node) => node.connected && (node.options.priority ?? 0) > 0);
|
|
69
|
+
const totalWeight = filteredNodes.reduce((total, node) => total + (node.options.priority ?? 0), 0);
|
|
70
|
+
const weightedNodes = filteredNodes.map((node) => ({
|
|
71
|
+
node,
|
|
72
|
+
weight: (node.options.priority ?? 0) / totalWeight,
|
|
73
|
+
}));
|
|
74
|
+
const randomNumber = Math.random();
|
|
75
|
+
let cumulativeWeight = 0;
|
|
76
|
+
for (const { node, weight } of weightedNodes) {
|
|
77
|
+
cumulativeWeight += weight;
|
|
78
|
+
if (randomNumber <= cumulativeWeight)
|
|
79
|
+
return node;
|
|
80
|
+
}
|
|
81
|
+
return this.leastPenaltyNode;
|
|
82
|
+
}
|
|
83
|
+
/** Returns the best node to use based on configuration. */
|
|
84
|
+
get useableNodes() {
|
|
85
|
+
if (this.options.usePriority)
|
|
86
|
+
return this.priorityNode;
|
|
87
|
+
switch (this.options.useNode) {
|
|
88
|
+
case "leastLoad":
|
|
89
|
+
return this.leastLoadNode.values().next().value;
|
|
90
|
+
case "leastPlayers":
|
|
91
|
+
return this.leastPlayersNode.values().next().value;
|
|
92
|
+
default:
|
|
93
|
+
// Default: use penalty-based selection (best overall)
|
|
94
|
+
return this.leastPenaltyNode ?? this.leastLoadNode.values().next().value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Initiates the Manager class.
|
|
99
|
+
* @param options
|
|
100
|
+
*/
|
|
101
|
+
constructor(options) {
|
|
102
|
+
super();
|
|
103
|
+
(0, ManagerCheck_1.default)(options);
|
|
104
|
+
Utils_1.Structure.get("Player").init(this);
|
|
105
|
+
Utils_1.Structure.get("Node").init(this);
|
|
106
|
+
Utils_1.TrackUtils.init(this);
|
|
107
|
+
if (options.trackPartial) {
|
|
108
|
+
Utils_1.TrackUtils.setTrackPartial(options.trackPartial);
|
|
109
|
+
delete options.trackPartial;
|
|
110
|
+
}
|
|
111
|
+
this.options = {
|
|
112
|
+
plugins: [],
|
|
113
|
+
nodes: [
|
|
114
|
+
{
|
|
115
|
+
identifier: "default",
|
|
116
|
+
host: "localhost",
|
|
117
|
+
resumeStatus: true,
|
|
118
|
+
resumeTimeout: 60,
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
shards: 1,
|
|
122
|
+
autoPlay: true,
|
|
123
|
+
usePriority: false,
|
|
124
|
+
clientName: "StellaLib/0.0.1 (https://github.com/Roki-Stella-Projects/StellaLib)",
|
|
125
|
+
defaultSearchPlatform: "youtube",
|
|
126
|
+
useNode: "leastPlayers",
|
|
127
|
+
caches: { enabled: false, time: 0, maxSize: 200 },
|
|
128
|
+
...options,
|
|
129
|
+
};
|
|
130
|
+
// Initialize LRU cache for search results
|
|
131
|
+
const cacheOpts = this.options.caches;
|
|
132
|
+
this.caches = new LRUCache_1.LRUCache(cacheOpts?.maxSize ?? 200, cacheOpts?.enabled ? (cacheOpts.time || 0) : 0);
|
|
133
|
+
if (this.options.plugins) {
|
|
134
|
+
for (const [index, plugin] of this.options.plugins.entries()) {
|
|
135
|
+
if (!(plugin instanceof Utils_1.Plugin))
|
|
136
|
+
throw new RangeError(`Plugin at index ${index} does not extend Plugin.`);
|
|
137
|
+
plugin.load(this);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (this.options.nodes) {
|
|
141
|
+
for (const nodeOptions of this.options.nodes) {
|
|
142
|
+
const node = new (Utils_1.Structure.get("Node"))(nodeOptions);
|
|
143
|
+
this.nodes.set(node.options.identifier, node);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Periodic LRU cache pruning (removes expired entries)
|
|
147
|
+
if (cacheOpts?.enabled && cacheOpts.time > 0) {
|
|
148
|
+
setInterval(() => {
|
|
149
|
+
const pruned = this.caches.prune();
|
|
150
|
+
if (pruned > 0) {
|
|
151
|
+
this.emit("Debug", `[Cache] Pruned ${pruned} expired entries (${this.caches.size} remaining)`);
|
|
152
|
+
}
|
|
153
|
+
}, Math.max(cacheOpts.time, 30000));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Initiates the Manager.
|
|
158
|
+
* @param clientId
|
|
159
|
+
*/
|
|
160
|
+
init(clientId) {
|
|
161
|
+
if (this.initiated)
|
|
162
|
+
return this;
|
|
163
|
+
if (typeof clientId !== "undefined")
|
|
164
|
+
this.options.clientId = clientId;
|
|
165
|
+
if (typeof this.options.clientId !== "string")
|
|
166
|
+
throw new Error('"clientId" set is not type of "string"');
|
|
167
|
+
if (!this.options.clientId)
|
|
168
|
+
throw new Error('"clientId" is not set. Pass it in Manager#init() or as an option in the constructor.');
|
|
169
|
+
for (const node of this.nodes.values()) {
|
|
170
|
+
Promise.resolve(node.connect()).catch((err) => {
|
|
171
|
+
this.emit("NodeError", node, err);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
this.initiated = true;
|
|
175
|
+
this.emit("Debug", "[Manager] Initialized");
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Searches the enabled sources based off the URL or the `source` property.
|
|
180
|
+
* @param query
|
|
181
|
+
* @param requester The user who requested the search.
|
|
182
|
+
*/
|
|
183
|
+
async search(query, requester) {
|
|
184
|
+
const node = this.useableNodes;
|
|
185
|
+
if (!node)
|
|
186
|
+
throw new Error("No available nodes.");
|
|
187
|
+
if (this.options.caches?.enabled && typeof query === "string") {
|
|
188
|
+
const cached = this.caches.get(query);
|
|
189
|
+
if (cached)
|
|
190
|
+
return cached;
|
|
191
|
+
}
|
|
192
|
+
const _query = typeof query === "string" ? { query } : query;
|
|
193
|
+
const rawQuery = _query.query;
|
|
194
|
+
const isURL = /^https?:\/\//.test(rawQuery);
|
|
195
|
+
// Build the list of platforms to try: primary + fallbacks
|
|
196
|
+
const primarySource = (_query.source ?? this.options.defaultSearchPlatform ?? "youtube");
|
|
197
|
+
const platformsToTry = [primarySource];
|
|
198
|
+
if (!isURL && this.options.searchFallback?.length) {
|
|
199
|
+
for (const fb of this.options.searchFallback) {
|
|
200
|
+
if (fb !== primarySource)
|
|
201
|
+
platformsToTry.push(fb);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
let lastError = null;
|
|
205
|
+
for (const platform of platformsToTry) {
|
|
206
|
+
const prefix = StellaManager.DEFAULT_SOURCES[platform] ?? platform;
|
|
207
|
+
const search = isURL ? rawQuery : `${prefix}:${rawQuery}`;
|
|
208
|
+
try {
|
|
209
|
+
const res = await node.rest.loadTracks(search);
|
|
210
|
+
if (!res)
|
|
211
|
+
continue;
|
|
212
|
+
// If empty or error, try next fallback
|
|
213
|
+
if (res.loadType === "empty" || res.loadType === "error") {
|
|
214
|
+
if (platformsToTry.length > 1) {
|
|
215
|
+
this.emit("Debug", `[Search] "${platform}" returned ${res.loadType} for "${rawQuery}", trying next fallback...`);
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
let searchData = [];
|
|
220
|
+
let playlistData;
|
|
221
|
+
switch (res.loadType) {
|
|
222
|
+
case "search":
|
|
223
|
+
searchData = res.data;
|
|
224
|
+
break;
|
|
225
|
+
case "track":
|
|
226
|
+
searchData = [res.data];
|
|
227
|
+
break;
|
|
228
|
+
case "playlist":
|
|
229
|
+
playlistData = res.data;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
const tracks = searchData.map((track) => Utils_1.TrackUtils.build(track, requester));
|
|
233
|
+
let playlist;
|
|
234
|
+
if (res.loadType === "playlist" && playlistData) {
|
|
235
|
+
playlist = {
|
|
236
|
+
name: playlistData.info.name,
|
|
237
|
+
tracks: playlistData.tracks.map((track) => Utils_1.TrackUtils.build(track, requester)),
|
|
238
|
+
duration: playlistData.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const result = {
|
|
242
|
+
loadType: res.loadType,
|
|
243
|
+
tracks,
|
|
244
|
+
playlist,
|
|
245
|
+
};
|
|
246
|
+
if (this.options.replaceYouTubeCredentials) {
|
|
247
|
+
let tracksToReplace = [];
|
|
248
|
+
if (result.loadType === "playlist" && result.playlist) {
|
|
249
|
+
tracksToReplace = result.playlist.tracks;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
tracksToReplace = result.tracks;
|
|
253
|
+
}
|
|
254
|
+
for (const track of tracksToReplace) {
|
|
255
|
+
if (isYouTubeURL(track.uri)) {
|
|
256
|
+
track.author = track.author.replace("- Topic", "").trim();
|
|
257
|
+
track.title = track.title.replace("Topic -", "").trim();
|
|
258
|
+
}
|
|
259
|
+
if (track.title.includes("-")) {
|
|
260
|
+
const [author, title] = track.title
|
|
261
|
+
.split("-")
|
|
262
|
+
.map((str) => str.trim());
|
|
263
|
+
track.author = author;
|
|
264
|
+
track.title = title;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (this.options.caches?.enabled) {
|
|
269
|
+
this.caches.set(search, result);
|
|
270
|
+
}
|
|
271
|
+
if (platform !== primarySource) {
|
|
272
|
+
this.emit("Debug", `[Search] Found results via fallback "${platform}" for "${rawQuery}"`);
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
lastError = err;
|
|
278
|
+
this.emit("Debug", `[Search] Error on "${platform}" for "${rawQuery}": ${err.message}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// All platforms exhausted — return empty result
|
|
282
|
+
return {
|
|
283
|
+
loadType: "empty",
|
|
284
|
+
tracks: [],
|
|
285
|
+
playlist: undefined,
|
|
286
|
+
};
|
|
287
|
+
function isYouTubeURL(uri) {
|
|
288
|
+
return uri?.includes("youtube.com") || uri?.includes("youtu.be");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Returns the available source managers and plugins on a connected node.
|
|
293
|
+
* Useful for checking which search platforms the Lavalink server supports.
|
|
294
|
+
*/
|
|
295
|
+
async getAvailableSources() {
|
|
296
|
+
const node = this.useableNodes;
|
|
297
|
+
if (!node)
|
|
298
|
+
throw new Error("No available nodes.");
|
|
299
|
+
if (!node.info) {
|
|
300
|
+
await node.fetchInfo();
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
sourceManagers: node.info?.sourceManagers ?? [],
|
|
304
|
+
plugins: node.info?.plugins ?? [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Decodes the base64 encoded tracks and returns a TrackData array.
|
|
309
|
+
* @param tracks
|
|
310
|
+
*/
|
|
311
|
+
async decodeTracks(tracks) {
|
|
312
|
+
const node = this.nodes.values().next().value;
|
|
313
|
+
if (!node)
|
|
314
|
+
throw new Error("No available nodes.");
|
|
315
|
+
const res = await node.rest.decodeTracks(tracks);
|
|
316
|
+
if (!res)
|
|
317
|
+
throw new Error("No data returned from query.");
|
|
318
|
+
return res;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Decodes the base64 encoded track and returns a TrackData.
|
|
322
|
+
* @param track
|
|
323
|
+
*/
|
|
324
|
+
async decodeTrack(track) {
|
|
325
|
+
const res = await this.decodeTracks([track]);
|
|
326
|
+
return res[0];
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Creates a player or returns one if it already exists.
|
|
330
|
+
* @param options
|
|
331
|
+
*/
|
|
332
|
+
create(options) {
|
|
333
|
+
if (this.players.has(options.guild)) {
|
|
334
|
+
return this.players.get(options.guild);
|
|
335
|
+
}
|
|
336
|
+
return new (Utils_1.Structure.get("Player"))(options);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Returns a player or undefined if it does not exist.
|
|
340
|
+
* @param guild
|
|
341
|
+
*/
|
|
342
|
+
get(guild) {
|
|
343
|
+
return this.players.get(guild);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Destroys a player if it exists.
|
|
347
|
+
* @param guild
|
|
348
|
+
*/
|
|
349
|
+
destroy(guild) {
|
|
350
|
+
this.players.delete(guild);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Creates a node or returns one if it already exists.
|
|
354
|
+
* @param options
|
|
355
|
+
*/
|
|
356
|
+
createNode(options) {
|
|
357
|
+
if (this.nodes.has(options.identifier || options.host)) {
|
|
358
|
+
return this.nodes.get(options.identifier || options.host);
|
|
359
|
+
}
|
|
360
|
+
return new (Utils_1.Structure.get("Node"))(options);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Destroys a node if it exists.
|
|
364
|
+
* @param identifier
|
|
365
|
+
*/
|
|
366
|
+
destroyNode(identifier) {
|
|
367
|
+
const node = this.nodes.get(identifier);
|
|
368
|
+
if (!node)
|
|
369
|
+
return;
|
|
370
|
+
node.destroy();
|
|
371
|
+
this.nodes.delete(identifier);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Sends voice data to the Lavalink server.
|
|
375
|
+
* Handles both VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE from Discord.
|
|
376
|
+
* Includes channelId in voice state to satisfy Lavalink v4 requirements.
|
|
377
|
+
* @param data
|
|
378
|
+
*/
|
|
379
|
+
async updateVoiceState(data) {
|
|
380
|
+
if ("t" in data &&
|
|
381
|
+
!["VOICE_STATE_UPDATE", "VOICE_SERVER_UPDATE"].includes(data.t))
|
|
382
|
+
return;
|
|
383
|
+
const update = "d" in data ? data.d : data;
|
|
384
|
+
if (!update || (!("token" in update) && !("session_id" in update)))
|
|
385
|
+
return;
|
|
386
|
+
const player = this.players.get(update.guild_id ?? update.guild_id);
|
|
387
|
+
if (!player)
|
|
388
|
+
return;
|
|
389
|
+
// VOICE_SERVER_UPDATE — contains token & endpoint
|
|
390
|
+
if ("token" in update) {
|
|
391
|
+
player.voiceState.event = update;
|
|
392
|
+
const { sessionId, event: { token, endpoint }, } = player.voiceState;
|
|
393
|
+
// Include channelId in voice state (required by Lavalink v4)
|
|
394
|
+
await player.node.rest
|
|
395
|
+
.updatePlayer({
|
|
396
|
+
guildId: player.guild,
|
|
397
|
+
data: {
|
|
398
|
+
voice: {
|
|
399
|
+
token,
|
|
400
|
+
endpoint,
|
|
401
|
+
sessionId: sessionId,
|
|
402
|
+
channelId: player.voiceChannel ?? undefined,
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
.then(() => {
|
|
407
|
+
player.resolveVoiceReady();
|
|
408
|
+
this.emit("Debug", `[Player:${player.guild}] Voice state flushed to Lavalink`);
|
|
409
|
+
})
|
|
410
|
+
.catch((err) => {
|
|
411
|
+
this.emit("NodeError", player.node, err instanceof Error ? err : new Error(String(err)));
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// VOICE_STATE_UPDATE — contains session_id & channel_id
|
|
416
|
+
const voiceUpdate = update;
|
|
417
|
+
if (voiceUpdate.user_id !== this.options.clientId)
|
|
418
|
+
return;
|
|
419
|
+
if (voiceUpdate.channel_id) {
|
|
420
|
+
if (player.voiceChannel !== voiceUpdate.channel_id) {
|
|
421
|
+
this.emit("PlayerMove", player, player.voiceChannel, voiceUpdate.channel_id);
|
|
422
|
+
}
|
|
423
|
+
player.voiceState.sessionId = voiceUpdate.session_id;
|
|
424
|
+
player.voiceState.channelId = voiceUpdate.channel_id;
|
|
425
|
+
player.voiceChannel = voiceUpdate.channel_id;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// Channel is null — user disconnected
|
|
429
|
+
this.emit("PlayerDisconnect", player, player.voiceChannel);
|
|
430
|
+
player.voiceChannel = null;
|
|
431
|
+
player.voiceState = Object.assign({
|
|
432
|
+
op: "voiceUpdate",
|
|
433
|
+
guildId: player.guild,
|
|
434
|
+
});
|
|
435
|
+
player.destroy();
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Gracefully shuts down the Manager: persists sessions, closes all nodes, and cleans up.
|
|
439
|
+
* Call this before your bot exits to enable seamless session resume on restart.
|
|
440
|
+
*
|
|
441
|
+
* Usage:
|
|
442
|
+
* ```ts
|
|
443
|
+
* process.on("SIGINT", async () => {
|
|
444
|
+
* await manager.shutdown();
|
|
445
|
+
* process.exit(0);
|
|
446
|
+
* });
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
async shutdown() {
|
|
450
|
+
if (this.shuttingDown)
|
|
451
|
+
return;
|
|
452
|
+
this.shuttingDown = true;
|
|
453
|
+
this.emit("Debug", "[Manager] Graceful shutdown initiated...");
|
|
454
|
+
// Gracefully close all nodes (persists session IDs for resume)
|
|
455
|
+
const closePromises = [];
|
|
456
|
+
for (const node of this.nodes.values()) {
|
|
457
|
+
closePromises.push(node.gracefulClose());
|
|
458
|
+
}
|
|
459
|
+
await Promise.allSettled(closePromises);
|
|
460
|
+
// Flush session store if it supports it
|
|
461
|
+
const store = this.options.sessionStore;
|
|
462
|
+
if (store && "destroy" in store && typeof store.destroy === "function") {
|
|
463
|
+
try {
|
|
464
|
+
store.destroy();
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
// Ignore
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Clear caches
|
|
471
|
+
this.caches.clear();
|
|
472
|
+
this.emit("Debug", `[Manager] Shutdown complete. ${this.nodes.size} nodes closed, sessions persisted.`);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Returns memory and performance statistics for monitoring.
|
|
476
|
+
*/
|
|
477
|
+
getStats() {
|
|
478
|
+
const nodes = [...this.nodes.values()].map((node) => ({
|
|
479
|
+
identifier: node.options.identifier,
|
|
480
|
+
connected: node.connected,
|
|
481
|
+
players: node.stats.players,
|
|
482
|
+
playingPlayers: node.stats.playingPlayers,
|
|
483
|
+
penalties: node.penalties,
|
|
484
|
+
uptime: node.uptime,
|
|
485
|
+
memory: {
|
|
486
|
+
used: node.stats.memory.used,
|
|
487
|
+
free: node.stats.memory.free,
|
|
488
|
+
allocated: node.stats.memory.allocated,
|
|
489
|
+
},
|
|
490
|
+
restRequests: node.rest.requestCount,
|
|
491
|
+
restFailed: node.rest.failedCount,
|
|
492
|
+
}));
|
|
493
|
+
return {
|
|
494
|
+
nodes,
|
|
495
|
+
totalPlayers: this.players.size,
|
|
496
|
+
totalPlayingPlayers: [...this.players.values()].filter((p) => p.playing).length,
|
|
497
|
+
cacheSize: this.caches.size,
|
|
498
|
+
cacheMemoryEstimate: this.caches.memoryEstimate,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
exports.StellaManager = StellaManager;
|
|
503
|
+
//# sourceMappingURL=Manager.js.map
|