Add playback manager

This commit is contained in:
Manuel Ruwe 2022-12-16 23:17:26 +01:00
parent ca10c0fd6d
commit 17eee92404
13 changed files with 207 additions and 22 deletions

View File

@ -38,7 +38,8 @@
"libsodium-wrappers": "^0.7.10",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0"
"rxjs": "^7.2.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",

View File

@ -11,6 +11,7 @@ import { DiscordClientModule } from './clients/discord/discord.module';
import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
import { CommandModule } from './commands/command.module';
import { DiscordConfigService } from './clients/discord/discord.config.service';
import { PlaybackModule } from './playback/playback.module';
@Module({
imports: [
@ -30,6 +31,7 @@ import { DiscordConfigService } from './clients/discord/discord.config.service';
CommandModule,
DiscordClientModule,
JellyfinClientModule,
PlaybackModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { APIEmbed, EmbedBuilder } from 'discord.js';
import { DefaultJellyfinColor, ErrorJellyfinColor } from '../../types/colors';
@Injectable()
export class DiscordMessageService {
buildErrorMessage({
title,
description,
}: {
title: string;
description?: string;
}): APIEmbed {
const embedBuilder = new EmbedBuilder()
.setColor(ErrorJellyfinColor)
.setAuthor({
name: title,
iconURL:
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/alert-circle.png?raw=true',
});
if (description !== undefined) {
embedBuilder.setDescription(description);
}
return embedBuilder.toJSON();
}
buildMessage({
title,
description,
}: {
title: string;
description?: string;
}): APIEmbed {
const embedBuilder = new EmbedBuilder()
.setColor(DefaultJellyfinColor)
.setAuthor({
name: title,
iconURL:
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/circle-check.png?raw=true',
});
if (description !== undefined) {
embedBuilder.setDescription(description);
}
return embedBuilder.toJSON();
}
}

View File

@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks';
import { DiscordConfigService } from './discord.config.service';
import { DiscordMessageService } from './discord.message.service';
import { DiscordVoiceService } from './discord.voice.service';
@Module({
imports: [],
controllers: [],
providers: [DiscordConfigService, DiscordVoiceService],
exports: [DiscordConfigService],
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
exports: [DiscordConfigService, DiscordMessageService],
})
export class DiscordClientModule implements OnModuleDestroy {
constructor(private readonly discordVoiceService: DiscordVoiceService) {}

View File

@ -11,6 +11,8 @@ import { PlayCommand } from './play.command';
import { SkipTrackCommand } from './skip.command';
import { StopPlaybackCommand } from './stop.command';
import { SummonCommand } from './summon.command';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service';
@Module({
imports: [DiscordModule.forFeature()],
@ -26,6 +28,8 @@ import { SummonCommand } from './summon.command';
SkipTrackCommand,
StopPlaybackCommand,
SummonCommand,
DiscordMessageService,
PlaybackService,
],
exports: [],
})

View File

@ -7,17 +7,37 @@ import {
UsePipes,
} from '@discord-nestjs/core';
import { InteractionReplyOptions } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { TrackRequestDto } from '../models/track-request.dto';
import { PlaybackService } from '../playback/playback.service';
@Command({
name: 'enqueue',
description: 'Enqueue a track to the current playlist',
})
@UsePipes(TransformPipe)
export class EnqueueCommand implements DiscordTransformedCommand<unknown> {
export class EnqueueCommand
implements DiscordTransformedCommand<TrackRequestDto>
{
constructor(
private readonly discordMessageService: DiscordMessageService,
private readonly playbackService: PlaybackService,
) {}
handler(
dto: unknown,
dto: TrackRequestDto,
executionContext: TransformedCommandExecutionContext<any>,
): InteractionReplyOptions | string {
return 'nice';
const index = this.playbackService.eneuqueTrack({});
return {
embeds: [
this.discordMessageService.buildMessage({
title: `Track Added to queue`,
description: `Your track \`\`${
dto.search
}\`\` was added to the queue at position \`\`${index + 1}\`\``,
}),
],
};
}
}

View File

@ -6,7 +6,12 @@ import {
TransformedCommandExecutionContext,
UsePipes,
} from '@discord-nestjs/core';
import { InteractionReplyOptions, MessagePayload } from 'discord.js';
import {
EmbedBuilder,
GuildMember,
InteractionReplyOptions,
MessagePayload,
} from 'discord.js';
import { Injectable } from '@nestjs/common';
import { TrackRequestDto } from '../models/track-request.dto';
@ -14,8 +19,10 @@ import {
createAudioPlayer,
createAudioResource,
getVoiceConnection,
joinVoiceChannel,
} from '@discordjs/voice';
import { Logger } from '@nestjs/common/services';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
@Command({
name: 'play',
@ -26,6 +33,8 @@ import { Logger } from '@nestjs/common/services';
export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
private readonly logger = new Logger(PlayCommand.name);
constructor(private readonly discordMessageService: DiscordMessageService) {}
handler(
@Payload() dto: TrackRequestDto,
executionContext: TransformedCommandExecutionContext<any>,
@ -35,27 +44,53 @@ export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
| MessagePayload
| InteractionReplyOptions
| Promise<string | void | MessagePayload | InteractionReplyOptions> {
const player = createAudioPlayer();
const guildMember = executionContext.interaction.member as GuildMember;
this.logger.debug('bruh');
if (guildMember.voice.channel === null) {
return {
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'Unable to join your channel',
description:
'You are in a channel, I am either unabelt to connect to or you aren&apost in a channel yet',
}),
],
};
}
player.on('error', (error) => {
this.logger.error(error);
const channel = guildMember.voice.channel;
joinVoiceChannel({
channelId: channel.id,
adapterCreator: channel.guild.voiceAdapterCreator,
guildId: channel.guildId,
});
player.on('debug', (error) => {
this.logger.debug(error);
});
const resource = createAudioResource(dto.search);
const connection = getVoiceConnection(executionContext.interaction.guildId);
if (!connection) {
return {
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'Unable to establish audio connection',
description:
'I was unable to establish an audio connection to your voice channel',
}),
],
};
}
const player = createAudioPlayer();
const resource = createAudioResource(dto.search);
connection.subscribe(player);
player.play(resource);
player.unpause();
return 'Playing Audio...';
return {
embeds: [new EmbedBuilder().setTitle(`Playing ${dto.search}`).toJSON()],
};
}
}

View File

@ -7,9 +7,9 @@ import {
CommandInteraction,
EmbedBuilder,
GuildMember,
InteractionReplyOptions
InteractionReplyOptions,
} from 'discord.js';
import { DefaultJellyfinColor } from '../types/colors';
import { DefaultJellyfinColor, ErrorJellyfinColor } from '../types/colors';
@Command({
name: 'summon',
@ -26,7 +26,7 @@ export class SummonCommand implements DiscordCommand {
return {
embeds: [
new EmbedBuilder()
.setColor(DefaultJellyfinColor)
.setColor(ErrorJellyfinColor)
.setAuthor({
name: 'Unable to join your channel',
iconURL:

View File

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { PlaybackService } from './playback.service';
@Module({
imports: [],
controllers: [],
providers: [PlaybackService],
exports: [PlaybackService],
})
export class PlaybackModule {}

View File

@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { Playlist } from '../types/playlist';
import { Track } from '../types/track';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class PlaybackService {
private readonly playlist: Playlist = {
tracks: [],
activeTrack: null,
};
getActiveTrack() {
return this.getTrackById(this.playlist.activeTrack);
}
setActiveTrack(trackId: string) {
const track = this.getTrackById(trackId);
if (!track) {
throw Error('track is not in playlist');
}
this.playlist.activeTrack = track.id;
}
eneuqueTrack(track: Track) {
const uuid = uuidv4();
this.playlist.tracks.push({
id: uuid,
track: track,
});
return this.playlist.tracks.findIndex((x) => x.id === uuid);
}
set(tracks: Track[]) {
this.playlist.tracks = tracks.map((t) => ({
id: uuidv4(),
track: t,
}));
}
clear() {
this.playlist.tracks = [];
}
private getTrackById(id: string) {
return this.playlist.tracks.find((x) => x.id === id);
}
}

9
src/types/playlist.ts Normal file
View File

@ -0,0 +1,9 @@
import { Track } from './track';
export interface Playlist {
tracks: {
id: string;
track: Track;
}[];
activeTrack: string | null;
}

1
src/types/track.ts Normal file
View File

@ -0,0 +1 @@
export interface Track {}

View File

@ -4487,6 +4487,7 @@ __metadata:
ts-node: ^10.0.0
tsconfig-paths: 4.1.0
typescript: ^4.7.4
uuid: ^9.0.0
languageName: unknown
linkType: soft
@ -7246,7 +7247,7 @@ __metadata:
languageName: node
linkType: hard
"uuid@npm:9.0.0":
"uuid@npm:9.0.0, uuid@npm:^9.0.0":
version: 9.0.0
resolution: "uuid@npm:9.0.0"
bin: