rewrite na js bo rust jest zjebany
This commit is contained in:
88
src/util/adapter.ts
Normal file
88
src/util/adapter.ts
Normal 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
51
src/util/downloader.ts
Normal 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
77
src/util/helpers.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user