Ir para o conteúdo principal

Lavalink

Transforme seu bot em um DJ profissional com o poder do ecossistema Lavalink. Este pacote usa lavalink-client nos bastidores, oferecendo uma solução de alto desempenho e eficiente para o gerenciamento de fluxos de áudio no Discord. Aproveitando o Lavalink, o seu bot ganha a capacidade de gerenciar a reprodução de áudio, filas, e controles em tempo real, com latência mínima, transformando-o em um sistema de música totalmente capaz e profissional.

Instalação

npm i @necord/lavalink necord discord.js lavalink-client

Utilização

Uma vez que o processo de instalação estiver concluído, nós podemos importar o NecordLavalinkModule com o seu NecordModule para a raiz AppModule:

app.module.ts
import { NecordLavalinkModule } from '@necord/lavalink';
import { Module } from '@nestjs/common';
import { Client } from 'discord.js';
import { AppService } from './app.service';

@Module({
imports: [
NecordModule.forRoot({
token: process.env.DISCORD_TOKEN,
intents: [
IntentsBitField.Flags.Guilds,
IntentsBitField.Flags.GuildVoiceStates
],
}),
NecordLavalinkModule.forRoot({
// Pelo menos 1 node é necessário
nodes: [
{
authorization: 'youshallnotpass',
host: 'localhost',
port: 2333,
}
]
})
],
providers: [AppService]
})
export class AppModule {}

Confira mais opções do módulo na documentação oficial lavalink-client.

Ouvintes

Em '@necord/lavalink' o manuseio de eventos funciona o mesmo que os ouvintes Necord padrão com algumas alterações.

  • Em vez de decoradores On/Once e ContextOf padrão você pode usar
app.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Context } from 'necord';
import { OnLavalinkManager, OnNodeManager, LavalinkManagerContextOf, NodeManagerContextOf } from '@necord/lavalink';

@Injectable()
export class AppService {
private readonly logger = new Logger(AppService.name);

@OnNodeManager('connect')
public onConnect(@Context() [node]: NodeManagerContextOf<'connect'>) {
this.logger.log(`Node: ${node.options.id} Conectado`);
}

@OnLavalinkManager('playerCreate')
public onPlayerCreate(@Context() [player]: LavalinkManagerContextOf<'playerCreate'>) {
this.logger.log(`Player criado em ${player.guildId}`);
}
}

Eventos do LavalinkManager

Nome do eventoDescrição
trackStartEmitido sempre que uma Faixa é reproduzida.
trackEndEmitido quando uma Faixa terminou de tocar.
trackStuckEmitido quando uma Faixa travou.
trackErrorEmitido sempre que ocorre um erro em uma Faixa.
queueEndEmitido quando a faixa terminou, mas não há mais faixas na fila. (trackEnd, NÃO é executado)
playerCreateEmitido sempre que um Player é criado.
playerMoveEmitido sempre que um Player é movido entre os canais de voz.
playerDisconnectEmitido sempre que um Player é desconectado de um canal.
playerSocketCloseEmitido quando um Node-Socket foi fechado para um Player específico.
playerDestroyEmitido sempre que um Player é destruído.
playerUpdateEmitido sempre que um Player recebe uma atualização do evento playerUpdate do Lavalink.
playerMuteChangeEmitido sempre que o estado de voz do Player relacionado ao silenciamento é alterado.
playerDeafChangeEmitido sempre que o estado de voz do Player relacionado ao ensurdecimento é alterado.
playerSuppressChangeEmitido sempre que o estado de voz do Player relacionado à supressão é alterado.
playerQueueEmptyStartEmitido sempre que o manipulador vazio da fila começou (timeout).
playerQueueEmptyEndEmitido quando o manipulador vazio da fila terminou (com sucesso) e destruiu o Player.
playerQueueEmptyCancelEmitido sempre que o manipulador vazio da fila foi cancelado (por exemplo, porque uma nova faixa foi adicionada).
playerVoiceJoinEmitido sempre que um usuário entra em um canal de voz do Player.
playerVoiceLeaveEmitido sempre que um usuário sai do canal de voz do Player.
debugEmitido por vários erros, e logs dentro de lavalink-client, se managerOptions.advancedOptions.enableDebugEvents é true.

Eventos do Plugin SponsorBlock

Nome do eventoDescrição
SegmentsLoadedEmitido quando Segmentos são carregados.
SegmentSkippedEmitido quando um Segmento específico foi ignorado.
ChapterStartedEmitido quando um Capítulo específico começa a ser reproduzido.
ChaptersLoadedEmitido quando Capítulos são carregados.

