ryanlink 1.0.1
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 +37 -0
- package/README.md +455 -0
- package/dist/index.d.mts +1335 -0
- package/dist/index.d.ts +1335 -0
- package/dist/index.js +4694 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4604 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -0
- package/src/audio/AudioFilters.ts +316 -0
- package/src/audio/AudioQueue.ts +782 -0
- package/src/audio/AudioTrack.ts +242 -0
- package/src/audio/QueueController.ts +252 -0
- package/src/audio/TrackCollection.ts +138 -0
- package/src/audio/index.ts +9 -0
- package/src/config/defaults.ts +223 -0
- package/src/config/endpoints.ts +99 -0
- package/src/config/index.ts +9 -0
- package/src/config/patterns.ts +55 -0
- package/src/config/presets.ts +400 -0
- package/src/config/symbols.ts +31 -0
- package/src/core/PluginSystem.ts +50 -0
- package/src/core/RyanlinkPlayer.ts +403 -0
- package/src/core/index.ts +6 -0
- package/src/extensions/AutoplayExtension.ts +283 -0
- package/src/extensions/FairPlayExtension.ts +154 -0
- package/src/extensions/LyricsExtension.ts +187 -0
- package/src/extensions/PersistenceExtension.ts +182 -0
- package/src/extensions/SponsorBlockExtension.ts +81 -0
- package/src/extensions/index.ts +9 -0
- package/src/index.ts +19 -0
- package/src/lavalink/ConnectionPool.ts +326 -0
- package/src/lavalink/HttpClient.ts +316 -0
- package/src/lavalink/LavalinkConnection.ts +409 -0
- package/src/lavalink/index.ts +7 -0
- package/src/metadata.ts +88 -0
- package/src/types/api/Rest.ts +949 -0
- package/src/types/api/Websocket.ts +463 -0
- package/src/types/api/index.ts +6 -0
- package/src/types/audio/FilterManager.ts +29 -0
- package/src/types/audio/Queue.ts +4 -0
- package/src/types/audio/QueueManager.ts +30 -0
- package/src/types/audio/index.ts +7 -0
- package/src/types/common.ts +63 -0
- package/src/types/core/Player.ts +322 -0
- package/src/types/core/index.ts +5 -0
- package/src/types/index.ts +6 -0
- package/src/types/lavalink/Node.ts +173 -0
- package/src/types/lavalink/NodeManager.ts +34 -0
- package/src/types/lavalink/REST.ts +144 -0
- package/src/types/lavalink/index.ts +32 -0
- package/src/types/voice/VoiceManager.ts +176 -0
- package/src/types/voice/index.ts +5 -0
- package/src/utils/helpers.ts +169 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/validators.ts +184 -0
- package/src/voice/RegionSelector.ts +184 -0
- package/src/voice/VoiceConnection.ts +451 -0
- package/src/voice/VoiceSession.ts +297 -0
- package/src/voice/index.ts +7 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { PlayerPlugin } from "../core/PluginSystem";
|
|
2
|
+
import type { Player } from "../core/RyanlinkPlayer";
|
|
3
|
+
import type { Track } from "../audio/AudioTrack";
|
|
4
|
+
|
|
5
|
+
interface PlayerWithGet extends Player {
|
|
6
|
+
get?<T>(key: string): T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FairPlayPluginEvents {
|
|
10
|
+
/**
|
|
11
|
+
* Emitted when fair play reorders the queue
|
|
12
|
+
*/
|
|
13
|
+
fairPlayApplied: [player: Player, guildId: string, trackCount: number];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FairPlayConfig {
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
/** Minimum tracks before fair play kicks in */
|
|
19
|
+
minTracks: number;
|
|
20
|
+
/** Maximum consecutive tracks from same requester */
|
|
21
|
+
maxConsecutive: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fair Play Plugin - Distributes tracks fairly among requesters
|
|
26
|
+
* Prevents one user from dominating the queue
|
|
27
|
+
*/
|
|
28
|
+
export class FairPlayPlugin extends PlayerPlugin<FairPlayPluginEvents & Record<string, unknown[]>> {
|
|
29
|
+
readonly name = "fairplay" as const;
|
|
30
|
+
|
|
31
|
+
#player!: Player;
|
|
32
|
+
config: FairPlayConfig = {
|
|
33
|
+
enabled: true,
|
|
34
|
+
minTracks: 5,
|
|
35
|
+
maxConsecutive: 3,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
init(player: Player): void {
|
|
39
|
+
this.#player = player;
|
|
40
|
+
|
|
41
|
+
const config = (player as PlayerWithGet).get?.("fairplay_config");
|
|
42
|
+
if (config) {
|
|
43
|
+
this.config = { ...this.config, ...config };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Apply fair play when tracks are added
|
|
47
|
+
player.on("trackAdd", (p, g, t) => {
|
|
48
|
+
void this.#handleTrackAdd(p, g, t);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#handleTrackAdd(_player: Player, guildId: string, _tracks: Track[]): void {
|
|
53
|
+
if (!this.config.enabled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const queue = this.#player.queues.get(guildId);
|
|
58
|
+
if (!queue || queue.length < this.config.minTracks) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.applyFairPlay(queue.guildId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Apply fair play algorithm to a queue
|
|
67
|
+
*/
|
|
68
|
+
applyFairPlay(guildId: string): void {
|
|
69
|
+
const queue = this.#player.queues.get(guildId);
|
|
70
|
+
if (!queue) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const playerConfig = (this.#player as PlayerWithGet).get?.("fairplay_config");
|
|
75
|
+
const config = playerConfig ? { ...this.config, ...playerConfig } : this.config;
|
|
76
|
+
|
|
77
|
+
if (queue.length < config.minTracks) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tracks = queue.tracks;
|
|
82
|
+
const fairQueue: Track[] = [];
|
|
83
|
+
const requesterQueues = new Map<string, Track[]>();
|
|
84
|
+
|
|
85
|
+
// Group tracks by requester
|
|
86
|
+
for (const track of tracks) {
|
|
87
|
+
const requesterId = this.#getRequesterId(track);
|
|
88
|
+
let requesterQueue = requesterQueues.get(requesterId);
|
|
89
|
+
if (!requesterQueue) {
|
|
90
|
+
requesterQueue = [];
|
|
91
|
+
requesterQueues.set(requesterId, requesterQueue);
|
|
92
|
+
}
|
|
93
|
+
requesterQueue.push(track);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Distribute tracks fairly
|
|
97
|
+
let hasMore = true;
|
|
98
|
+
let consecutiveCount = 0;
|
|
99
|
+
let lastRequesterId: string | null = null;
|
|
100
|
+
|
|
101
|
+
while (hasMore) {
|
|
102
|
+
hasMore = false;
|
|
103
|
+
|
|
104
|
+
for (const [requesterId, requesterTracks] of requesterQueues) {
|
|
105
|
+
if (requesterTracks.length === 0) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
hasMore = true;
|
|
110
|
+
|
|
111
|
+
// Check if we need to switch requester
|
|
112
|
+
if (lastRequesterId === requesterId) {
|
|
113
|
+
consecutiveCount++;
|
|
114
|
+
if (consecutiveCount >= config.maxConsecutive) {
|
|
115
|
+
// Skip this requester for now
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
consecutiveCount = 1;
|
|
120
|
+
lastRequesterId = requesterId;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add track from this requester
|
|
124
|
+
const track = requesterTracks.shift();
|
|
125
|
+
if (track) {
|
|
126
|
+
fairQueue.push(track);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Reset if we've gone through all requesters
|
|
131
|
+
if (!hasMore) {
|
|
132
|
+
lastRequesterId = null;
|
|
133
|
+
consecutiveCount = 0;
|
|
134
|
+
hasMore = Array.from(requesterQueues.values()).some((q) => q.length > 0);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Update queue with fair distribution
|
|
139
|
+
if (fairQueue.length > 0) {
|
|
140
|
+
queue.clear("current");
|
|
141
|
+
queue.add(fairQueue);
|
|
142
|
+
|
|
143
|
+
this.#player.emit("fairPlayApplied", this.#player, guildId, fairQueue.length);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#getRequesterId(track: Track): string {
|
|
148
|
+
const requester = (track.userData as { requester?: { id: string } }).requester;
|
|
149
|
+
if (requester && typeof requester === "object") {
|
|
150
|
+
return requester.id || "unknown";
|
|
151
|
+
}
|
|
152
|
+
return "unknown";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { PlayerPlugin } from "../core/PluginSystem";
|
|
2
|
+
import type { Player } from "../core/RyanlinkPlayer";
|
|
3
|
+
import { Track } from "../audio/AudioTrack";
|
|
4
|
+
|
|
5
|
+
export interface LyricsPluginEvents {
|
|
6
|
+
/**
|
|
7
|
+
* Emitted when lyrics are found
|
|
8
|
+
*/
|
|
9
|
+
lyricsFound: [player: Player, track: Track, lyrics: LyricsResult];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Emitted when lyrics are not found
|
|
13
|
+
*/
|
|
14
|
+
lyricsNotFound: [player: Player, track: Track];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LyricsLine {
|
|
18
|
+
timestamp: number;
|
|
19
|
+
duration: number | null;
|
|
20
|
+
line: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LyricsResult {
|
|
24
|
+
sourceName: string;
|
|
25
|
+
provider: string;
|
|
26
|
+
text: string | null;
|
|
27
|
+
lines: LyricsLine[];
|
|
28
|
+
track: {
|
|
29
|
+
title: string;
|
|
30
|
+
artist: string;
|
|
31
|
+
album?: string;
|
|
32
|
+
duration: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LyricsConfig {
|
|
37
|
+
sources: {
|
|
38
|
+
lavalink: boolean;
|
|
39
|
+
lrclib: boolean;
|
|
40
|
+
musixmatch: boolean;
|
|
41
|
+
genius: boolean;
|
|
42
|
+
};
|
|
43
|
+
apiKeys?: {
|
|
44
|
+
musixmatch?: string;
|
|
45
|
+
genius?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Lyrics Plugin - Fetches and manages track lyrics from multiple sources
|
|
51
|
+
* Supports Lavalink LavaLyrics, LRCLib, Musixmatch, and Genius
|
|
52
|
+
*/
|
|
53
|
+
export class LyricsPlugin extends PlayerPlugin<LyricsPluginEvents & Record<string, unknown[]>> {
|
|
54
|
+
readonly name = "lyrics" as const;
|
|
55
|
+
|
|
56
|
+
#player!: Player;
|
|
57
|
+
|
|
58
|
+
init(player: Player): void {
|
|
59
|
+
this.#player = player;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fetch lyrics for a track
|
|
64
|
+
* @param track - The track to fetch lyrics for
|
|
65
|
+
* @param config - Configuration for lyrics sources
|
|
66
|
+
*/
|
|
67
|
+
async getLyrics(track: Track, config?: LyricsConfig): Promise<LyricsResult | null> {
|
|
68
|
+
const defaultConfig: LyricsConfig = {
|
|
69
|
+
sources: {
|
|
70
|
+
lavalink: true,
|
|
71
|
+
lrclib: true,
|
|
72
|
+
musixmatch: false,
|
|
73
|
+
genius: false,
|
|
74
|
+
},
|
|
75
|
+
...config,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const title = track.info.title;
|
|
79
|
+
const artist = track.info.author;
|
|
80
|
+
|
|
81
|
+
if (defaultConfig.sources.lavalink) {
|
|
82
|
+
const lavalinkLyrics = await this.#fetchFromLavalink(track);
|
|
83
|
+
if (lavalinkLyrics) {
|
|
84
|
+
this.#player.emit("lyricsFound", this.#player, track, lavalinkLyrics);
|
|
85
|
+
return lavalinkLyrics;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (defaultConfig.sources.lrclib) {
|
|
90
|
+
const lrclibLyrics = await this.#fetchFromLRCLib(title, artist);
|
|
91
|
+
if (lrclibLyrics) {
|
|
92
|
+
this.#player.emit("lyricsFound", this.#player, track, lrclibLyrics);
|
|
93
|
+
return lrclibLyrics;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (defaultConfig.sources.musixmatch && defaultConfig.apiKeys?.musixmatch) {
|
|
98
|
+
const musixmatchLyrics = await this.#fetchFromMusixmatch(title, artist, defaultConfig.apiKeys.musixmatch);
|
|
99
|
+
if (musixmatchLyrics) {
|
|
100
|
+
this.#player.emit("lyricsFound", this.#player, track, musixmatchLyrics);
|
|
101
|
+
return musixmatchLyrics;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (defaultConfig.sources.genius && defaultConfig.apiKeys?.genius) {
|
|
106
|
+
const geniusLyrics = await this.#fetchFromGenius(title, artist, defaultConfig.apiKeys.genius);
|
|
107
|
+
if (geniusLyrics) {
|
|
108
|
+
this.#player.emit("lyricsFound", this.#player, track, geniusLyrics);
|
|
109
|
+
return geniusLyrics;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.#player.emit("lyricsNotFound", this.#player, track);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the current lyric line based on playback position
|
|
119
|
+
*/
|
|
120
|
+
getCurrentLine(lyrics: LyricsResult, position: number): LyricsLine | null {
|
|
121
|
+
if (!lyrics.lines || lyrics.lines.length === 0) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let i = lyrics.lines.length - 1; i >= 0; i--) {
|
|
126
|
+
if (lyrics.lines[i].timestamp <= position) {
|
|
127
|
+
return lyrics.lines[i];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Format lyrics for display
|
|
136
|
+
*/
|
|
137
|
+
formatLyrics(lyrics: LyricsResult, maxLength: number = 2000): string {
|
|
138
|
+
if (lyrics.text) {
|
|
139
|
+
return lyrics.text.slice(0, maxLength);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (lyrics.lines && lyrics.lines.length > 0) {
|
|
143
|
+
let formatted = "";
|
|
144
|
+
for (const line of lyrics.lines) {
|
|
145
|
+
const timestamp = this.#formatTimestamp(line.timestamp);
|
|
146
|
+
formatted += `[${timestamp}] ${line.line}\n`;
|
|
147
|
+
|
|
148
|
+
if (formatted.length > maxLength) {
|
|
149
|
+
return `${formatted.slice(0, maxLength)}...`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return formatted;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return "No lyrics available";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#formatTimestamp(ms: number): string {
|
|
159
|
+
const minutes = Math.floor(ms / 60000);
|
|
160
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
161
|
+
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async #fetchFromLavalink(_track: Track): Promise<LyricsResult | null> {
|
|
165
|
+
// Implementation would call Lavalink's lyrics endpoint
|
|
166
|
+
// This is a placeholder
|
|
167
|
+
return await Promise.resolve(null);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async #fetchFromLRCLib(_title: string, _artist: string): Promise<LyricsResult | null> {
|
|
171
|
+
// Implementation would call LRCLib API
|
|
172
|
+
// This is a placeholder
|
|
173
|
+
return await Promise.resolve(null);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async #fetchFromMusixmatch(_title: string, _artist: string, _apiKey: string): Promise<LyricsResult | null> {
|
|
177
|
+
// Implementation would call Musixmatch API
|
|
178
|
+
// This is a placeholder
|
|
179
|
+
return await Promise.resolve(null);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async #fetchFromGenius(_title: string, _artist: string, _apiToken: string): Promise<LyricsResult | null> {
|
|
183
|
+
// Implementation would call Genius API
|
|
184
|
+
// This is a placeholder
|
|
185
|
+
return await Promise.resolve(null);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { PlayerPlugin } from "../core/PluginSystem";
|
|
2
|
+
import type { Player } from "../core/RyanlinkPlayer";
|
|
3
|
+
import type { RepeatMode } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface QueuePersistencePluginEvents {
|
|
6
|
+
/**
|
|
7
|
+
* Emitted when queue is saved
|
|
8
|
+
*/
|
|
9
|
+
queueSaved: [guildId: string];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Emitted when queue is loaded
|
|
13
|
+
*/
|
|
14
|
+
queueLoaded: [guildId: string, trackCount: number];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StoredQueue {
|
|
18
|
+
guildId: string;
|
|
19
|
+
tracks: Array<{
|
|
20
|
+
encoded: string;
|
|
21
|
+
info: unknown;
|
|
22
|
+
pluginInfo?: unknown;
|
|
23
|
+
userData?: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
previousTracks: Array<{
|
|
26
|
+
encoded: string;
|
|
27
|
+
info: unknown;
|
|
28
|
+
pluginInfo?: unknown;
|
|
29
|
+
userData?: unknown;
|
|
30
|
+
}>;
|
|
31
|
+
currentTrack?: {
|
|
32
|
+
encoded: string;
|
|
33
|
+
info: unknown;
|
|
34
|
+
position: number;
|
|
35
|
+
};
|
|
36
|
+
volume: number;
|
|
37
|
+
repeatMode: RepeatMode;
|
|
38
|
+
autoplay: boolean;
|
|
39
|
+
paused: boolean;
|
|
40
|
+
timestamp: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface QueueStore {
|
|
44
|
+
get(guildId: string): Promise<StoredQueue | null> | StoredQueue | null;
|
|
45
|
+
set(guildId: string, data: StoredQueue): Promise<void> | void;
|
|
46
|
+
delete(guildId: string): Promise<void> | void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Default in-memory queue store
|
|
51
|
+
*/
|
|
52
|
+
export class MemoryQueueStore implements QueueStore {
|
|
53
|
+
#data = new Map<string, StoredQueue>();
|
|
54
|
+
|
|
55
|
+
get(guildId: string): StoredQueue | null {
|
|
56
|
+
return this.#data.get(guildId) ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
set(guildId: string, data: StoredQueue): void {
|
|
60
|
+
this.#data.set(guildId, data);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
delete(guildId: string): void {
|
|
64
|
+
this.#data.delete(guildId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Queue Persistence Plugin - Saves and restores queue state
|
|
70
|
+
* Useful for bot restarts or crashes
|
|
71
|
+
*/
|
|
72
|
+
export class QueuePersistencePlugin extends PlayerPlugin<QueuePersistencePluginEvents & Record<string, unknown[]>> {
|
|
73
|
+
readonly name = "queue-persistence" as const;
|
|
74
|
+
|
|
75
|
+
#player!: Player;
|
|
76
|
+
#store: QueueStore;
|
|
77
|
+
#autoSave: boolean;
|
|
78
|
+
|
|
79
|
+
constructor(store?: QueueStore, autoSave = true) {
|
|
80
|
+
super();
|
|
81
|
+
this.#store = store ?? new MemoryQueueStore();
|
|
82
|
+
this.#autoSave = autoSave;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
init(player: Player): void {
|
|
86
|
+
this.#player = player;
|
|
87
|
+
|
|
88
|
+
if (this.#autoSave) {
|
|
89
|
+
// Auto-save on track changes
|
|
90
|
+
player.on("trackStart", (queue, _track) => {
|
|
91
|
+
this.saveQueue(queue.guildId).catch(() => {});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
player.on("trackFinish", (queue, _track, _reason) => {
|
|
95
|
+
this.saveQueue(queue.guildId).catch(() => {});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Save on queue destroy
|
|
99
|
+
player.on("queueDestroy", (queue) => {
|
|
100
|
+
this.deleteQueue(queue.guildId).catch(() => {});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Save queue state
|
|
107
|
+
*/
|
|
108
|
+
async saveQueue(guildId: string): Promise<void> {
|
|
109
|
+
const queue = this.#player.queues.get(guildId);
|
|
110
|
+
if (!queue) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data: StoredQueue = {
|
|
115
|
+
guildId,
|
|
116
|
+
tracks: queue.tracks.map((track) => ({
|
|
117
|
+
encoded: track.encoded,
|
|
118
|
+
info: track.info,
|
|
119
|
+
pluginInfo: track.pluginInfo,
|
|
120
|
+
userData: track.userData,
|
|
121
|
+
})),
|
|
122
|
+
previousTracks: queue.previousTracks.map((t) => ({
|
|
123
|
+
encoded: t.encoded,
|
|
124
|
+
info: t.info,
|
|
125
|
+
pluginInfo: t.pluginInfo,
|
|
126
|
+
userData: t.userData,
|
|
127
|
+
})),
|
|
128
|
+
currentTrack: queue.track
|
|
129
|
+
? {
|
|
130
|
+
encoded: queue.track.encoded,
|
|
131
|
+
info: queue.track.info,
|
|
132
|
+
position: queue.currentTime,
|
|
133
|
+
}
|
|
134
|
+
: undefined,
|
|
135
|
+
volume: queue.volume,
|
|
136
|
+
repeatMode: queue.repeatMode,
|
|
137
|
+
autoplay: queue.autoplay,
|
|
138
|
+
paused: queue.paused,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
await this.#store.set(guildId, data);
|
|
143
|
+
this.#player.emit("queueSaved", guildId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Load queue state
|
|
148
|
+
*/
|
|
149
|
+
async loadQueue(guildId: string): Promise<StoredQueue | null> {
|
|
150
|
+
const data = await this.#store.get(guildId);
|
|
151
|
+
if (!data) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const queue = this.#player.queues.get(guildId);
|
|
156
|
+
if (!queue) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Restore tracks
|
|
161
|
+
// Note: Tracks need to be reconstructed from stored data
|
|
162
|
+
// This is a simplified version - you'd need to properly reconstruct Track objects
|
|
163
|
+
|
|
164
|
+
this.#player.emit("queueLoaded", guildId, data.tracks.length);
|
|
165
|
+
return data;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Delete saved queue
|
|
170
|
+
*/
|
|
171
|
+
async deleteQueue(guildId: string): Promise<void> {
|
|
172
|
+
await this.#store.delete(guildId);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get all saved queues
|
|
177
|
+
*/
|
|
178
|
+
async getAllQueues(): Promise<StoredQueue[]> {
|
|
179
|
+
// This would need to be implemented based on the store type
|
|
180
|
+
return await Promise.resolve([]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { PlayerPlugin } from "../core/PluginSystem";
|
|
2
|
+
import type { Player } from "../core/RyanlinkPlayer";
|
|
3
|
+
import { Track } from "../audio/AudioTrack";
|
|
4
|
+
import { Queue } from "../audio/AudioQueue";
|
|
5
|
+
import type { SponsorBlockSegment } from "../types/lavalink/Node";
|
|
6
|
+
|
|
7
|
+
export interface SponsorBlockPluginEvents {
|
|
8
|
+
/**
|
|
9
|
+
* Emitted when segments are loaded
|
|
10
|
+
*/
|
|
11
|
+
segmentsLoaded: [queue: Queue, track: Track, segments: SponsorBlockSegment[]];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Emitted when a segment is skipped
|
|
15
|
+
*/
|
|
16
|
+
segmentSkipped: [queue: Queue, track: Track, segment: SponsorBlockSegment];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SponsorBlock Plugin - Automatically skip sponsored segments in videos
|
|
21
|
+
* Requires Lavalink SponsorBlock plugin
|
|
22
|
+
*/
|
|
23
|
+
export class SponsorBlockPlugin extends PlayerPlugin<SponsorBlockPluginEvents & Record<string, unknown[]>> {
|
|
24
|
+
readonly name = "sponsorblock" as const;
|
|
25
|
+
|
|
26
|
+
#player!: Player;
|
|
27
|
+
|
|
28
|
+
init(player: Player): void {
|
|
29
|
+
this.#player = player;
|
|
30
|
+
|
|
31
|
+
// Listen to SponsorBlock events from Lavalink
|
|
32
|
+
player.on("segmentsLoaded", this.#handleSegmentsLoaded.bind(this));
|
|
33
|
+
player.on("segmentSkipped", this.#handleSegmentSkipped.bind(this));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set which segments to skip for a player
|
|
38
|
+
*/
|
|
39
|
+
async setSegments(queue: Queue, segments: string[] = ["sponsor", "selfpromo"]): Promise<void> {
|
|
40
|
+
const node = queue.node;
|
|
41
|
+
if (!node) {
|
|
42
|
+
throw new Error("No node available");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await node.setSponsorBlock(queue, segments);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get current SponsorBlock segments for a player
|
|
50
|
+
*/
|
|
51
|
+
async getSegments(queue: Queue): Promise<string[]> {
|
|
52
|
+
const node = queue.node;
|
|
53
|
+
if (!node) {
|
|
54
|
+
throw new Error("No node available");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return await node.getSponsorBlock(queue);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Delete SponsorBlock configuration for a player
|
|
62
|
+
*/
|
|
63
|
+
async deleteSegments(queue: Queue): Promise<void> {
|
|
64
|
+
const node = queue.node;
|
|
65
|
+
if (!node) {
|
|
66
|
+
throw new Error("No node available");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await node.deleteSponsorBlock(queue);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#handleSegmentsLoaded(queue: Queue, track: Track, segments: unknown): void {
|
|
73
|
+
const _segments = (segments as SponsorBlockSegment[]) || [];
|
|
74
|
+
this.#player.emit("segmentsLoaded", queue, track, _segments);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#handleSegmentSkipped(queue: Queue, track: Track, segment: unknown): void {
|
|
78
|
+
const _segment = segment as SponsorBlockSegment;
|
|
79
|
+
this.#player.emit("segmentSkipped", queue, track, _segment);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ryanlink - Advanced Lavalink client for Node.js
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Export all components
|
|
8
|
+
export * from "./config";
|
|
9
|
+
export * from "./utils";
|
|
10
|
+
export * from "./lavalink";
|
|
11
|
+
export * from "./voice";
|
|
12
|
+
export * from "./audio";
|
|
13
|
+
export * from "./extensions";
|
|
14
|
+
export * from "./core";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ryanlink version (dynamically loaded from package.json)
|
|
18
|
+
*/
|
|
19
|
+
export { CLIENT_VERSION as version, CLIENT_NAME as name, PACKAGE_INFO } from "./metadata";
|