rewrite na js bo rust jest zjebany

This commit is contained in:
Patryk Koreń
2025-12-26 18:31:14 +01:00
parent 7b72b15caa
commit 375779de9c
23 changed files with 1259 additions and 3827 deletions

88
src/util/adapter.ts Normal file
View File

@@ -0,0 +1,88 @@
import type { DiscordGatewayAdapterCreator, DiscordGatewayAdapterLibraryMethods } from '@discordjs/voice';
import {
Events,
GatewayDispatchEvents,
type Client,
type GatewayVoiceServerUpdateDispatchData,
type GatewayVoiceStateUpdateDispatchData,
type Guild,
type VoiceBasedChannel,
type Snowflake,
Status,
} from 'discord.js';
const adapters = new Map<Snowflake, DiscordGatewayAdapterLibraryMethods>();
const trackedClients = new Set<Client>();
const trackedShards = new Map<number, Set<Snowflake>>();
/**
* Tracks a Discord.js client, listening to VOICE_SERVER_UPDATE and VOICE_STATE_UPDATE events
*
* @param client - The Discord.js Client to track
*/
function trackClient(client: Client) {
if (trackedClients.has(client)) return;
trackedClients.add(client);
client.ws.on(GatewayDispatchEvents.VoiceServerUpdate, (payload: GatewayVoiceServerUpdateDispatchData) => {
adapters.get(payload.guild_id)?.onVoiceServerUpdate(payload);
});
client.ws.on(GatewayDispatchEvents.VoiceStateUpdate, (payload: GatewayVoiceStateUpdateDispatchData) => {
if (payload.guild_id && payload.session_id && payload.user_id === client.user?.id) {
adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
}
});
client.on(Events.ShardDisconnect, (_, shardId) => {
const guilds = trackedShards.get(shardId);
if (guilds) {
for (const guildId of guilds.values()) {
adapters.get(guildId)?.destroy();
}
}
trackedShards.delete(shardId);
});
}
function trackGuild(guild: Guild) {
let guilds = trackedShards.get(guild.shardId);
if (!guilds) {
guilds = new Set();
trackedShards.set(guild.shardId, guilds);
}
guilds.add(guild.id);
}
/**
* Creates an adapter for a Voice Channel.
*
* @param channel - The channel to create the adapter for
*/
export function createDiscordJSAdapter(channel: VoiceBasedChannel): DiscordGatewayAdapterCreator {
return (methods) => {
adapters.set(channel.guild.id, methods);
trackClient(channel.client);
trackGuild(channel.guild);
return {
sendPayload(data) {
if (channel.guild.shard.status !== Status.Ready) return false;
channel.guild.shard.send(data);
return true;
},
destroy() {
adapters.delete(channel.guild.id);
},
};
};
}

51
src/util/downloader.ts Normal file
View File

@@ -0,0 +1,51 @@
import { spawn } from "node:child_process";
export async function get_audio(url: string): Promise<string> {
const ytDlpBin = process.env.YT_DLP_BIN_PATH! ?? "yt-dlp";
const dataDir = "./data";
const idMatch = /[?&]v=([a-zA-Z0-9_-]{11})/.exec(url);
if (!idMatch) throw new Error("Cannot extract video ID");
const id = idMatch[1];
console.log(`ID: ${id}`);
// ---- run yt-dlp ----
await new Promise<void>((resolve, reject) => {
const p = spawn(ytDlpBin, [
"-x",
"--audio-format", "opus",
"--audio-quality", "0",
"--no-playlist",
"--paths", dataDir,
url,
]);
p.stderr.on("data", d => process.stderr.write(d));
p.on("close", code => code === 0 ? resolve() : reject(new Error("yt-dlp failed")));
});
// ---- find the file ----
return await new Promise<string>((resolve, reject) => {
const find = spawn("find", [
dataDir,
"-type", "f",
"-iname", `*${id}*.opus`,
"-exec", "readlink", "-f", "{}", ";",
]);
let out = "";
find.stdout.on("data", d => out += d.toString());
find.stderr.on("data", d => process.stderr.write(d));
find.on("close", code => {
if (code !== 0 || !out.trim()) {
reject(new Error("Audio file not found"));
} else {
resolve(out.trim());
}
});
});
}

77
src/util/helpers.ts Normal file
View File

@@ -0,0 +1,77 @@
import {
AudioPlayerStatus,
StreamType,
VoiceConnectionStatus,
createAudioResource,
entersState,
joinVoiceChannel,
type AudioPlayer,
} from '@discordjs/voice';
import type { VoiceBasedChannel } from 'discord.js';
import { createDiscordJSAdapter } from './adapter.js';
export async function connectToChannel(channel: VoiceBasedChannel) {
/**
* Here, we try to establish a connection to a voice channel. If we're already connected
* to this voice channel, \@discordjs/voice will just return the existing connection for us!
*/
const connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: createDiscordJSAdapter(channel),
});
/**
* If we're dealing with a connection that isn't yet Ready, we can set a reasonable
* time limit before giving up. In this example, we give the voice connection 30 seconds
* to enter the ready state before giving up.
*/
try {
/**
* Allow ourselves 30 seconds to join the voice channel. If we do not join within then,
* an error is thrown.
*/
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
/**
* At this point, the voice connection is ready within 30 seconds! This means we can
* start playing audio in the voice channel. We return the connection so it can be
* used by the caller.
*/
return connection;
} catch (error) {
/**
* At this point, the voice connection has not entered the Ready state. We should make
* sure to destroy it, and propagate the error by throwing it, so that the calling function
* is aware that we failed to connect to the channel.
*/
connection.destroy();
throw error;
}
}
export async function playSong(player: AudioPlayer, songUrl: string) {
/**
* Here we are creating an audio resource using a sample song freely available online
* (see https://www.soundhelix.com/audio-examples)
*
* We specify an arbitrary inputType. This means that we aren't too sure what the format of
* the input is, and that we'd like to have this converted into a format we can use. If we
* were using an Ogg or WebM source, then we could change this value. However, for now we
* will leave this as arbitrary.
*/
const resource = createAudioResource(songUrl, { inputType: StreamType.Arbitrary });
/**
* We will now play this to the audio player. By default, the audio player will not play until
* at least one voice connection is subscribed to it, so it is fine to attach our resource to the
* audio player this early.
*/
player.play(resource);
/**
* Here we are using a helper function. It will resolve if the player enters the Playing
* state within 5 seconds, otherwise it will reject with an error.
*/
return entersState(player, AudioPlayerStatus.Playing, 5_000);
}