yukimu 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.
Files changed (48) hide show
  1. package/.cache/replit/modules/nodejs-20.res +1 -0
  2. package/.cache/replit/modules/replit.res +1 -0
  3. package/.cache/replit/modules.stamp +0 -0
  4. package/.cache/replit/nix/dotreplitenv.json +1 -0
  5. package/.cache/replit/toolchain.json +1 -0
  6. package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.packager.installForAll.0 +9 -0
  7. package/.local/state/workflow-logs/7zVU0iVo-fBL1ccMCmELy/configure_your_app.shell.exec.1 +1 -0
  8. package/.local/state/workflow-logs/U0AinJQVHonnwGjj0RXLn/configure_your_app.packager.installForAll.0 +2 -0
  9. package/.replit +4 -0
  10. package/.upm/store.json +1 -0
  11. package/README.md +152 -0
  12. package/dist/Node.d.ts +32 -0
  13. package/dist/Node.d.ts.map +1 -0
  14. package/dist/Node.js +186 -0
  15. package/dist/Node.js.map +1 -0
  16. package/dist/Player.d.ts +63 -0
  17. package/dist/Player.d.ts.map +1 -0
  18. package/dist/Player.js +205 -0
  19. package/dist/Player.js.map +1 -0
  20. package/dist/Queue.d.ts +29 -0
  21. package/dist/Queue.d.ts.map +1 -0
  22. package/dist/Queue.js +75 -0
  23. package/dist/Queue.js.map +1 -0
  24. package/dist/Resolver.d.ts +30 -0
  25. package/dist/Resolver.d.ts.map +1 -0
  26. package/dist/Resolver.js +121 -0
  27. package/dist/Resolver.js.map +1 -0
  28. package/dist/Yukimu.d.ts +59 -0
  29. package/dist/Yukimu.d.ts.map +1 -0
  30. package/dist/Yukimu.js +135 -0
  31. package/dist/Yukimu.js.map +1 -0
  32. package/dist/index.d.ts +7 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +29 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/types.d.ts +137 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +15 -0
  39. package/dist/types.js.map +1 -0
  40. package/package.json +24 -0
  41. package/src/Node.ts +326 -0
  42. package/src/Player.ts +245 -0
  43. package/src/Queue.ts +82 -0
  44. package/src/Resolver.ts +127 -0
  45. package/src/Yukimu.ts +177 -0
  46. package/src/index.ts +6 -0
  47. package/src/types.ts +178 -0
  48. package/tsconfig.json +18 -0
