Denim-Bot/src/utils/voice.ts
2023-08-20 18:20:03 +02:00

254 lines
7.7 KiB
TypeScript

import { AudioPlayer, AudioPlayerState, AudioPlayerStatus, AudioResource, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus } from "@discordjs/voice";
import { Guild } from "discord.js";
import { log } from "./utils";
import { Readable } from "node:stream";
export type Hratelny = string | Readable | {
src: string | Readable;
volume: number;
type?: StreamType;
};
type Vojska = {
conn: VoiceConnection;
players: [AudioPlayer, AudioPlayer, AudioPlayer];
queues: [AudioResource[], AudioResource[], AudioResource[]];
};
export const enum Priority {
Time,
Etc,
Music
}
const vojsy = new Map<string, Vojska>();
let lastId = 0;
const getId = () => `Id:${lastId++}`;
/**
* @returns Předchozí stav bota, před tímto příkazem
* `true` znamená, že byl bot do tohoto kanelu již připojen
* `false` znamená, že nebyl připojen nikam
* `channelId` znamená, že v tutý gildě byl předtím připojenej do kanelu s tímto ID
*/
export const novejJoin = (guild: Guild, channelId: string) => new Promise<boolean | string>(async (resolve, reject) => {
const guildId = guild.id;
let prev: false | string = false;
const gildina = vojsy.get(guildId);
if (gildina) {
const con = gildina.conn;
if (con.joinConfig.channelId == channelId) {
if (con.state.status != VoiceConnectionStatus.Ready) {
log(`pri pripojovani do vojsu tady uz existovalo stejny pripojeni, ale status byl ${gildina.conn.state.status}, tak jsem to zahodil`);
novejLeave(guildId);
}
else return resolve(true);
}
else {
prev = con.joinConfig.channelId!;
if (!prev) {
log(new Error("fakt tam nic neni, jak je to mozny?"));
prev = false;
}
/* Tuto prostě nefunguje, řekl Discord. Sráč jeden. Ale já se jen tak nevzdám. Jen počkej, ty čuráku.
// Přesunout se
const state = guild.voiceStates.cache.get(guild.client.user!.id);
if (state) {
await state.setChannel(channelId);
resolve(prev);
return;
}
*/
// Z nějakýho neznámího důvodu se nepodařilo přepojit, prostě vytvoříme nový připojení
// log(new Error("jo tak tuto nechapu"));
novejLeave(guildId);
}
}
const conn = joinVoiceChannel({
channelId: channelId,
guildId,
adapterCreator: guild.voiceAdapterCreator
});
await entersState(conn, VoiceConnectionStatus.Ready, 1e4)
.catch(e => {
novejLeave(guildId);
reject(e);
});
vypocitatCas(guildId);
conn.on(VoiceConnectionStatus.Disconnected, () => {
// Je možný, že bota někdo "přetáhnul" do jinýho vojsu, v tom případě připojení recyklovat nechem
entersState(conn, VoiceConnectionStatus.Ready, 5e3).catch(() => {
// Taky je možný, že ho někdo fakt odpojil, ale hned znova připojil
// V tom případě už by bylo o recyklaci postaráno
if (conn.state.status != VoiceConnectionStatus.Disconnected) return;
log("recykluju");
novejLeave(guildId);
});
});
const player = createAudioPlayer();
conn.subscribe(player);
vojsy.set(guildId, {
conn,
players: [createAudioPlayer(), createAudioPlayer(), createAudioPlayer()],
queues: [[], [], []]
});
resolve(prev);
});
export function novejLeave(guildId: string) {
const vojs = vojsy.get(guildId);
if (!vojs) return false;
clearTimeout(timeouty.get(guildId));
timeouty.delete(guildId);
vojs.conn.destroy();
vojsy.delete(guildId);
return true;
}
/**
* Tahle funkce by se měla zavolat pokaždý, když dojde k nějaký změně v queue nebo něco dohraje.
* Řeší hovna typu, přeregistrace jinýho přehrávače, pouštění dalšího resource, a to je asi všechno
* @param vojska Vojska, o kterou se má postarat
* @param priority Priorita, na kterou se má brát ičkon ohled, je to buďto priorita nově zařazenýho zvuku, nebo prostě toho, s nejvyšší prioritou
*/
function handelieren(vojska: Vojska, priority: Priority) {
for (let i = 0; i < priority; i++) {
if (vojska.players[i].state.status != AudioPlayerStatus.Idle) return;
}
const hrac = vojska.players[priority];
vojska.conn.subscribe(hrac);
const resource = vojska.queues[priority].shift()!;
hrac.play(resource);
function nasafunkca(pred: AudioPlayerState, po: AudioPlayerState) {
if (po.status != AudioPlayerStatus.Idle || pred.status != AudioPlayerStatus.Playing || pred.resource.metadata != resource.metadata) return;
vojska?.players[priority].removeListener("stateChange", nasafunkca);
let nejprioritnejsi = -1;
for (let i = 0; i < 3; i++) {
const hrac = vojska.players[i];
if (hrac.state.status == AudioPlayerStatus.AutoPaused) {
vojska.conn.subscribe(hrac);
return;
}
if (!vojska.queues[i].length) continue;
nejprioritnejsi = i;
break;
}
if (nejprioritnejsi != -1)
handelieren(vojska, nejprioritnejsi);
}
vojska.players[priority].on("stateChange", nasafunkca);
}
function makeResource(co: Hratelny, id: string) {
if (typeof co == "string" || co instanceof Readable) return createAudioResource(co, { metadata: id });
const res = createAudioResource(co.src, { inlineVolume: true, inputType: co.type || StreamType.Arbitrary, metadata: id });
res.volume?.setVolume(co.volume);
return res;
}
function convert(resources: { id: string; res: AudioResource; }[]) {
const odpoved: AudioResource[] = [];
resources.forEach(res => {
odpoved.push(res.res);
});
return odpoved;
}
export const novejPlay = (guildId: string, co: Hratelny | Hratelny[], priority: Priority) => new Promise<AudioPlayerState>(async (resolve, reject) => {
const vojska = vojsy.get(guildId);
if (!vojska) return reject("nejsi pripojenej do vojsu debile");
const resources: { id: string; res: AudioResource; }[] = [];
if (!Array.isArray(co)) {
const id = getId();
resources.push({ id, res: makeResource(co, id) });
} else {
co.forEach(c => {
const id = getId();
resources.push({ id, res: makeResource(c, id) });
});
}
// Tuto se možná může někdy hodit, třeba nikdy, kdovi
// if (neprepisovat) {
// vojska.queues[priority].push(resource);
// } else {
// vojska.queues[priority] = [resource];
// }
vojska.queues[priority] = convert(resources);
const posledniId = resources.at(-1)?.id;
function nasafunkca(pred: AudioPlayerState, po: AudioPlayerState) {
if (!(("resource" in pred && pred.resource.metadata == posledniId) && (("resource" in po && po.resource.metadata != posledniId) || !("resource" in po)))) return;
vojska?.players[priority].removeListener("stateChange", nasafunkca);
resolve(po);
}
vojska.players[priority].on("stateChange", nasafunkca);
handelieren(vojska, priority);
});
export const handlePrevVoice = (prev: boolean | string, guild: Guild) => {
if (prev === true) return; // Byl jsem v tomdle vojsu
if (prev === false) return novejLeave(guild.id); // Nebyl jsem ve vojsu vůbec
novejJoin(guild, prev); // Byl jsem jinde
};
export function stopPlayer(guildId: string, priorita: Priority) {
const vojs = vojsy.get(guildId);
if (!vojs) return false;
vojs.players[priorita].stop();
return true;
}
export const getConn = (guildId: string) => vojsy.get(guildId)?.conn;
// Ohlašování času
const timeouty = new Map<string, NodeJS.Timeout>();
const nula = (a: number) => a < 10 ? `0${a}` : a;
const vypocitatCas = (guildId: string) => {
const d = new Date();
d.setMinutes((d.getMinutes() >= 30 ? 60 : 30));
d.setSeconds(0);
timeouty.set(guildId, setTimeout(() => rekniCas(guildId, `${nula(d.getHours())}${nula(d.getMinutes())}`), Number(d) - Number(new Date())));
};
const rekniCas = (guildId: string, cas: string) => {
novejPlay(guildId, [{ src: "zvuky/intro.mp3", volume: 0.8 }, { src: `zvuky/${cas}.mp3`, volume: 2 }, { src: "zvuky/grg.mp3", volume: 0.5 }], Priority.Time)
.then(() => {
vypocitatCas(guildId);
})
.catch(err => { log("cas error:", err); });
};