Eventos do Plugin LavaLyrics

Nome do eventoDescrição
LyricsLineEmitido quando uma linha de Letra é recebida.
LyricsFoundEmitido quando uma Letra é encontrada.
LyricsNotFoundEmitido quando uma Letra não é encontrada.

Eventos do NodeManager

Nome do eventoDescrição
createEmitido sempre que um Node é criado.
destroyEmitido sempre que um Node é destruído.
connectEmitido sempre que um Node é conectado.
reconnectingEmitido sempre que um Node está se reconectando.
reconnectinprogressEmitido sempre que um Node começa a reconectar. (se você tiver um atraso de reconexão, o evento de reconexão será emitido após retryDelay.) Útil para verificar se o sistema de reconexão de Node interno funciona ou não.
disconnectEmitido sempre que um Node é desconectado.
errorEmitido sempre que ocorre um erro em um Node.
rawEmite cada evento do Node.

Provedores

  • @necord/lavalink tem snippets para acessar os métodos do cliente lavalink. Você pode injetar os gerenciadores usando construtor.
app.service.ts
import { Injectable } from '@nestjs/common';
import { LavalinkManager, NodeManager } from 'lavalink-client';

@Injectable()
export class AppService {
public constructor(
private readonly lavalinkManager: LavalinkManager,
private readonly nodeManager: NodeManager,
) {}
}
Classe (Tipo para ser Injetado)Propriedade do Gerenciador (será acesso)Descrição
LavalinkManagerlavalinkManagerLavalink Manager
NodeManagerlavalinkManager.nodeManagerNode Manager
PlayerManagerlavalinkManager (player functions)Gerenciador de Player

Reproduzir

app.commands.ts
import { Injectable, UseInterceptors } from '@nestjs/common';
import { NecordLavalinkService, PlayerManager } from 'lavalink-client';
import { Context, Options, SlashCommand, SlashCommandContext } from 'necord';
import { QueryDto } from './query.dto';
import { SourceAutocompleteInterceptor } from 'source.autocomplete';

@Injectable()
export class AppCommands {
public constructor(
private readonly playerManager: PlayerManager,
private readonly lavalinkService: NecordLavalinkService
) {}

@UseInterceptors(SourceAutocompleteInterceptor)
@SlashCommand({
name: 'tocar',
description: 'toca uma faixa',
})
public async onPlay(
@Context() [interaction]: SlashCommandContext,
@Options() { query, source }: QueryDto,
) {
const player =
this.playerManager.get(interaction.guild.id) ??
this.playerManager.create({
...this.lavalinkService.extractInfoForPlayer(interaction),
// configurações opcionais:
selfDeaf: true,
selfMute: false,
volume: 100,
});

await player.connect();

const res = await player.search(
{
query,
source: source ?? 'soundcloud'
},
interaction.user.id,
);

await player.queue.add(res.tracks[0]);
if (!player.playing) await player.play();

// É extremamente recomendado utilizar o evento `trackStart` para realizar esse anuncio
return interaction.reply({
content: `Tocando agora ${res.tracks[0].info.title}`,
});
}
}
query.dto.ts
import { SearchPlatform } from 'lavalink-client';
import { StringOption } from 'necord';

export class QueryDto {
@StringOption({
name: 'busca',
description: '<nome | url> da faixa requerida'
required: true
})
public readonly query!: string;

@StringOption({
name: 'fonte',
description: 'fonte da faixa',
autocomplete: true,
required: false,
})
public readonly source?: SourcePlatform;
}
source.autocomplete.ts
import { Injectable } from '@nestjs/common';
import { AutocompleteInteraction } from 'discord.js';
import { DefaultSources } from 'lavalink-client';
import { AutocompleteInterceptor } from 'necord';

@Injectable()
export class SourceAutocompleteInterceptor extends AutocompleteInterceptor {
public transformOptions(interaction: AutocompleteInteraction) {
const focused = interaction.options.getFocused(true);
let choices: string[];

if (focused.name === 'source') {
choices = [DefaultSources.soundcloud] // Note que alguns Sources requerem plugins ou configurações extras para funcionar corretamente
}

return interaction.respond(
choices
.filter((choice) => choice.startsWith(focused.value.toString()))
.map((choice) => ({ name: choice, value: choice })),
);
}
}

Você pode ver um exemplo funcional aqui.