yukimu 1.2.0 → 2.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/dist/BitrateOptimizer.d.ts +52 -0
- package/dist/BitrateOptimizer.d.ts.map +1 -0
- package/dist/BitrateOptimizer.js +115 -0
- package/dist/BitrateOptimizer.js.map +1 -0
- package/dist/ConnectionPool.d.ts +52 -2
- package/dist/ConnectionPool.d.ts.map +1 -1
- package/dist/ConnectionPool.js +123 -44
- package/dist/ConnectionPool.js.map +1 -1
- package/dist/Constants.d.ts +26 -47
- package/dist/Constants.d.ts.map +1 -1
- package/dist/Constants.js +134 -67
- package/dist/Constants.js.map +1 -1
- package/dist/Logger.d.ts +22 -0
- package/dist/Logger.d.ts.map +1 -0
- package/dist/Logger.js +74 -0
- package/dist/Logger.js.map +1 -0
- package/dist/Node.d.ts +17 -4
- package/dist/Node.d.ts.map +1 -1
- package/dist/Node.js +217 -79
- package/dist/Node.js.map +1 -1
- package/dist/Player.d.ts +77 -6
- package/dist/Player.d.ts.map +1 -1
- package/dist/Player.js +319 -75
- package/dist/Player.js.map +1 -1
- package/dist/Plugin.d.ts +16 -12
- package/dist/Plugin.d.ts.map +1 -1
- package/dist/Plugin.js +5 -9
- package/dist/Plugin.js.map +1 -1
- package/dist/Queue.d.ts +69 -3
- package/dist/Queue.d.ts.map +1 -1
- package/dist/Queue.js +123 -17
- package/dist/Queue.js.map +1 -1
- package/dist/Resolver.d.ts +33 -2
- package/dist/Resolver.d.ts.map +1 -1
- package/dist/Resolver.js +233 -27
- package/dist/Resolver.js.map +1 -1
- package/dist/Rest.d.ts +25 -3
- package/dist/Rest.d.ts.map +1 -1
- package/dist/Rest.js +165 -23
- package/dist/Rest.js.map +1 -1
- package/dist/TrackCache.d.ts +28 -8
- package/dist/TrackCache.d.ts.map +1 -1
- package/dist/TrackCache.js +111 -24
- package/dist/TrackCache.js.map +1 -1
- package/dist/WsQueue.d.ts +30 -9
- package/dist/WsQueue.d.ts.map +1 -1
- package/dist/WsQueue.js +66 -24
- package/dist/WsQueue.js.map +1 -1
- package/dist/Yukimu.d.ts +37 -13
- package/dist/Yukimu.d.ts.map +1 -1
- package/dist/Yukimu.js +146 -59
- package/dist/Yukimu.js.map +1 -1
- package/dist/connector/Connector.d.ts +26 -0
- package/dist/connector/Connector.d.ts.map +1 -1
- package/dist/connector/Connector.js +28 -0
- package/dist/connector/Connector.js.map +1 -1
- package/dist/connector/DiscordJS.d.ts +21 -19
- package/dist/connector/DiscordJS.d.ts.map +1 -1
- package/dist/connector/DiscordJS.js +44 -2
- package/dist/connector/DiscordJS.js.map +1 -1
- package/dist/connector/Eris.d.ts +13 -17
- package/dist/connector/Eris.d.ts.map +1 -1
- package/dist/connector/Eris.js +36 -4
- package/dist/connector/Eris.js.map +1 -1
- package/dist/connector/Oceanic.d.ts +13 -14
- package/dist/connector/Oceanic.d.ts.map +1 -1
- package/dist/connector/Oceanic.js +35 -2
- package/dist/connector/Oceanic.js.map +1 -1
- package/dist/errors/YukimuError.d.ts +40 -4
- package/dist/errors/YukimuError.d.ts.map +1 -1
- package/dist/errors/YukimuError.js +79 -9
- package/dist/errors/YukimuError.js.map +1 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -21
- package/dist/index.js.map +1 -1
- package/dist/plugins/AutoResume.d.ts +18 -14
- package/dist/plugins/AutoResume.d.ts.map +1 -1
- package/dist/plugins/AutoResume.js +101 -21
- package/dist/plugins/AutoResume.js.map +1 -1
- package/dist/plugins/AutoplayPlugin.d.ts +35 -0
- package/dist/plugins/AutoplayPlugin.d.ts.map +1 -0
- package/dist/plugins/AutoplayPlugin.js +111 -0
- package/dist/plugins/AutoplayPlugin.js.map +1 -0
- package/dist/plugins/InactivityPlugin.d.ts +30 -0
- package/dist/plugins/InactivityPlugin.d.ts.map +1 -0
- package/dist/plugins/InactivityPlugin.js +86 -0
- package/dist/plugins/InactivityPlugin.js.map +1 -0
- package/dist/plugins/PlayerMoved.d.ts +23 -1
- package/dist/plugins/PlayerMoved.d.ts.map +1 -1
- package/dist/plugins/PlayerMoved.js +57 -12
- package/dist/plugins/PlayerMoved.js.map +1 -1
- package/dist/types.d.ts +196 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +49 -5
- package/.cache/replit/env/latest +0 -88
- package/.cache/replit/env/latest.json +0 -1
- package/.cache/replit/modules/nodejs-20.res +0 -1
- package/.cache/replit/modules/python-3.11.res +0 -1
- package/.cache/replit/modules/replit.res +0 -1
- package/.cache/replit/modules.stamp +0 -0
- package/.cache/replit/nix/dotreplitenv.json +0 -1
- package/.cache/replit/toolchain.json +0 -1
- package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.packager.installForAll.0 +0 -9
- package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.shell.exec.1 +0 -1
- package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.packager.installForAll.0 +0 -1
- package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.shell.exec.1 +0 -1
- package/.local/state/workflow-logs/U0AinJQVHonnwGjj0RXLn/configure_your_app.packager.installForAll.0 +0 -2
- package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.packager.installForAll.0 +0 -1
- package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.shell.exec.1 +0 -1
- package/.replit +0 -7
- package/.upm/store.json +0 -1
- package/src/ConnectionPool.ts +0 -131
- package/src/Constants.ts +0 -101
- package/src/Node.ts +0 -309
- package/src/Player.ts +0 -337
- package/src/Plugin.ts +0 -23
- package/src/Queue.ts +0 -91
- package/src/Resolver.ts +0 -79
- package/src/Rest.ts +0 -121
- package/src/TrackCache.ts +0 -69
- package/src/WsQueue.ts +0 -78
- package/src/Yukimu.ts +0 -248
- package/src/connector/Connector.ts +0 -13
- package/src/connector/DiscordJS.ts +0 -33
- package/src/connector/Eris.ts +0 -35
- package/src/connector/Oceanic.ts +0 -32
- package/src/errors/YukimuError.ts +0 -33
- package/src/index.ts +0 -46
- package/src/plugins/AutoResume.ts +0 -61
- package/src/plugins/PlayerMoved.ts +0 -26
- package/src/types.ts +0 -145
- package/tsconfig.json +0 -20
package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.packager.installForAll.0
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
--> npm install
|
|
2
|
-
[1G[0K⠙[1G[0K⠹[1G[0K⠸[1G[0K⠼[1G[0K⠴[1G[0K
|
|
3
|
-
up to date, audited 28 packages in 657ms
|
|
4
|
-
[1G[0K⠴[1G[0K
|
|
5
|
-
[1G[0K⠴[1G[0K7 packages are looking for funding
|
|
6
|
-
[1G[0K⠴[1G[0K run `npm fund` for details
|
|
7
|
-
[1G[0K⠴[1G[0K
|
|
8
|
-
found [32m[1m0[22m[39m vulnerabilities
|
|
9
|
-
[1G[0K⠴[1G[0K
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Please use the [1;93mWorkflows[0m pane to configure your app. For more info, see: https://docs.replit.com/replit-workspace/workflows#creating-workflows
|
package/.local/state/workflow-logs/KRgHXizaECjWI5nWtS7Dj/configure_your_app.packager.installForAll.0
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Please use the [1;93mWorkflows[0m pane to configure your app. For more info, see: https://docs.replit.com/replit-workspace/workflows#creating-workflows
|
package/.local/state/workflow-logs/jVavLOnv1MqxUvxhMmqER/configure_your_app.packager.installForAll.0
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Please use the [1;93mWorkflows[0m pane to configure your app. For more info, see: https://docs.replit.com/replit-workspace/workflows#creating-workflows
|
package/.replit
DELETED
package/.upm/store.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":2,"languages":{"nodejs-npm":{},"python3-uv":{}}}
|
package/src/ConnectionPool.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { Yukimu } from "./Yukimu";
|
|
2
|
-
|
|
3
|
-
export interface VoiceConnection {
|
|
4
|
-
guildId: string;
|
|
5
|
-
channelId: string;
|
|
6
|
-
sessionId: string | null;
|
|
7
|
-
token: string | null;
|
|
8
|
-
endpoint: string | null;
|
|
9
|
-
timeout: ReturnType<typeof setTimeout> | null;
|
|
10
|
-
retryTimeout: ReturnType<typeof setTimeout> | null;
|
|
11
|
-
connected: boolean;
|
|
12
|
-
retries: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const MAX_VOICE_RETRIES = 3;
|
|
16
|
-
const VOICE_RETRY_DELAY = 5000;
|
|
17
|
-
|
|
18
|
-
export class ConnectionPool {
|
|
19
|
-
private readonly manager: Yukimu;
|
|
20
|
-
private readonly connections: Map<string, VoiceConnection> = new Map();
|
|
21
|
-
|
|
22
|
-
constructor(manager: Yukimu) {
|
|
23
|
-
this.manager = manager;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
public add(guildId: string, channelId: string): VoiceConnection {
|
|
27
|
-
const existing = this.connections.get(guildId);
|
|
28
|
-
if (existing) { existing.channelId = channelId; return existing; }
|
|
29
|
-
|
|
30
|
-
const conn: VoiceConnection = {
|
|
31
|
-
guildId, channelId,
|
|
32
|
-
sessionId: null, token: null, endpoint: null,
|
|
33
|
-
timeout: null, retryTimeout: null,
|
|
34
|
-
connected: false, retries: 0,
|
|
35
|
-
};
|
|
36
|
-
this.connections.set(guildId, conn);
|
|
37
|
-
this.startTimeout(conn);
|
|
38
|
-
return conn;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public get(guildId: string): VoiceConnection | undefined {
|
|
42
|
-
return this.connections.get(guildId);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public remove(guildId: string): void {
|
|
46
|
-
const conn = this.connections.get(guildId);
|
|
47
|
-
if (!conn) return;
|
|
48
|
-
if (conn.timeout) clearTimeout(conn.timeout);
|
|
49
|
-
if (conn.retryTimeout) clearTimeout(conn.retryTimeout);
|
|
50
|
-
this.connections.delete(guildId);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public has(guildId: string): boolean {
|
|
54
|
-
return this.connections.has(guildId);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public updateSession(guildId: string, sessionId: string, channelId?: string): void {
|
|
58
|
-
const conn = this.connections.get(guildId);
|
|
59
|
-
if (!conn) return;
|
|
60
|
-
conn.sessionId = sessionId;
|
|
61
|
-
if (channelId) conn.channelId = channelId;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public updateServer(guildId: string, token: string, endpoint: string): void {
|
|
65
|
-
const conn = this.connections.get(guildId);
|
|
66
|
-
if (!conn) return;
|
|
67
|
-
conn.token = token;
|
|
68
|
-
conn.endpoint = endpoint;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
public markConnected(guildId: string): void {
|
|
72
|
-
const conn = this.connections.get(guildId);
|
|
73
|
-
if (!conn) return;
|
|
74
|
-
conn.connected = true;
|
|
75
|
-
conn.retries = 0;
|
|
76
|
-
if (conn.timeout) { clearTimeout(conn.timeout); conn.timeout = null; }
|
|
77
|
-
if (conn.retryTimeout) { clearTimeout(conn.retryTimeout); conn.retryTimeout = null; }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
public isReady(guildId: string): boolean {
|
|
81
|
-
const conn = this.connections.get(guildId);
|
|
82
|
-
return !!(conn?.sessionId && conn?.token && conn?.endpoint);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public getVoiceState(guildId: string): { token: string; endpoint: string; sessionId: string } | null {
|
|
86
|
-
const conn = this.connections.get(guildId);
|
|
87
|
-
if (!conn?.sessionId || !conn?.token || !conn?.endpoint) return null;
|
|
88
|
-
return { token: conn.token, endpoint: conn.endpoint, sessionId: conn.sessionId };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private startTimeout(conn: VoiceConnection): void {
|
|
92
|
-
const timeoutMs = (this.manager.options as { voiceConnectionTimeout?: number }).voiceConnectionTimeout ?? 15000;
|
|
93
|
-
if (conn.timeout) clearTimeout(conn.timeout);
|
|
94
|
-
conn.timeout = setTimeout(() => {
|
|
95
|
-
if (!conn.connected) this.handleConnectionFailure(conn);
|
|
96
|
-
}, timeoutMs);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private handleConnectionFailure(conn: VoiceConnection): void {
|
|
100
|
-
if (conn.retries >= MAX_VOICE_RETRIES) {
|
|
101
|
-
const player = this.manager.players.get(conn.guildId);
|
|
102
|
-
if (player) {
|
|
103
|
-
(this.manager as any).emit("playerError", player, new Error(`Voice connection failed after ${MAX_VOICE_RETRIES} retries`));
|
|
104
|
-
this.manager.destroyPlayer(conn.guildId).catch(() => {});
|
|
105
|
-
}
|
|
106
|
-
this.remove(conn.guildId);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
conn.retries++;
|
|
111
|
-
const player = this.manager.players.get(conn.guildId);
|
|
112
|
-
if (player) {
|
|
113
|
-
console.warn(`[Yukimu] 🔄 Voice retry ${conn.retries}/${MAX_VOICE_RETRIES} for guild ${conn.guildId}`);
|
|
114
|
-
conn.retryTimeout = setTimeout(() => {
|
|
115
|
-
// Re-send join payload via connector
|
|
116
|
-
this.manager.connector.sendPayload(conn.guildId, {
|
|
117
|
-
op: 4,
|
|
118
|
-
d: {
|
|
119
|
-
guild_id: conn.guildId,
|
|
120
|
-
channel_id: conn.channelId,
|
|
121
|
-
self_deaf: true,
|
|
122
|
-
self_mute: false,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
this.startTimeout(conn);
|
|
126
|
-
}, VOICE_RETRY_DELAY * conn.retries);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
get size(): number { return this.connections.size; }
|
|
131
|
-
}
|
package/src/Constants.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/** Lavalink v3/v4 supported ops and constants */
|
|
2
|
-
|
|
3
|
-
export const VERSION = "2.0.0";
|
|
4
|
-
export const CLIENT_NAME = `Yukimu/${VERSION}`;
|
|
5
|
-
|
|
6
|
-
export enum State {
|
|
7
|
-
CONNECTING = 0,
|
|
8
|
-
NEARLY = 1,
|
|
9
|
-
CONNECTED = 2,
|
|
10
|
-
DISCONNECTING = 3,
|
|
11
|
-
DISCONNECTED = 4,
|
|
12
|
-
RECONNECTING = 5,
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export enum PlayerState {
|
|
16
|
-
CONNECTING = 0,
|
|
17
|
-
CONNECTED = 1,
|
|
18
|
-
DISCONNECTING = 2,
|
|
19
|
-
DISCONNECTED = 3,
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export enum OpCodes {
|
|
23
|
-
// v3 WebSocket ops
|
|
24
|
-
PLAY = "play",
|
|
25
|
-
STOP = "stop",
|
|
26
|
-
PAUSE = "pause",
|
|
27
|
-
SEEK = "seek",
|
|
28
|
-
VOLUME = "volume",
|
|
29
|
-
FILTERS = "filters",
|
|
30
|
-
DESTROY = "destroy",
|
|
31
|
-
VOICE_UPDATE = "voiceUpdate",
|
|
32
|
-
EQUALIZER = "equalizer",
|
|
33
|
-
// Incoming
|
|
34
|
-
STATS = "stats",
|
|
35
|
-
PLAYER_UPDATE = "playerUpdate",
|
|
36
|
-
EVENT = "event",
|
|
37
|
-
READY = "ready",
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export enum LoadType {
|
|
41
|
-
TRACK = "track",
|
|
42
|
-
PLAYLIST = "playlist",
|
|
43
|
-
SEARCH = "search",
|
|
44
|
-
EMPTY = "empty",
|
|
45
|
-
ERROR = "error",
|
|
46
|
-
// v3 equivalents (normalized internally)
|
|
47
|
-
TRACK_LOADED = "track_loaded",
|
|
48
|
-
PLAYLIST_LOADED = "playlist_loaded",
|
|
49
|
-
SEARCH_RESULT = "search_result",
|
|
50
|
-
NO_MATCHES = "no_matches",
|
|
51
|
-
LOAD_FAILED = "load_failed",
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export enum EventType {
|
|
55
|
-
TRACK_START = "TrackStartEvent",
|
|
56
|
-
TRACK_END = "TrackEndEvent",
|
|
57
|
-
TRACK_EXCEPTION = "TrackExceptionEvent",
|
|
58
|
-
TRACK_STUCK = "TrackStuckEvent",
|
|
59
|
-
WS_CLOSED = "WebSocketClosedEvent",
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export enum TrackEndReason {
|
|
63
|
-
FINISHED = "finished",
|
|
64
|
-
LOAD_FAILED = "loadFailed",
|
|
65
|
-
STOPPED = "stopped",
|
|
66
|
-
REPLACED = "replaced",
|
|
67
|
-
CLEANUP = "cleanup",
|
|
68
|
-
// v3
|
|
69
|
-
FINISHED_V3 = "FINISHED",
|
|
70
|
-
STOPPED_V3 = "STOPPED",
|
|
71
|
-
REPLACED_V3 = "REPLACED",
|
|
72
|
-
CLEANUP_V3 = "CLEANUP",
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export const STOPPED_REASONS = new Set([
|
|
76
|
-
"replaced", "stopped", "REPLACED", "STOPPED",
|
|
77
|
-
]);
|
|
78
|
-
|
|
79
|
-
export const SOURCE_PREFIXES: Record<string, string> = {
|
|
80
|
-
youtube: "ytsearch",
|
|
81
|
-
youtubemusic: "ytmsearch",
|
|
82
|
-
spotify: "spsearch",
|
|
83
|
-
deezer: "dzsearch",
|
|
84
|
-
applemusic: "amsearch",
|
|
85
|
-
soundcloud: "scsearch",
|
|
86
|
-
tidal: "tdsearch",
|
|
87
|
-
jiosaavn: "jssearch",
|
|
88
|
-
yandexmusic: "ymsearch",
|
|
89
|
-
flowery: "ftts",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export const URL_PATTERNS: Record<string, RegExp[]> = {
|
|
93
|
-
youtube: [/^https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/, /^https?:\/\/music\.youtube\.com\/.+/],
|
|
94
|
-
spotify: [/^https?:\/\/open\.spotify\.com\/(track|album|playlist|artist)\/.+/],
|
|
95
|
-
soundcloud: [/^https?:\/\/(www\.)?soundcloud\.com\/.+/],
|
|
96
|
-
deezer: [/^https?:\/\/(www\.)?deezer\.com\/(track|album|playlist)\/.+/],
|
|
97
|
-
applemusic: [/^https?:\/\/music\.apple\.com\/.+/],
|
|
98
|
-
tidal: [/^https?:\/\/(www\.)?tidal\.com\/(browse\/)?(track|album|playlist)\/.+/],
|
|
99
|
-
jiosaavn: [/^https?:\/\/(www\.)?jiosaavn\.com\/.+/],
|
|
100
|
-
yandexmusic: [/^https?:\/\/music\.yandex\.(ru|com)\/.+/],
|
|
101
|
-
};
|
package/src/Node.ts
DELETED
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import { Rest } from "./Rest";
|
|
3
|
-
import { WsQueue } from "./WsQueue";
|
|
4
|
-
import { State } from "./Constants";
|
|
5
|
-
import { NodeError } from "./errors/YukimuError";
|
|
6
|
-
import type { Yukimu } from "./Yukimu";
|
|
7
|
-
import type { NodeOptions, NodeStats, NodeInfo, Track, SearchResult, LavalinkException } from "./types";
|
|
8
|
-
|
|
9
|
-
const WS_CLOSE_NORMAL = 1000;
|
|
10
|
-
const HEARTBEAT_INTERVAL = 30000;
|
|
11
|
-
|
|
12
|
-
export class Node {
|
|
13
|
-
public readonly manager: Yukimu;
|
|
14
|
-
public readonly options: NodeOptions;
|
|
15
|
-
public readonly rest: Rest;
|
|
16
|
-
public readonly wsQueue: WsQueue;
|
|
17
|
-
public readonly version: 3 | 4;
|
|
18
|
-
|
|
19
|
-
public ws: WebSocket | null = null;
|
|
20
|
-
public state: State = State.DISCONNECTED;
|
|
21
|
-
public stats: NodeStats | null = null;
|
|
22
|
-
public info: NodeInfo | null = null;
|
|
23
|
-
public sessionId: string | null = null;
|
|
24
|
-
public resumed: boolean = false;
|
|
25
|
-
public penalties: number = 0;
|
|
26
|
-
public ping: number = -1;
|
|
27
|
-
|
|
28
|
-
private restQueue: Promise<unknown> = Promise.resolve();
|
|
29
|
-
private reconnectAttempts = 0;
|
|
30
|
-
private reconnectTimeout?: ReturnType<typeof setTimeout>;
|
|
31
|
-
private heartbeatInterval?: ReturnType<typeof setInterval>;
|
|
32
|
-
private lastHeartbeatAt = 0;
|
|
33
|
-
|
|
34
|
-
constructor(manager: Yukimu, options: NodeOptions) {
|
|
35
|
-
this.manager = manager;
|
|
36
|
-
this.options = { secure: false, retries: 5, version: 4, resumeTimeout: 60, ...options };
|
|
37
|
-
this.version = (this.options.version ?? 4) as 3 | 4;
|
|
38
|
-
this.rest = new Rest(this);
|
|
39
|
-
this.wsQueue = new WsQueue();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
get connected(): boolean {
|
|
43
|
-
return this.state === State.CONNECTED || this.state === State.NEARLY;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
get wsUrl(): string {
|
|
47
|
-
const protocol = this.options.secure ? "wss" : "ws";
|
|
48
|
-
const base = `${protocol}://${this.options.host}:${this.options.port}`;
|
|
49
|
-
return this.version === 4 ? `${base}/v4/websocket` : base;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
get wsHeaders(): Record<string, string> {
|
|
53
|
-
const h: Record<string, string> = {
|
|
54
|
-
Authorization: this.options.password,
|
|
55
|
-
"User-Id": this.manager.options.clientId,
|
|
56
|
-
"Client-Name": "Yukimu/1.2.0",
|
|
57
|
-
};
|
|
58
|
-
if (this.version === 3) h["Num-Shards"] = "1";
|
|
59
|
-
if (this.options.resumeKey) h["Resume-Key"] = this.options.resumeKey;
|
|
60
|
-
return h;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public calculatePenalties(): number {
|
|
64
|
-
if (!this.stats) return Infinity;
|
|
65
|
-
const cpu = Math.pow(1.05, 100 * this.stats.cpu.systemLoad) * 10 - 10;
|
|
66
|
-
const fs = this.stats.frameStats;
|
|
67
|
-
const deficit = fs ? Math.pow(1.03, 500 * fs.deficit / 3000) * 600 - 600 : 0;
|
|
68
|
-
const nulled = fs ? Math.pow(1.03, 500 * fs.nulled / 3000) * 300 - 300 : 0;
|
|
69
|
-
this.penalties = ~~(cpu + deficit + nulled + this.stats.playingPlayers);
|
|
70
|
-
return this.penalties;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
public connect(): void {
|
|
74
|
-
if (this.ws?.readyState === WebSocket.OPEN) return;
|
|
75
|
-
this.state = State.CONNECTING;
|
|
76
|
-
this.manager.emit("nodeConnecting", this);
|
|
77
|
-
this.ws = new WebSocket(this.wsUrl, { headers: this.wsHeaders });
|
|
78
|
-
this.wsQueue.setSocket(this.ws);
|
|
79
|
-
this.ws.on("open", () => this.onOpen());
|
|
80
|
-
this.ws.on("message", (d: WebSocket.RawData) => this.onMessage(d));
|
|
81
|
-
this.ws.on("close", (code: number, reason: Buffer) => this.onClose(code, reason.toString()));
|
|
82
|
-
this.ws.on("error", (err: Error) => this.onError(err));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public destroy(reconnect = false): void {
|
|
86
|
-
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
|
87
|
-
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
88
|
-
this.wsQueue.clear();
|
|
89
|
-
this.wsQueue.setSocket(null);
|
|
90
|
-
this.ws?.removeAllListeners();
|
|
91
|
-
this.ws?.close(WS_CLOSE_NORMAL, "destroy");
|
|
92
|
-
this.ws = null;
|
|
93
|
-
this.state = State.DISCONNECTED;
|
|
94
|
-
if (!reconnect) this.sessionId = null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private async onOpen(): Promise<void> {
|
|
98
|
-
this.state = State.NEARLY;
|
|
99
|
-
this.reconnectAttempts = 0;
|
|
100
|
-
this.manager.emit("nodeConnect", this);
|
|
101
|
-
console.log(`[Yukimu] Node "${this.options.name}" connected (v${this.version})`);
|
|
102
|
-
|
|
103
|
-
try { this.info = await this.rest.getInfo(); } catch {}
|
|
104
|
-
|
|
105
|
-
if (this.version === 4 && this.options.resumeKey) {
|
|
106
|
-
await this.rest.updateSession({ resuming: true, timeout: this.options.resumeTimeout ?? 60 }).catch(() => {});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (this.version === 3) {
|
|
110
|
-
this.state = State.CONNECTED;
|
|
111
|
-
this.sessionId = this.options.resumeKey ?? `v3-${Date.now()}`;
|
|
112
|
-
this.manager.emit("nodeReady", this);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
this.startHeartbeat();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private onMessage(raw: WebSocket.RawData): void {
|
|
119
|
-
let payload: Record<string, unknown>;
|
|
120
|
-
try { payload = JSON.parse(raw.toString()); } catch { return; }
|
|
121
|
-
const op = payload.op as string;
|
|
122
|
-
|
|
123
|
-
if (this.version === 4) {
|
|
124
|
-
switch (op) {
|
|
125
|
-
case "ready":
|
|
126
|
-
this.state = State.CONNECTED;
|
|
127
|
-
this.sessionId = payload.sessionId as string;
|
|
128
|
-
this.resumed = (payload.resumed as boolean) ?? false;
|
|
129
|
-
this.manager.emit("nodeReady", this);
|
|
130
|
-
console.log(`[Yukimu] Node "${this.options.name}" ready | session: ${this.sessionId} | resumed: ${this.resumed}`);
|
|
131
|
-
if (this.resumed) this.reconnectPlayers();
|
|
132
|
-
break;
|
|
133
|
-
case "stats":
|
|
134
|
-
this.stats = payload as unknown as NodeStats;
|
|
135
|
-
this.calculatePenalties();
|
|
136
|
-
break;
|
|
137
|
-
case "playerUpdate":
|
|
138
|
-
this.handlePlayerUpdate(payload);
|
|
139
|
-
break;
|
|
140
|
-
case "event":
|
|
141
|
-
this.handleEvent(payload);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
switch (op) {
|
|
148
|
-
case "stats":
|
|
149
|
-
this.stats = payload as unknown as NodeStats;
|
|
150
|
-
this.calculatePenalties();
|
|
151
|
-
break;
|
|
152
|
-
case "playerUpdate":
|
|
153
|
-
this.handlePlayerUpdate(payload);
|
|
154
|
-
break;
|
|
155
|
-
case "event":
|
|
156
|
-
this.handleEvent(payload);
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private onClose(code: number, reason: string): void {
|
|
162
|
-
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
163
|
-
this.state = State.DISCONNECTED;
|
|
164
|
-
this.manager.emit("nodeDisconnect", this, code, reason);
|
|
165
|
-
console.warn(`[Yukimu] Node "${this.options.name}" disconnected (${code}): ${reason || "No reason"}`);
|
|
166
|
-
if (code !== WS_CLOSE_NORMAL) this.scheduleReconnect();
|
|
167
|
-
else this.movePlayers();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private onError(error: Error): void {
|
|
171
|
-
this.manager.emit("nodeError", this, error);
|
|
172
|
-
console.error(`[Yukimu] Node "${this.options.name}" error:`, error.message);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private startHeartbeat(): void {
|
|
176
|
-
if (this.heartbeatInterval) clearInterval(this.heartbeatInterval);
|
|
177
|
-
this.heartbeatInterval = setInterval(async () => {
|
|
178
|
-
if (!this.connected) return;
|
|
179
|
-
this.lastHeartbeatAt = Date.now();
|
|
180
|
-
try {
|
|
181
|
-
this.stats = await this.rest.getStats();
|
|
182
|
-
this.ping = Date.now() - this.lastHeartbeatAt;
|
|
183
|
-
this.calculatePenalties();
|
|
184
|
-
} catch {}
|
|
185
|
-
}, HEARTBEAT_INTERVAL);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private scheduleReconnect(): void {
|
|
189
|
-
const max = this.options.retries ?? 5;
|
|
190
|
-
if (this.reconnectAttempts >= max) {
|
|
191
|
-
this.manager.emit("nodeError", this, new NodeError(`Node "${this.options.name}" max retries reached`));
|
|
192
|
-
this.movePlayers();
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
196
|
-
this.reconnectAttempts++;
|
|
197
|
-
this.state = State.RECONNECTING;
|
|
198
|
-
console.log(`[Yukimu] Reconnecting "${this.options.name}" in ${delay}ms (${this.reconnectAttempts}/${max})`);
|
|
199
|
-
this.reconnectTimeout = setTimeout(() => { this.destroy(true); this.connect(); }, delay);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private movePlayers(): void {
|
|
203
|
-
const players = [...this.manager.players.values()].filter(p => p.node === this);
|
|
204
|
-
if (!players.length) return;
|
|
205
|
-
let best: Node;
|
|
206
|
-
try { best = this.manager.getBestNode(); } catch { return; }
|
|
207
|
-
if (best === this) return;
|
|
208
|
-
console.log(`[Yukimu] Moving ${players.length} players to "${best.options.name}"`);
|
|
209
|
-
for (const player of players) player.moveToNode(best).catch(console.error);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private reconnectPlayers(): void {
|
|
213
|
-
for (const player of this.manager.players.values()) {
|
|
214
|
-
if (player.node === this) player.checkVoiceReady();
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private handlePlayerUpdate(payload: Record<string, unknown>): void {
|
|
219
|
-
const player = this.manager.players.get(payload.guildId as string);
|
|
220
|
-
if (!player) return;
|
|
221
|
-
const state = payload.state as { position?: number; ping?: number; time?: number };
|
|
222
|
-
player.position = state.position ?? player.position;
|
|
223
|
-
player.ping = state.ping ?? player.ping;
|
|
224
|
-
player.lastUpdated = state.time ?? Date.now();
|
|
225
|
-
this.manager.emit("playerUpdate", player);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private handleEvent(payload: Record<string, unknown>): void {
|
|
229
|
-
const player = this.manager.players.get(payload.guildId as string);
|
|
230
|
-
if (!player) return;
|
|
231
|
-
|
|
232
|
-
let track: Track;
|
|
233
|
-
if (this.version === 3) {
|
|
234
|
-
track = { encoded: payload.track as string, info: {} as Track["info"], pluginInfo: {} };
|
|
235
|
-
} else {
|
|
236
|
-
track = payload.track as Track;
|
|
237
|
-
if (track?.encoded) this.manager.trackCache.set(track.encoded, track);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
switch (payload.type) {
|
|
241
|
-
case "TrackStartEvent":
|
|
242
|
-
player.playing = true;
|
|
243
|
-
player.paused = false;
|
|
244
|
-
player.lastUpdated = Date.now();
|
|
245
|
-
this.manager.emit("trackStart", player, track);
|
|
246
|
-
break;
|
|
247
|
-
|
|
248
|
-
case "TrackEndEvent": {
|
|
249
|
-
const reason = payload.reason as string;
|
|
250
|
-
player.playing = false;
|
|
251
|
-
player.position = 0;
|
|
252
|
-
this.manager.emit("trackEnd", player, track, reason);
|
|
253
|
-
const stopped = ["replaced", "stopped", "REPLACED", "STOPPED"].includes(reason);
|
|
254
|
-
if (!stopped) {
|
|
255
|
-
if (player.loop === "track" && player.queue.current) {
|
|
256
|
-
player.play(player.queue.current).catch(() => {});
|
|
257
|
-
} else {
|
|
258
|
-
if (player.loop === "queue" && player.queue.current) {
|
|
259
|
-
player.queue.add(player.queue.current);
|
|
260
|
-
}
|
|
261
|
-
const next = player.queue.next();
|
|
262
|
-
if (next) {
|
|
263
|
-
player.play(next).catch(() => {});
|
|
264
|
-
} else {
|
|
265
|
-
this.manager.emit("queueEnd", player);
|
|
266
|
-
if (player.autoplay && track.info?.uri) {
|
|
267
|
-
this.manager.emit("autoplayRequest", player, track);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
break;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
case "TrackExceptionEvent":
|
|
276
|
-
this.manager.emit("trackError", player, track, payload.exception as LavalinkException);
|
|
277
|
-
break;
|
|
278
|
-
|
|
279
|
-
case "TrackStuckEvent":
|
|
280
|
-
this.manager.emit("trackStuck", player, track, payload.thresholdMs as number);
|
|
281
|
-
player.skip().catch(() => {});
|
|
282
|
-
break;
|
|
283
|
-
|
|
284
|
-
case "WebSocketClosedEvent":
|
|
285
|
-
this.manager.emit("socketClosed", player, payload.code as number, payload.reason as string, payload.byRemote as boolean);
|
|
286
|
-
break;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
public send(data: Record<string, unknown>): Promise<void> {
|
|
291
|
-
return this.wsQueue.send(data);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
public enqueueRest<T>(fn: () => Promise<T>): Promise<T> {
|
|
295
|
-
const result = this.restQueue.then(() => fn());
|
|
296
|
-
this.restQueue = result.catch(() => {});
|
|
297
|
-
return result;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
public async loadTracks(identifier: string): Promise<SearchResult> {
|
|
301
|
-
return this.rest.loadTracks(identifier);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
public playerPath(guildId: string): string {
|
|
305
|
-
return this.version === 4
|
|
306
|
-
? `/sessions/${this.sessionId}/players/${guildId}`
|
|
307
|
-
: `/players/${guildId}`;
|
|
308
|
-
}
|
|
309
|
-
}
|