yukimu 1.2.0 → 1.3.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/.cache/replit/env/latest +72 -72
- package/.cache/replit/env/latest.json +1 -1
- package/.cache/replit/toolchain.json +1 -1
- package/dist/ConnectionPool.d.ts +1 -0
- package/dist/ConnectionPool.d.ts.map +1 -1
- package/dist/ConnectionPool.js +11 -24
- package/dist/ConnectionPool.js.map +1 -1
- package/dist/Constants.d.ts +3 -49
- package/dist/Constants.d.ts.map +1 -1
- package/dist/Constants.js +6 -61
- package/dist/Constants.js.map +1 -1
- package/dist/Node.d.ts +1 -1
- package/dist/Node.d.ts.map +1 -1
- package/dist/Node.js +17 -17
- package/dist/Node.js.map +1 -1
- package/dist/Player.d.ts +9 -5
- package/dist/Player.d.ts.map +1 -1
- package/dist/Player.js +73 -54
- package/dist/Player.js.map +1 -1
- package/dist/Plugin.d.ts +0 -14
- package/dist/Plugin.d.ts.map +1 -1
- package/dist/Plugin.js +0 -12
- package/dist/Plugin.js.map +1 -1
- package/dist/Queue.d.ts +0 -1
- package/dist/Queue.d.ts.map +1 -1
- package/dist/Queue.js +7 -22
- package/dist/Queue.js.map +1 -1
- package/dist/Resolver.d.ts +1 -1
- package/dist/Resolver.d.ts.map +1 -1
- package/dist/Resolver.js +24 -10
- package/dist/Resolver.js.map +1 -1
- package/dist/Rest.d.ts +3 -4
- package/dist/Rest.d.ts.map +1 -1
- package/dist/Rest.js +26 -16
- package/dist/Rest.js.map +1 -1
- package/dist/TrackCache.d.ts +0 -5
- package/dist/TrackCache.d.ts.map +1 -1
- package/dist/TrackCache.js +6 -23
- package/dist/TrackCache.js.map +1 -1
- package/dist/WsQueue.d.ts +2 -11
- package/dist/WsQueue.d.ts.map +1 -1
- package/dist/WsQueue.js +5 -21
- package/dist/WsQueue.js.map +1 -1
- package/dist/Yukimu.d.ts +3 -12
- package/dist/Yukimu.d.ts.map +1 -1
- package/dist/Yukimu.js +2 -8
- package/dist/Yukimu.js.map +1 -1
- package/dist/connector/DiscordJS.d.ts +1 -18
- package/dist/connector/DiscordJS.d.ts.map +1 -1
- package/dist/connector/DiscordJS.js.map +1 -1
- package/dist/connector/Eris.d.ts +1 -16
- package/dist/connector/Eris.d.ts.map +1 -1
- package/dist/connector/Eris.js +2 -4
- package/dist/connector/Eris.js.map +1 -1
- package/dist/connector/Oceanic.d.ts +1 -13
- package/dist/connector/Oceanic.d.ts.map +1 -1
- package/dist/connector/Oceanic.js +2 -4
- package/dist/connector/Oceanic.js.map +1 -1
- package/dist/errors/YukimuError.d.ts.map +1 -1
- package/dist/errors/YukimuError.js +0 -1
- package/dist/errors/YukimuError.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -5
- package/dist/index.js.map +1 -1
- package/dist/plugins/AutoResume.d.ts +0 -13
- package/dist/plugins/AutoResume.d.ts.map +1 -1
- package/dist/plugins/AutoResume.js +10 -18
- package/dist/plugins/AutoResume.js.map +1 -1
- package/dist/plugins/PlayerMoved.js.map +1 -1
- package/dist/types.d.ts +25 -34
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ConnectionPool.ts +14 -31
- package/src/Constants.ts +6 -62
- package/src/Node.ts +44 -51
- package/src/Player.ts +86 -91
- package/src/Plugin.ts +0 -16
- package/src/Queue.ts +7 -32
- package/src/Resolver.ts +25 -14
- package/src/Rest.ts +34 -28
- package/src/TrackCache.ts +7 -30
- package/src/WsQueue.ts +9 -29
- package/src/Yukimu.ts +10 -45
- package/src/connector/DiscordJS.ts +5 -12
- package/src/connector/Eris.ts +6 -17
- package/src/connector/Oceanic.ts +5 -15
- package/src/errors/YukimuError.ts +0 -2
- package/src/index.ts +4 -26
- package/src/plugins/AutoResume.ts +13 -37
- package/src/plugins/PlayerMoved.ts +4 -4
- package/src/types.ts +26 -26
- package/tsconfig.json +3 -1
package/src/Queue.ts
CHANGED
|
@@ -5,21 +5,16 @@ export class Queue {
|
|
|
5
5
|
public previous: Track[] = [];
|
|
6
6
|
private tracks: Track[] = [];
|
|
7
7
|
|
|
8
|
-
// ─── Add / Remove ─────────────────────────────────────────────────
|
|
9
|
-
|
|
10
8
|
public add(tracks: Track | Track[], position?: number): void {
|
|
11
9
|
const arr = Array.isArray(tracks) ? tracks : [tracks];
|
|
12
|
-
if (position !== undefined)
|
|
13
|
-
|
|
14
|
-
} else {
|
|
15
|
-
this.tracks.push(...arr);
|
|
16
|
-
}
|
|
10
|
+
if (position !== undefined) this.tracks.splice(position, 0, ...arr);
|
|
11
|
+
else this.tracks.push(...arr);
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
public next(): Track | null {
|
|
20
15
|
if (this.current) {
|
|
21
16
|
this.previous.unshift(this.current);
|
|
22
|
-
if (this.previous.length > 10) this.previous.pop();
|
|
17
|
+
if (this.previous.length > 10) this.previous.pop();
|
|
23
18
|
}
|
|
24
19
|
this.current = this.tracks.shift() ?? null;
|
|
25
20
|
return this.current;
|
|
@@ -41,8 +36,6 @@ export class Queue {
|
|
|
41
36
|
this.previous = [];
|
|
42
37
|
}
|
|
43
38
|
|
|
44
|
-
// ─── Reorder ──────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
39
|
public shuffle(): void {
|
|
47
40
|
for (let i = this.tracks.length - 1; i > 0; i--) {
|
|
48
41
|
const j = Math.floor(Math.random() * (i + 1));
|
|
@@ -56,36 +49,18 @@ export class Queue {
|
|
|
56
49
|
this.tracks.splice(to, 0, track);
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
/** Skip to a specific position in queue */
|
|
60
52
|
public skipto(index: number): Track | null {
|
|
61
53
|
if (index < 0 || index >= this.tracks.length) return null;
|
|
62
54
|
this.tracks.splice(0, index);
|
|
63
55
|
return this.next();
|
|
64
56
|
}
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
public
|
|
69
|
-
return this.tracks.slice(0, count);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public find(predicate: (track: Track) => boolean): Track | undefined {
|
|
73
|
-
return this.tracks.find(predicate);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
public filter(predicate: (track: Track) => boolean): Track[] {
|
|
77
|
-
return this.tracks.filter(predicate);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ─── Getters ──────────────────────────────────────────────────────
|
|
58
|
+
public peek(count: number = 10): Track[] { return this.tracks.slice(0, count); }
|
|
59
|
+
public find(predicate: (track: Track) => boolean): Track | undefined { return this.tracks.find(predicate); }
|
|
60
|
+
public filter(predicate: (track: Track) => boolean): Track[] { return this.tracks.filter(predicate); }
|
|
81
61
|
|
|
82
62
|
get size(): number { return this.tracks.length; }
|
|
83
|
-
|
|
84
63
|
get isEmpty(): boolean { return this.tracks.length === 0; }
|
|
85
|
-
|
|
86
|
-
get totalDuration(): number {
|
|
87
|
-
return this.tracks.reduce((acc, t) => acc + (t.info?.length ?? 0), 0);
|
|
88
|
-
}
|
|
89
|
-
|
|
64
|
+
get totalDuration(): number { return this.tracks.reduce((acc, t) => acc + (t.info?.length ?? 0), 0); }
|
|
90
65
|
get list(): ReadonlyArray<Track> { return this.tracks; }
|
|
91
66
|
}
|
package/src/Resolver.ts
CHANGED
|
@@ -7,19 +7,28 @@ export class Resolver {
|
|
|
7
7
|
private spotifyToken: string | null = null;
|
|
8
8
|
private spotifyExpiry: number = 0;
|
|
9
9
|
|
|
10
|
-
constructor(manager: Yukimu) {
|
|
11
|
-
this.manager = manager;
|
|
12
|
-
}
|
|
10
|
+
constructor(manager: Yukimu) { this.manager = manager; }
|
|
13
11
|
|
|
14
|
-
public async resolve(query: string, source: SearchSource, requester?:
|
|
12
|
+
public async resolve(query: string, source: SearchSource, requester?: any): Promise<SearchResult> {
|
|
15
13
|
const node = this.manager.getBestNode();
|
|
16
14
|
const isUrl = /^https?:\/\//.test(query);
|
|
17
15
|
const identifier = isUrl ? query : `${SOURCE_PREFIXES[source] ?? "ytsearch"}:${query}`;
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
let result: SearchResult;
|
|
18
|
+
try {
|
|
19
|
+
result = await node.loadTracks(identifier);
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
console.error("[Resolver] loadTracks error:", err.message);
|
|
22
|
+
return { loadType: "error", tracks: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!result) return { loadType: "empty", tracks: [] };
|
|
26
|
+
if (!result.tracks) result.tracks = [];
|
|
20
27
|
|
|
21
|
-
// Attach requester
|
|
22
|
-
if (requester
|
|
28
|
+
// Attach requester to all tracks
|
|
29
|
+
if (requester && result.tracks.length > 0) {
|
|
30
|
+
result.tracks.forEach((t: any) => { t.requester = requester; });
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
// Spotify fallback — if LavaSrc not installed
|
|
25
34
|
if ((result.loadType === "error" || result.loadType === "empty") && isUrl) {
|
|
@@ -30,9 +39,11 @@ export class Resolver {
|
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
// Cache
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
// Cache tracks
|
|
43
|
+
if (result.tracks.length > 0) {
|
|
44
|
+
for (const track of result.tracks) {
|
|
45
|
+
if (track?.encoded) this.manager.trackCache.set(track.encoded, track);
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
return result;
|
|
@@ -50,7 +61,7 @@ export class Resolver {
|
|
|
50
61
|
if (!opts) return null;
|
|
51
62
|
if (this.spotifyToken && Date.now() < this.spotifyExpiry) return this.spotifyToken;
|
|
52
63
|
try {
|
|
53
|
-
const res = await fetch("https://accounts.spotify.com/api/token", {
|
|
64
|
+
const res = await (globalThis as any).fetch("https://accounts.spotify.com/api/token", {
|
|
54
65
|
method: "POST",
|
|
55
66
|
headers: {
|
|
56
67
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
@@ -58,7 +69,7 @@ export class Resolver {
|
|
|
58
69
|
},
|
|
59
70
|
body: "grant_type=client_credentials",
|
|
60
71
|
});
|
|
61
|
-
const data = await res.json()
|
|
72
|
+
const data = await res.json();
|
|
62
73
|
this.spotifyToken = data.access_token;
|
|
63
74
|
this.spotifyExpiry = Date.now() + data.expires_in * 1000 - 5000;
|
|
64
75
|
return this.spotifyToken;
|
|
@@ -69,10 +80,10 @@ export class Resolver {
|
|
|
69
80
|
const token = await this.getSpotifyToken();
|
|
70
81
|
if (!token) return null;
|
|
71
82
|
try {
|
|
72
|
-
const res = await fetch(`https://api.spotify.com/v1/tracks/${trackId}`, {
|
|
83
|
+
const res = await (globalThis as any).fetch(`https://api.spotify.com/v1/tracks/${trackId}`, {
|
|
73
84
|
headers: { Authorization: `Bearer ${token}` },
|
|
74
85
|
});
|
|
75
|
-
const data = await res.json()
|
|
86
|
+
const data = await res.json();
|
|
76
87
|
return { title: data.name, artist: data.artists[0]?.name ?? "Unknown" };
|
|
77
88
|
} catch { return null; }
|
|
78
89
|
}
|
package/src/Rest.ts
CHANGED
|
@@ -2,35 +2,31 @@ import { RestError } from "./errors/YukimuError";
|
|
|
2
2
|
import type { Node } from "./Node";
|
|
3
3
|
import type { SearchResult, Track, NodeStats, NodeInfo } from "./types";
|
|
4
4
|
|
|
5
|
-
const TIMEOUT_MS =
|
|
5
|
+
const TIMEOUT_MS = 30000;
|
|
6
6
|
|
|
7
7
|
export class Rest {
|
|
8
8
|
private node: Node;
|
|
9
9
|
|
|
10
|
-
constructor(node: Node) {
|
|
11
|
-
this.node = node;
|
|
12
|
-
}
|
|
10
|
+
constructor(node: Node) { this.node = node; }
|
|
13
11
|
|
|
14
12
|
get baseUrl(): string {
|
|
15
13
|
const protocol = this.node.options.secure ? "https" : "http";
|
|
16
14
|
return `${protocol}://${this.node.options.host}:${this.node.options.port}`;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
get prefix(): string {
|
|
20
|
-
return this.node.version === 4 ? "/v4" : "";
|
|
21
|
-
}
|
|
17
|
+
get prefix(): string { return this.node.version === 4 ? "/v4" : ""; }
|
|
22
18
|
|
|
23
19
|
get headers(): Record<string, string> {
|
|
24
20
|
return {
|
|
25
21
|
Authorization: this.node.options.password,
|
|
26
22
|
"User-Id": this.node.manager.options.clientId,
|
|
27
|
-
"Client-Name": "Yukimu/1.
|
|
23
|
+
"Client-Name": "Yukimu/1.3.0",
|
|
28
24
|
"Content-Type": "application/json",
|
|
29
25
|
...(this.node.version === 3 ? { "Num-Shards": "1" } : {}),
|
|
30
26
|
};
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
public async request<T =
|
|
29
|
+
public async request<T = any>(method: string, path: string, body?: any): Promise<T> {
|
|
34
30
|
const url = `${this.baseUrl}${this.prefix}${path}`;
|
|
35
31
|
const controller = new AbortController();
|
|
36
32
|
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
@@ -51,7 +47,17 @@ export class Rest {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
if (res.status === 204) return undefined as T;
|
|
54
|
-
|
|
50
|
+
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
let parsed: any;
|
|
53
|
+
try { parsed = JSON.parse(text); } catch { return null as T; }
|
|
54
|
+
|
|
55
|
+
// Normalize Lavalink v4.2+ response — uses "data" instead of "tracks"
|
|
56
|
+
if (parsed && parsed.data !== undefined && !parsed.tracks) {
|
|
57
|
+
parsed.tracks = Array.isArray(parsed.data) ? parsed.data : [parsed.data];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parsed as T;
|
|
55
61
|
} catch (err: any) {
|
|
56
62
|
clearTimeout(timeout);
|
|
57
63
|
if (err.name === "AbortError") throw new RestError(`REST timeout on ${method} ${path}`, 408, path);
|
|
@@ -60,8 +66,9 @@ export class Rest {
|
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
public async loadTracks(identifier: string): Promise<SearchResult> {
|
|
63
|
-
const raw = await this.request<
|
|
64
|
-
|
|
69
|
+
const raw = await this.request<any>("GET", `/loadtracks?identifier=${encodeURIComponent(identifier)}`);
|
|
70
|
+
if (!raw) return { loadType: "empty", tracks: [] };
|
|
71
|
+
return this.node.version === 3 ? this.normalizeV3(raw) : raw;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
public async decodeTrack(encoded: string): Promise<Track> {
|
|
@@ -80,12 +87,7 @@ export class Rest {
|
|
|
80
87
|
return this.request("GET", "/stats");
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
public async
|
|
84
|
-
const res = await (globalThis as any).fetch(`${this.baseUrl}/version`, { headers: this.headers });
|
|
85
|
-
return res.text();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public async updatePlayer(guildId: string, body: unknown, noReplace = false): Promise<unknown> {
|
|
90
|
+
public async updatePlayer(guildId: string, body: any, noReplace = false): Promise<any> {
|
|
89
91
|
const path = this.node.version === 4
|
|
90
92
|
? `/sessions/${this.node.sessionId}/players/${guildId}?noReplace=${noReplace}`
|
|
91
93
|
: `/players/${guildId}`;
|
|
@@ -96,25 +98,29 @@ export class Rest {
|
|
|
96
98
|
const path = this.node.version === 4
|
|
97
99
|
? `/sessions/${this.node.sessionId}/players/${guildId}`
|
|
98
100
|
: `/players/${guildId}`;
|
|
99
|
-
await this.request("DELETE", path);
|
|
101
|
+
await this.request("DELETE", path).catch(() => {});
|
|
100
102
|
}
|
|
101
103
|
|
|
102
|
-
public async updateSession(body: { resuming?: boolean; timeout?: number }): Promise<
|
|
104
|
+
public async updateSession(body: { resuming?: boolean; timeout?: number }): Promise<any> {
|
|
103
105
|
if (this.node.version !== 4) return;
|
|
104
106
|
return this.request("PATCH", `/sessions/${this.node.sessionId}`, body);
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
private normalizeV3(raw:
|
|
108
|
-
const loadType = (
|
|
109
|
-
const norm = (tracks:
|
|
110
|
-
(tracks ?? []).map((t: any) => ({
|
|
109
|
+
private normalizeV3(raw: any): SearchResult {
|
|
110
|
+
const loadType = (raw?.loadType ?? "").toLowerCase();
|
|
111
|
+
const norm = (tracks: any[]): Track[] =>
|
|
112
|
+
(tracks ?? []).map((t: any) => ({
|
|
113
|
+
encoded: t.track ?? t.encoded,
|
|
114
|
+
info: t.info,
|
|
115
|
+
pluginInfo: t.pluginInfo ?? {},
|
|
116
|
+
}));
|
|
111
117
|
|
|
112
118
|
switch (loadType) {
|
|
113
|
-
case "track_loaded": return { loadType: "track", tracks: norm(raw.tracks
|
|
114
|
-
case "playlist_loaded": return { loadType: "playlist", tracks: norm(raw.tracks
|
|
115
|
-
case "search_result": return { loadType: "search", tracks: norm(raw.tracks
|
|
119
|
+
case "track_loaded": return { loadType: "track", tracks: norm(raw.tracks) };
|
|
120
|
+
case "playlist_loaded": return { loadType: "playlist", tracks: norm(raw.tracks), playlistInfo: raw.playlistInfo };
|
|
121
|
+
case "search_result": return { loadType: "search", tracks: norm(raw.tracks) };
|
|
116
122
|
case "no_matches": return { loadType: "empty", tracks: [] };
|
|
117
|
-
case "load_failed": return { loadType: "error", tracks: [], exception: raw.exception
|
|
123
|
+
case "load_failed": return { loadType: "error", tracks: [], exception: raw.exception };
|
|
118
124
|
default: return { loadType: "empty", tracks: [] };
|
|
119
125
|
}
|
|
120
126
|
}
|
package/src/TrackCache.ts
CHANGED
|
@@ -5,51 +5,30 @@ interface CacheEntry {
|
|
|
5
5
|
expiresAt: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Track Decode Cache
|
|
10
|
-
* Caches decoded tracks to avoid repeated Lavalink decode calls.
|
|
11
|
-
* TTL-based expiry — default 1 hour.
|
|
12
|
-
*/
|
|
13
8
|
export class TrackCache {
|
|
14
9
|
private cache: Map<string, CacheEntry> = new Map();
|
|
15
10
|
private readonly ttl: number;
|
|
16
11
|
private sweepInterval: ReturnType<typeof setInterval>;
|
|
17
12
|
|
|
18
|
-
constructor(ttlMs: number = 3600000
|
|
13
|
+
constructor(ttlMs: number = 3600000) {
|
|
19
14
|
this.ttl = ttlMs;
|
|
20
|
-
|
|
21
|
-
// Sweep expired entries every 10 minutes
|
|
22
15
|
this.sweepInterval = setInterval(() => this.sweep(), 600000);
|
|
23
16
|
}
|
|
24
17
|
|
|
25
18
|
public set(encoded: string, track: Track): void {
|
|
26
|
-
this.cache.set(encoded, {
|
|
27
|
-
track,
|
|
28
|
-
expiresAt: Date.now() + this.ttl,
|
|
29
|
-
});
|
|
19
|
+
this.cache.set(encoded, { track, expiresAt: Date.now() + this.ttl });
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
public get(encoded: string): Track | null {
|
|
33
23
|
const entry = this.cache.get(encoded);
|
|
34
24
|
if (!entry) return null;
|
|
35
|
-
if (Date.now() > entry.expiresAt) {
|
|
36
|
-
this.cache.delete(encoded);
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
25
|
+
if (Date.now() > entry.expiresAt) { this.cache.delete(encoded); return null; }
|
|
39
26
|
return entry.track;
|
|
40
27
|
}
|
|
41
28
|
|
|
42
|
-
public has(encoded: string): boolean {
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
public delete(encoded: string): void {
|
|
47
|
-
this.cache.delete(encoded);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
public clear(): void {
|
|
51
|
-
this.cache.clear();
|
|
52
|
-
}
|
|
29
|
+
public has(encoded: string): boolean { return this.get(encoded) !== null; }
|
|
30
|
+
public delete(encoded: string): void { this.cache.delete(encoded); }
|
|
31
|
+
public clear(): void { this.cache.clear(); }
|
|
53
32
|
|
|
54
33
|
private sweep(): void {
|
|
55
34
|
const now = Date.now();
|
|
@@ -63,7 +42,5 @@ export class TrackCache {
|
|
|
63
42
|
this.cache.clear();
|
|
64
43
|
}
|
|
65
44
|
|
|
66
|
-
get size(): number {
|
|
67
|
-
return this.cache.size;
|
|
68
|
-
}
|
|
45
|
+
get size(): number { return this.cache.size; }
|
|
69
46
|
}
|
package/src/WsQueue.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import WebSocket from "ws";
|
|
2
2
|
|
|
3
3
|
interface QueuedMessage {
|
|
4
4
|
data: string;
|
|
@@ -6,14 +6,8 @@ interface QueuedMessage {
|
|
|
6
6
|
reject: (err: Error) => void;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* WebSocket Message Queue
|
|
11
|
-
* Queues outgoing WS messages and flushes them on reconnect.
|
|
12
|
-
* Prevents message loss when node briefly disconnects.
|
|
13
|
-
*/
|
|
14
9
|
export class WsQueue {
|
|
15
10
|
private queue: QueuedMessage[] = [];
|
|
16
|
-
private flushing = false;
|
|
17
11
|
private ws: WebSocket | null = null;
|
|
18
12
|
|
|
19
13
|
public setSocket(ws: WebSocket | null): void {
|
|
@@ -23,56 +17,42 @@ export class WsQueue {
|
|
|
23
17
|
}
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
public send(data: Record<string, unknown>): Promise<void> {
|
|
20
|
+
public send(data: Record<string, any>): Promise<void> {
|
|
28
21
|
return new Promise((resolve, reject) => {
|
|
29
22
|
const serialized = JSON.stringify(data);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.ws.send(serialized, (err) => {
|
|
23
|
+
if (this.ws?.readyState === 1) {
|
|
24
|
+
this.ws.send(serialized, (err: any) => {
|
|
33
25
|
if (err) reject(err);
|
|
34
26
|
else resolve();
|
|
35
27
|
});
|
|
36
28
|
} else {
|
|
37
|
-
// Queue for later when reconnected
|
|
38
29
|
this.queue.push({ data: serialized, resolve, reject });
|
|
39
30
|
}
|
|
40
31
|
});
|
|
41
32
|
}
|
|
42
33
|
|
|
43
|
-
/** Flush all queued messages after reconnect */
|
|
44
34
|
public flush(): void {
|
|
45
|
-
if (
|
|
46
|
-
this.flushing = true;
|
|
47
|
-
|
|
35
|
+
if (!this.ws) return;
|
|
48
36
|
const toSend = [...this.queue];
|
|
49
37
|
this.queue = [];
|
|
50
|
-
|
|
51
38
|
for (const msg of toSend) {
|
|
52
|
-
if (this.ws.readyState !== 1
|
|
53
|
-
// Re-queue if disconnected again
|
|
39
|
+
if (this.ws.readyState !== 1) {
|
|
54
40
|
this.queue.unshift(msg);
|
|
55
|
-
this.flushing = false;
|
|
56
41
|
return;
|
|
57
42
|
}
|
|
58
|
-
this.ws.send(msg.data, (err) => {
|
|
43
|
+
this.ws.send(msg.data, (err: any) => {
|
|
59
44
|
if (err) msg.reject(err);
|
|
60
45
|
else msg.resolve();
|
|
61
46
|
});
|
|
62
47
|
}
|
|
63
|
-
|
|
64
|
-
this.flushing = false;
|
|
65
48
|
}
|
|
66
49
|
|
|
67
|
-
/** Drop all queued messages (on intentional destroy) */
|
|
68
50
|
public clear(): void {
|
|
69
51
|
for (const msg of this.queue) {
|
|
70
|
-
msg.reject(new Error("WsQueue cleared
|
|
52
|
+
msg.reject(new Error("WsQueue cleared"));
|
|
71
53
|
}
|
|
72
54
|
this.queue = [];
|
|
73
55
|
}
|
|
74
56
|
|
|
75
|
-
get size(): number {
|
|
76
|
-
return this.queue.length;
|
|
77
|
-
}
|
|
57
|
+
get size(): number { return this.queue.length; }
|
|
78
58
|
}
|
package/src/Yukimu.ts
CHANGED
|
@@ -8,10 +8,7 @@ import { URL_PATTERNS } from "./Constants";
|
|
|
8
8
|
import { YukimuError } from "./errors/YukimuError";
|
|
9
9
|
import type { Plugin } from "./Plugin";
|
|
10
10
|
import type { Connector } from "./connector/Connector";
|
|
11
|
-
import type {
|
|
12
|
-
YukimuOptions, NodeOptions, PlayerOptions,
|
|
13
|
-
YukimuEvents, SearchSource, SearchResult,
|
|
14
|
-
} from "./types";
|
|
11
|
+
import type { YukimuOptions, NodeOptions, PlayerOptions, YukimuEvents, SearchSource, SearchResult } from "./types";
|
|
15
12
|
|
|
16
13
|
export class Yukimu extends EventEmitter {
|
|
17
14
|
public readonly options: YukimuOptions;
|
|
@@ -21,40 +18,27 @@ export class Yukimu extends EventEmitter {
|
|
|
21
18
|
public readonly resolver: Resolver;
|
|
22
19
|
public readonly pool: ConnectionPool;
|
|
23
20
|
public readonly trackCache: TrackCache;
|
|
24
|
-
|
|
25
21
|
private readonly plugins: Map<string, Plugin> = new Map();
|
|
26
|
-
|
|
27
22
|
public nodeResolver?: (nodes: Map<string, Node>) => Node | undefined;
|
|
28
23
|
|
|
29
|
-
constructor(
|
|
30
|
-
options: YukimuOptions,
|
|
31
|
-
connector: Connector,
|
|
32
|
-
nodes: NodeOptions[],
|
|
33
|
-
plugins: Plugin[] = []
|
|
34
|
-
) {
|
|
24
|
+
constructor(options: YukimuOptions, connector: Connector, nodes: NodeOptions[], plugins: Plugin[] = []) {
|
|
35
25
|
super();
|
|
36
|
-
this.options = {
|
|
37
|
-
moveOnDisconnect: true,
|
|
38
|
-
voiceConnectionTimeout: 15000,
|
|
39
|
-
...options,
|
|
40
|
-
};
|
|
26
|
+
this.options = { moveOnDisconnect: true, voiceConnectionTimeout: 15000, ...options };
|
|
41
27
|
this.connector = connector.set(this);
|
|
42
28
|
this.resolver = new Resolver(this);
|
|
43
29
|
this.pool = new ConnectionPool(this);
|
|
44
30
|
this.trackCache = new TrackCache();
|
|
45
|
-
|
|
46
31
|
for (const plugin of plugins) this.use(plugin);
|
|
47
32
|
this.connector.listen();
|
|
48
33
|
for (const node of nodes) this.addNode(node);
|
|
49
34
|
}
|
|
50
35
|
|
|
51
36
|
// ─── Plugin System ────────────────────────────────────────────────
|
|
52
|
-
|
|
53
37
|
public use(plugin: Plugin): this {
|
|
54
38
|
if (this.plugins.has(plugin.name)) throw new YukimuError(`Plugin "${plugin.name}" already loaded`);
|
|
55
39
|
plugin.load(this);
|
|
56
40
|
this.plugins.set(plugin.name, plugin);
|
|
57
|
-
console.log(`[Yukimu]
|
|
41
|
+
console.log(`[Yukimu] Plugin "${plugin.name}" loaded`);
|
|
58
42
|
return this;
|
|
59
43
|
}
|
|
60
44
|
|
|
@@ -71,7 +55,6 @@ export class Yukimu extends EventEmitter {
|
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
// ─── Node Management ──────────────────────────────────────────────
|
|
74
|
-
|
|
75
58
|
public addNode(options: NodeOptions): Node {
|
|
76
59
|
if (this.nodes.has(options.name)) throw new YukimuError(`Node "${options.name}" already exists`);
|
|
77
60
|
const node = new Node(this, options);
|
|
@@ -107,7 +90,6 @@ export class Yukimu extends EventEmitter {
|
|
|
107
90
|
}
|
|
108
91
|
|
|
109
92
|
// ─── Player Management ────────────────────────────────────────────
|
|
110
|
-
|
|
111
93
|
public createPlayer(options: PlayerOptions): Player {
|
|
112
94
|
const existing = this.players.get(options.guildId);
|
|
113
95
|
if (existing) return existing;
|
|
@@ -133,8 +115,7 @@ export class Yukimu extends EventEmitter {
|
|
|
133
115
|
}
|
|
134
116
|
|
|
135
117
|
// ─── Search ───────────────────────────────────────────────────────
|
|
136
|
-
|
|
137
|
-
public async search(query: string, options?: { source?: SearchSource; requester?: unknown }): Promise<SearchResult> {
|
|
118
|
+
public async search(query: string, options?: { source?: SearchSource; requester?: any }): Promise<SearchResult> {
|
|
138
119
|
return this.resolver.resolve(
|
|
139
120
|
query,
|
|
140
121
|
options?.source ?? this.options.defaultSource ?? "youtube",
|
|
@@ -143,13 +124,7 @@ export class Yukimu extends EventEmitter {
|
|
|
143
124
|
}
|
|
144
125
|
|
|
145
126
|
// ─── Voice Gateway ────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
public handleVoiceStateUpdate(data: {
|
|
148
|
-
guild_id?: string;
|
|
149
|
-
user_id: string;
|
|
150
|
-
session_id: string;
|
|
151
|
-
channel_id?: string | null;
|
|
152
|
-
}): void {
|
|
127
|
+
public handleVoiceStateUpdate(data: any): void {
|
|
153
128
|
if (data.user_id !== this.options.clientId) return;
|
|
154
129
|
if (!data.guild_id) return;
|
|
155
130
|
|
|
@@ -184,11 +159,7 @@ export class Yukimu extends EventEmitter {
|
|
|
184
159
|
}
|
|
185
160
|
}
|
|
186
161
|
|
|
187
|
-
public handleVoiceServerUpdate(data: {
|
|
188
|
-
guild_id: string;
|
|
189
|
-
token: string;
|
|
190
|
-
endpoint?: string | null;
|
|
191
|
-
}): void {
|
|
162
|
+
public handleVoiceServerUpdate(data: any): void {
|
|
192
163
|
if (!data.endpoint) return;
|
|
193
164
|
this.pool.updateServer(data.guild_id, data.token, data.endpoint);
|
|
194
165
|
const player = this.players.get(data.guild_id);
|
|
@@ -202,7 +173,6 @@ export class Yukimu extends EventEmitter {
|
|
|
202
173
|
}
|
|
203
174
|
|
|
204
175
|
// ─── Utility ─────────────────────────────────────────────────────
|
|
205
|
-
|
|
206
176
|
public detectSource(url: string): SearchSource | null {
|
|
207
177
|
for (const [source, patterns] of Object.entries(URL_PATTERNS)) {
|
|
208
178
|
if (patterns.some(p => p.test(url))) return source as SearchSource;
|
|
@@ -223,25 +193,20 @@ export class Yukimu extends EventEmitter {
|
|
|
223
193
|
|
|
224
194
|
public destroy(): void {
|
|
225
195
|
for (const node of this.nodes.values()) node.destroy();
|
|
226
|
-
for (const guildId of this.players.keys()) this.destroyPlayer(guildId).catch(() => {});
|
|
227
196
|
this.trackCache.destroy();
|
|
228
197
|
this.nodes.clear();
|
|
229
198
|
}
|
|
230
199
|
|
|
231
200
|
// ─── Typed EventEmitter ───────────────────────────────────────────
|
|
232
|
-
|
|
233
201
|
public on<K extends keyof YukimuEvents>(event: K, listener: (...args: YukimuEvents[K]) => void): this {
|
|
234
|
-
return super.on(event, listener as
|
|
202
|
+
return super.on(event, listener as any);
|
|
235
203
|
}
|
|
236
|
-
|
|
237
204
|
public once<K extends keyof YukimuEvents>(event: K, listener: (...args: YukimuEvents[K]) => void): this {
|
|
238
|
-
return super.once(event, listener as
|
|
205
|
+
return super.once(event, listener as any);
|
|
239
206
|
}
|
|
240
|
-
|
|
241
207
|
public off<K extends keyof YukimuEvents>(event: K, listener: (...args: YukimuEvents[K]) => void): this {
|
|
242
|
-
return super.off(event, listener as
|
|
208
|
+
return super.off(event, listener as any);
|
|
243
209
|
}
|
|
244
|
-
|
|
245
210
|
public emit<K extends keyof YukimuEvents>(event: K, ...args: YukimuEvents[K]): boolean {
|
|
246
211
|
return super.emit(event, ...args);
|
|
247
212
|
}
|
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import { Connector } from "./Connector";
|
|
2
|
-
import type { Yukimu } from "../Yukimu";
|
|
3
|
-
|
|
4
|
-
interface DiscordJSClient {
|
|
5
|
-
user: { id: string } | null;
|
|
6
|
-
guilds: { cache: Map<string, { shard: { send(payload: unknown): void } }> };
|
|
7
|
-
on(event: "raw", listener: (packet: { t: string; d: unknown }) => void): this;
|
|
8
|
-
}
|
|
9
2
|
|
|
10
3
|
export class DiscordJS extends Connector {
|
|
11
|
-
private client:
|
|
4
|
+
private client: any;
|
|
12
5
|
|
|
13
|
-
constructor(client:
|
|
6
|
+
constructor(client: any) {
|
|
14
7
|
super();
|
|
15
8
|
this.client = client;
|
|
16
9
|
}
|
|
17
10
|
|
|
18
11
|
public listen(): void {
|
|
19
|
-
this.client.on("raw", (packet:
|
|
12
|
+
this.client.on("raw", (packet: any) => {
|
|
20
13
|
if (packet.t === "VOICE_STATE_UPDATE") {
|
|
21
|
-
this.manager.handleVoiceStateUpdate(packet.d
|
|
14
|
+
this.manager.handleVoiceStateUpdate(packet.d);
|
|
22
15
|
}
|
|
23
16
|
if (packet.t === "VOICE_SERVER_UPDATE") {
|
|
24
|
-
this.manager.handleVoiceServerUpdate(packet.d
|
|
17
|
+
this.manager.handleVoiceServerUpdate(packet.d);
|
|
25
18
|
}
|
|
26
19
|
});
|
|
27
20
|
}
|
package/src/connector/Eris.ts
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
import { Connector } from "./Connector";
|
|
2
|
-
import type { Yukimu } from "../Yukimu";
|
|
3
|
-
|
|
4
|
-
interface ErisClient {
|
|
5
|
-
guilds: Map<string, { shard: { id: number } }>;
|
|
6
|
-
shards: Map<number, { sendWS(op: number, data: unknown): void }>;
|
|
7
|
-
on(event: "rawWS", listener: (packet: { t: string; d: unknown }) => void): this;
|
|
8
|
-
}
|
|
9
2
|
|
|
10
3
|
export class Eris extends Connector {
|
|
11
|
-
private client:
|
|
4
|
+
private client: any;
|
|
12
5
|
|
|
13
|
-
constructor(client:
|
|
6
|
+
constructor(client: any) {
|
|
14
7
|
super();
|
|
15
8
|
this.client = client;
|
|
16
9
|
}
|
|
17
10
|
|
|
18
11
|
public listen(): void {
|
|
19
|
-
this.client.on("rawWS", (packet:
|
|
20
|
-
if (packet.t === "VOICE_STATE_UPDATE")
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
if (packet.t === "VOICE_SERVER_UPDATE") {
|
|
24
|
-
this.manager.handleVoiceServerUpdate(packet.d as Parameters<Yukimu["handleVoiceServerUpdate"]>[0]);
|
|
25
|
-
}
|
|
12
|
+
this.client.on("rawWS", (packet: any) => {
|
|
13
|
+
if (packet.t === "VOICE_STATE_UPDATE") this.manager.handleVoiceStateUpdate(packet.d);
|
|
14
|
+
if (packet.t === "VOICE_SERVER_UPDATE") this.manager.handleVoiceServerUpdate(packet.d);
|
|
26
15
|
});
|
|
27
16
|
}
|
|
28
17
|
|
|
@@ -30,6 +19,6 @@ export class Eris extends Connector {
|
|
|
30
19
|
const guild = this.client.guilds.get(guildId);
|
|
31
20
|
if (!guild) return;
|
|
32
21
|
const shard = this.client.shards.get(guild.shard.id);
|
|
33
|
-
if (shard) shard.sendWS(4, (payload as
|
|
22
|
+
if (shard) shard.sendWS(4, (payload as any).d);
|
|
34
23
|
}
|
|
35
24
|
}
|