mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-23 18:21:55 +01:00
✨ Add playback manager
This commit is contained in:
parent
ca10c0fd6d
commit
17eee92404
@ -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",
|
||||
|
@ -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],
|
||||
|
50
src/clients/discord/discord.message.service.ts
Normal file
50
src/clients/discord/discord.message.service.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
|
@ -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: [],
|
||||
})
|
||||
|
@ -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}\`\``,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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()],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
10
src/playback/playback.module.ts
Normal file
10
src/playback/playback.module.ts
Normal 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 {}
|
51
src/playback/playback.service.ts
Normal file
51
src/playback/playback.service.ts
Normal 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
9
src/types/playlist.ts
Normal 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
1
src/types/track.ts
Normal file
@ -0,0 +1 @@
|
||||
export interface Track {}
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user