@@ -0,0 +1,127 @@
1
+ import { SearchResult, SearchSource, SourcePrefixes, SpotifyOptions } from "./types";
2
+ import type { Yukimu } from "./Yukimu";
3
+
4
+ // ─── URL Patterns ─────────────────────────────────────────────────────────────
5
+
6
+ const URL_PATTERNS: Record<string, RegExp[]> = {
7
+ youtube: [
8
+ /^https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/.+/,
9
+ /^https?:\/\/music\.youtube\.com\/.+/,
10
+ ],
11
+ spotify: [
12
+ /^https?:\/\/open\.spotify\.com\/(track|album|playlist|artist)\/.+/,
13
+ ],
14
+ soundcloud: [
15
+ /^https?:\/\/(www\.)?soundcloud\.com\/.+/,
16
+ ],
17
+ deezer: [
18
+ /^https?:\/\/(www\.)?deezer\.com\/(track|album|playlist)\/.+/,
19
+ ],
20
+ applemusic: [
21
+ /^https?:\/\/music\.apple\.com\/.+/,
22
+ ],
23
+ tidal: [
24
+ /^https?:\/\/(www\.)?tidal\.com\/(browse\/)?(track|album|playlist)\/.+/,
25
+ ],
26
+ jiosaavn: [
27
+ /^https?:\/\/(www\.)?jiosaavn\.com\/.+/,
28
+ ],
29
+ yandexmusic: [
30
+ /^https?:\/\/music\.yandex\.(ru|com)\/.+/,
31
+ ],
32
+ };
33
+
34
+ export class Resolver {
35
+ private manager: Yukimu;
36
+ private spotifyToken: string | null = null;
37
+ private spotifyExpiry: number = 0;
38
+
39
+ constructor(manager: Yukimu) {
40
+ this.manager = manager;
41
+ }
42
+
43
+ /**
44
+ * Resolve a query or URL to Lavalink tracks
45
+ */
46
+ public async resolve(query: string, source: SearchSource): Promise<SearchResult> {
47
+ const node = this.manager.getBestNode();
48
+
49
+ // Check if query is a direct URL
50
+ const detectedSource = this.detectSource(query);
51
+
52
+ if (detectedSource) {
53
+ // It's a URL — pass directly to Lavalink (LavaSrc handles platform resolution)
54
+ return node.loadTracks(query);
55
+ }
56
+
57
+ // It's a search query — use source prefix
58
+ const prefix = SourcePrefixes[source];
59
+ return node.loadTracks(`${prefix}:${query}`);
60
+ }
61
+
62
+ /**
63
+ * Detect what platform a URL belongs to
64
+ */
65
+ public detectSource(url: string): SearchSource | null {
66
+ for (const [source, patterns] of Object.entries(URL_PATTERNS)) {
67
+ if (patterns.some(p => p.test(url))) {
68
+ return source as SearchSource;
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Get Spotify API token (for resolving Spotify metadata without LavaSrc)
76
+ * Only needed if you're NOT using the LavaSrc Lavalink plugin
77
+ */
78
+ public async getSpotifyToken(): Promise<string | null> {
79
+ const opts = this.manager.options.spotify;
80
+ if (!opts) return null;
81
+ if (this.spotifyToken && Date.now() < this.spotifyExpiry) return this.spotifyToken;
82
+
83
+ try {
84
+ const res = await fetch("https://accounts.spotify.com/api/token", {
85
+ method: "POST",
86
+ headers: {
87
+ "Content-Type": "application/x-www-form-urlencoded",
88
+ Authorization: `Basic ${Buffer.from(`${opts.clientId}:${opts.clientSecret}`).toString("base64")}`,
89
+ },
90
+ body: "grant_type=client_credentials",
91
+ });
92
+
93
+ const data = await res.json() as { access_token: string; expires_in: number };
94
+ this.spotifyToken = data.access_token;
95
+ this.spotifyExpiry = Date.now() + data.expires_in * 1000 - 5000;
96
+ return this.spotifyToken;
97
+ } catch (err) {
98
+ console.error("[Yukimu] Failed to fetch Spotify token:", err);
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Resolve a Spotify URL to track metadata (title + artist for YouTube fallback search)
105
+ * Used when LavaSrc plugin is NOT installed
106
+ */
107
+ public async resolveSpotifyFallback(url: string): Promise<{ title: string; artist: string } | null> {
108
+ const token = await this.getSpotifyToken();
109
+ if (!token) return null;
110
+
111
+ const match = url.match(/spotify\.com\/track\/([a-zA-Z0-9]+)/);
112
+ if (!match) return null;
113
+
114
+ try {
115
+ const res = await fetch(`https://api.spotify.com/v1/tracks/${match[1]}`, {
116
+ headers: { Authorization: `Bearer ${token}` },
117
+ });
118
+ const data = await res.json() as { name: string; artists: { name: string }[] };
119
+ return {
120
+ title: data.name,
121
+ artist: data.artists[0]?.name ?? "Unknown",
122
+ };
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ }
package/src/Yukimu.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { EventEmitter } from "events";
2
+ import { Node } from "./Node";
3
+ import { Player } from "./Player";
4
+ import { Resolver } from "./Resolver";
5
+ import {
6
+ YukimuOptions,
7
+ PlayerOptions,
8
+ YukimuEvents,
9
+ SearchSource,
10
+ SearchResult,
11
+ } from "./types";
12
+
13
+ /**
14
+ * Yukimu - A powerful Lavalink v4 wrapper for Discord bots
15
+ * Supports: YouTube, YouTube Music, Spotify, Deezer, Apple Music,
16
+ * JioSaavn, Tidal, SoundCloud, Yandex Music
17
+ */
18
+ export class Yukimu extends EventEmitter {
19
+ public readonly options: YukimuOptions;
20
+ public readonly nodes: Map<string, Node> = new Map();
21
+ public readonly players: Map<string, Player> = new Map();
22
+ public readonly resolver: Resolver;
23
+
24
+ /** Send function — you must set this to send voice payloads to Discord */
25
+ public sendPayload!: (guildId: string, payload: unknown) => void;
26
+
27
+ constructor(options: YukimuOptions) {
28
+ super();
29
+ this.options = options;
30
+ this.resolver = new Resolver(this);
31
+
32
+ // Initialize all nodes
33
+ for (const nodeOpts of options.nodes) {
34
+ this.addNode(nodeOpts);
35
+ }
36
+ }
37
+
38
+ // ─── Node Management ────────────────────────────────────────────
39
+
40
+ /** Add a new Lavalink node */
41
+ public addNode(options: import("./types").NodeOptions): Node {
42
+ const node = new Node(this, options);
43
+ this.nodes.set(options.name, node);
44
+ node.connect();
45
+ return node;
46
+ }
47
+
48
+ /** Remove a node by name */
49
+ public removeNode(name: string): void {
50
+ const node = this.nodes.get(name);
51
+ if (!node) throw new Error(`Node "${name}" not found`);
52
+ node.destroy();
53
+ this.nodes.delete(name);
54
+ }
55
+
56
+ /** Get the best available node (least load) */
57
+ public getBestNode(): Node {
58
+ const connected = [...this.nodes.values()].filter(n => n.connected);
59
+ if (!connected.length) throw new Error("No connected Lavalink nodes available");
60
+
61
+ return connected.sort((a, b) => {
62
+ const aLoad = a.stats?.cpu?.lavalinkLoad ?? 0;
63
+ const bLoad = b.stats?.cpu?.lavalinkLoad ?? 0;
64
+ return aLoad - bLoad;
65
+ })[0];
66
+ }
67
+
68
+ // ─── Player Management ───────────────────────────────────────────
69
+
70
+ /** Create a new player for a guild */
71
+ public createPlayer(options: PlayerOptions): Player {
72
+ const existing = this.players.get(options.guildId);
73
+ if (existing) return existing;
74
+
75
+ const node = options.nodeName
76
+ ? this.nodes.get(options.nodeName) ?? this.getBestNode()
77
+ : this.getBestNode();
78
+
79
+ const player = new Player(this, node, options);
80
+ this.players.set(options.guildId, player);
81
+ this.emit("playerCreate", player);
82
+ return player;
83
+ }
84
+
85
+ /** Get an existing player */
86
+ public getPlayer(guildId: string): Player | undefined {
87
+ return this.players.get(guildId);
88
+ }
89
+
90
+ /** Destroy a player */
91
+ public destroyPlayer(guildId: string): void {
92
+ const player = this.players.get(guildId);
93
+ if (!player) return;
94
+ player.destroy();
95
+ this.players.delete(guildId);
96
+ this.emit("playerDestroy", player);
97
+ }
98
+
99
+ // ─── Search ──────────────────────────────────────────────────────
100
+
101
+ /**
102
+ * Search for tracks across any supported source
103
+ * @param query - Search query or direct URL
104
+ * @param source - Where to search (youtube, spotify, deezer, etc.)
105
+ */
106
+ public async search(
107
+ query: string,
108
+ source: SearchSource = this.options.defaultSource ?? "youtube"
109
+ ): Promise<SearchResult> {
110
+ return this.resolver.resolve(query, source);
111
+ }
112
+
113
+ // ─── Discord Voice Gateway Handlers ──────────────────────────────
114
+
115
+ /**
116
+ * Handle Discord voice state updates — call this in your bot's
117
+ * voiceStateUpdate event handler
118
+ */
119
+ public handleVoiceStateUpdate(data: {
120
+ guild_id?: string;
121
+ user_id: string;
122
+ session_id: string;
123
+ channel_id?: string | null;
124
+ }): void {
125
+ if (data.user_id !== this.options.clientId) return;
126
+ if (!data.guild_id) return;
127
+
128
+ const player = this.players.get(data.guild_id);
129
+ if (!player) return;
130
+
131
+ if (!data.channel_id) {
132
+ // Bot was disconnected from voice
133
+ player.voiceChannelId = null;
134
+ player.connected = false;
135
+ return;
136
+ }
137
+
138
+ player.voiceChannelId = data.channel_id;
139
+ player.sessionId = data.session_id;
140
+ player.connected = true;
141
+ player.checkVoiceReady();
142
+ }
143
+
144
+ /**
145
+ * Handle Discord voice server updates — call this in your bot's
146
+ * voiceServerUpdate event handler
147
+ */
148
+ public handleVoiceServerUpdate(data: {
149
+ guild_id: string;
150
+ token: string;
151
+ endpoint?: string | null;
152
+ }): void {
153
+ const player = this.players.get(data.guild_id);
154
+ if (!player) return;
155
+ if (!data.endpoint) return;
156
+
157
+ player.voiceToken = data.token;
158
+ player.voiceEndpoint = data.endpoint;
159
+ player.checkVoiceReady();
160
+ }
161
+
162
+ // ─── EventEmitter typing ─────────────────────────────────────────
163
+
164
+ public on<K extends keyof YukimuEvents>(
165
+ event: K,
166
+ listener: (...args: YukimuEvents[K]) => void
167
+ ): this {
168
+ return super.on(event, listener as (...args: unknown[]) => void);
169
+ }
170
+
171
+ public emit<K extends keyof YukimuEvents>(
172
+ event: K,
173
+ ...args: YukimuEvents[K]
174
+ ): boolean {
175
+ return super.emit(event, ...args);
176
+ }
177
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { Yukimu } from "./Yukimu";
2
+ export { Node } from "./Node";
3
+ export { Player } from "./Player";
4
+ export { Queue } from "./Queue";
5
+ export { Resolver } from "./Resolver";
6
+ export * from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,178 @@
1
+ export interface YukimuOptions {
2
+ /** Your Discord bot token */
3
+ token: string;
4
+ /** Client ID of your bot */
5
+ clientId: string;
6
+ /** Lavalink nodes to connect to */
7
+ nodes: NodeOptions[];
8
+ /** Default search source */
9
+ defaultSource?: SearchSource;
10
+ /** Spotify credentials (for Spotify support) */
11
+ spotify?: SpotifyOptions;
12
+ /** Deezer options */
13
+ deezer?: DeezerOptions;
14
+ /** Apple Music options */
15
+ appleMusic?: AppleMusicOptions;
16
+ }
17
+
18
+ export interface NodeOptions {
19
+ /** Node identifier */
20
+ name: string;
21
+ /** Lavalink server host */
22
+ host: string;
23
+ /** Lavalink server port */
24
+ port: number;
25
+ /** Lavalink server password */
26
+ password: string;
27
+ /** Use SSL/TLS */
28
+ secure?: boolean;
29
+ /** Number of retries on disconnect */
30
+ retries?: number;
31
+ /** Lavalink version — 3 or 4 (default: 4) */
32
+ version?: 3 | 4;
33
+ }
34
+
35
+ export interface SpotifyOptions {
36
+ clientId: string;
37
+ clientSecret: string;
38
+ }
39
+
40
+ export interface DeezerOptions {
41
+ /** Deezer master decryption key (from LavaSrc plugin) */
42
+ masterKey?: string;
43
+ }
44
+
45
+ export interface AppleMusicOptions {
46
+ /** Apple Music media API token */
47
+ mediaAPIToken?: string;
48
+ countryCode?: string;
49
+ }
50
+
51
+ export type SearchSource =
52
+ | "youtube"
53
+ | "youtubemusic"
54
+ | "spotify"
55
+ | "deezer"
56
+ | "applemusic"
57
+ | "soundcloud"
58
+ | "tidal"
59
+ | "jiosaavn"
60
+ | "yandexmusic";
61
+
62
+ export const SourcePrefixes: Record<SearchSource, string> = {
63
+ youtube: "ytsearch",
64
+ youtubemusic: "ytmsearch",
65
+ spotify: "spsearch",
66
+ deezer: "dzsearch",
67
+ applemusic: "amsearch",
68
+ soundcloud: "scsearch",
69
+ tidal: "tdsearch",
70
+ jiosaavn: "jssearch",
71
+ yandexmusic: "ymsearch",
72
+ };
73
+
74
+ export interface Track {
75
+ encoded: string;
76
+ info: TrackInfo;
77
+ pluginInfo?: Record<string, unknown>;
78
+ }
79
+
80
+ export interface TrackInfo {
81
+ identifier: string;
82
+ isSeekable: boolean;
83
+ author: string;
84
+ length: number;
85
+ isStream: boolean;
86
+ position: number;
87
+ title: string;
88
+ uri?: string;
89
+ artworkUrl?: string;
90
+ isrc?: string;
91
+ sourceName: string;
92
+ }
93
+
94
+ export interface SearchResult {
95
+ loadType: LoadType;
96
+ tracks: Track[];
97
+ playlistInfo?: PlaylistInfo;
98
+ exception?: LavalinkException;
99
+ }
100
+
101
+ export type LoadType =
102
+ | "track"
103
+ | "playlist"
104
+ | "search"
105
+ | "empty"
106
+ | "error";
107
+
108
+ export interface PlaylistInfo {
109
+ name: string;
110
+ selectedTrack: number;
111
+ }
112
+
113
+ export interface LavalinkException {
114
+ message?: string;
115
+ severity: "common" | "suspicious" | "fault";
116
+ cause: string;
117
+ }
118
+
119
+ export interface PlayerOptions {
120
+ /** Guild ID to create player for */
121
+ guildId: string;
122
+ /** Voice channel ID */
123
+ voiceChannelId: string;
124
+ /** Text channel ID (for sending messages) */
125
+ textChannelId?: string;
126
+ /** Whether to self-deaf */
127
+ selfDeaf?: boolean;
128
+ /** Whether to self-mute */
129
+ selfMute?: boolean;
130
+ /** Node name to use (optional, auto-selected if not set) */
131
+ nodeName?: string;
132
+ /** Default volume 0-100 */
133
+ volume?: number;
134
+ }
135
+
136
+ export interface VoiceState {
137
+ token: string;
138
+ endpoint: string;
139
+ sessionId: string;
140
+ }
141
+
142
+ export interface NodeStats {
143
+ players: number;
144
+ playingPlayers: number;
145
+ uptime: number;
146
+ memory: {
147
+ free: number;
148
+ used: number;
149
+ allocated: number;
150
+ reservable: number;
151
+ };
152
+ cpu: {
153
+ cores: number;
154
+ systemLoad: number;
155
+ lavalinkLoad: number;
156
+ };
157
+ frameStats?: {
158
+ sent: number;
159
+ nulled: number;
160
+ deficit: number;
161
+ };
162
+ }
163
+
164
+ export type YukimuEvents = {
165
+ nodeConnect: [node: import("./Node").Node];
166
+ nodeDisconnect: [node: import("./Node").Node, code: number, reason: string];
167
+ nodeError: [node: import("./Node").Node, error: Error];
168
+ nodeReady: [node: import("./Node").Node];
169
+ trackStart: [player: import("./Player").Player, track: Track];
170
+ trackEnd: [player: import("./Player").Player, track: Track, reason: string];
171
+ trackError: [player: import("./Player").Player, track: Track, exception: LavalinkException];
172
+ trackStuck: [player: import("./Player").Player, track: Track, threshold: number];
173
+ playerCreate: [player: import("./Player").Player];
174
+ playerDestroy: [player: import("./Player").Player];
175
+ playerUpdate: [player: import("./Player").Player];
176
+ queueEnd: [player: import("./Player").Player];
177
+ socketClosed: [player: import("./Player").Player, code: number, reason: string];
178
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "CommonJS",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "resolveJsonModule": true
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist", "example"]
18
+ }