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",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0"
|
"rxjs": "^7.2.0",
|
||||||
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.0.0",
|
"@nestjs/cli": "^9.0.0",
|
||||||
|
@ -11,6 +11,7 @@ import { DiscordClientModule } from './clients/discord/discord.module';
|
|||||||
import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
|
import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
|
||||||
import { CommandModule } from './commands/command.module';
|
import { CommandModule } from './commands/command.module';
|
||||||
import { DiscordConfigService } from './clients/discord/discord.config.service';
|
import { DiscordConfigService } from './clients/discord/discord.config.service';
|
||||||
|
import { PlaybackModule } from './playback/playback.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -30,6 +31,7 @@ import { DiscordConfigService } from './clients/discord/discord.config.service';
|
|||||||
CommandModule,
|
CommandModule,
|
||||||
DiscordClientModule,
|
DiscordClientModule,
|
||||||
JellyfinClientModule,
|
JellyfinClientModule,
|
||||||
|
PlaybackModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
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 { Module } from '@nestjs/common';
|
||||||
import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks';
|
import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks';
|
||||||
import { DiscordConfigService } from './discord.config.service';
|
import { DiscordConfigService } from './discord.config.service';
|
||||||
|
import { DiscordMessageService } from './discord.message.service';
|
||||||
import { DiscordVoiceService } from './discord.voice.service';
|
import { DiscordVoiceService } from './discord.voice.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [DiscordConfigService, DiscordVoiceService],
|
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||||
exports: [DiscordConfigService],
|
exports: [DiscordConfigService, DiscordMessageService],
|
||||||
})
|
})
|
||||||
export class DiscordClientModule implements OnModuleDestroy {
|
export class DiscordClientModule implements OnModuleDestroy {
|
||||||
constructor(private readonly discordVoiceService: DiscordVoiceService) {}
|
constructor(private readonly discordVoiceService: DiscordVoiceService) {}
|
||||||
|
@ -11,6 +11,8 @@ import { PlayCommand } from './play.command';
|
|||||||
import { SkipTrackCommand } from './skip.command';
|
import { SkipTrackCommand } from './skip.command';
|
||||||
import { StopPlaybackCommand } from './stop.command';
|
import { StopPlaybackCommand } from './stop.command';
|
||||||
import { SummonCommand } from './summon.command';
|
import { SummonCommand } from './summon.command';
|
||||||
|
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||||
|
import { PlaybackService } from '../playback/playback.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DiscordModule.forFeature()],
|
imports: [DiscordModule.forFeature()],
|
||||||
@ -26,6 +28,8 @@ import { SummonCommand } from './summon.command';
|
|||||||
SkipTrackCommand,
|
SkipTrackCommand,
|
||||||
StopPlaybackCommand,
|
StopPlaybackCommand,
|
||||||
SummonCommand,
|
SummonCommand,
|
||||||
|
DiscordMessageService,
|
||||||
|
PlaybackService,
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
})
|
})
|
||||||
|
@ -7,17 +7,37 @@ import {
|
|||||||
UsePipes,
|
UsePipes,
|
||||||
} from '@discord-nestjs/core';
|
} from '@discord-nestjs/core';
|
||||||
import { InteractionReplyOptions } from 'discord.js';
|
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({
|
@Command({
|
||||||
name: 'enqueue',
|
name: 'enqueue',
|
||||||
description: 'Enqueue a track to the current playlist',
|
description: 'Enqueue a track to the current playlist',
|
||||||
})
|
})
|
||||||
@UsePipes(TransformPipe)
|
@UsePipes(TransformPipe)
|
||||||
export class EnqueueCommand implements DiscordTransformedCommand<unknown> {
|
export class EnqueueCommand
|
||||||
|
implements DiscordTransformedCommand<TrackRequestDto>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private readonly discordMessageService: DiscordMessageService,
|
||||||
|
private readonly playbackService: PlaybackService,
|
||||||
|
) {}
|
||||||
|
|
||||||
handler(
|
handler(
|
||||||
dto: unknown,
|
dto: TrackRequestDto,
|
||||||
executionContext: TransformedCommandExecutionContext<any>,
|
executionContext: TransformedCommandExecutionContext<any>,
|
||||||
): InteractionReplyOptions | string {
|
): 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,
|
TransformedCommandExecutionContext,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
} from '@discord-nestjs/core';
|
} from '@discord-nestjs/core';
|
||||||
import { InteractionReplyOptions, MessagePayload } from 'discord.js';
|
import {
|
||||||
|
EmbedBuilder,
|
||||||
|
GuildMember,
|
||||||
|
InteractionReplyOptions,
|
||||||
|
MessagePayload,
|
||||||
|
} from 'discord.js';
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrackRequestDto } from '../models/track-request.dto';
|
import { TrackRequestDto } from '../models/track-request.dto';
|
||||||
@ -14,8 +19,10 @@ import {
|
|||||||
createAudioPlayer,
|
createAudioPlayer,
|
||||||
createAudioResource,
|
createAudioResource,
|
||||||
getVoiceConnection,
|
getVoiceConnection,
|
||||||
|
joinVoiceChannel,
|
||||||
} from '@discordjs/voice';
|
} from '@discordjs/voice';
|
||||||
import { Logger } from '@nestjs/common/services';
|
import { Logger } from '@nestjs/common/services';
|
||||||
|
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'play',
|
name: 'play',
|
||||||
@ -26,6 +33,8 @@ import { Logger } from '@nestjs/common/services';
|
|||||||
export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
|
export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
|
||||||
private readonly logger = new Logger(PlayCommand.name);
|
private readonly logger = new Logger(PlayCommand.name);
|
||||||
|
|
||||||
|
constructor(private readonly discordMessageService: DiscordMessageService) {}
|
||||||
|
|
||||||
handler(
|
handler(
|
||||||
@Payload() dto: TrackRequestDto,
|
@Payload() dto: TrackRequestDto,
|
||||||
executionContext: TransformedCommandExecutionContext<any>,
|
executionContext: TransformedCommandExecutionContext<any>,
|
||||||
@ -35,27 +44,53 @@ export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
|
|||||||
| MessagePayload
|
| MessagePayload
|
||||||
| InteractionReplyOptions
|
| InteractionReplyOptions
|
||||||
| Promise<string | void | 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) => {
|
const channel = guildMember.voice.channel;
|
||||||
this.logger.error(error);
|
|
||||||
|
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);
|
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);
|
connection.subscribe(player);
|
||||||
|
|
||||||
player.play(resource);
|
player.play(resource);
|
||||||
player.unpause();
|
player.unpause();
|
||||||
|
|
||||||
return 'Playing Audio...';
|
return {
|
||||||
|
embeds: [new EmbedBuilder().setTitle(`Playing ${dto.search}`).toJSON()],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,9 @@ import {
|
|||||||
CommandInteraction,
|
CommandInteraction,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
GuildMember,
|
GuildMember,
|
||||||
InteractionReplyOptions
|
InteractionReplyOptions,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
import { DefaultJellyfinColor } from '../types/colors';
|
import { DefaultJellyfinColor, ErrorJellyfinColor } from '../types/colors';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'summon',
|
name: 'summon',
|
||||||
@ -26,7 +26,7 @@ export class SummonCommand implements DiscordCommand {
|
|||||||
return {
|
return {
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
.setColor(DefaultJellyfinColor)
|
.setColor(ErrorJellyfinColor)
|
||||||
.setAuthor({
|
.setAuthor({
|
||||||
name: 'Unable to join your channel',
|
name: 'Unable to join your channel',
|
||||||
iconURL:
|
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
|
ts-node: ^10.0.0
|
||||||
tsconfig-paths: 4.1.0
|
tsconfig-paths: 4.1.0
|
||||||
typescript: ^4.7.4
|
typescript: ^4.7.4
|
||||||
|
uuid: ^9.0.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -7246,7 +7247,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"uuid@npm:9.0.0":
|
"uuid@npm:9.0.0, uuid@npm:^9.0.0":
|
||||||
version: 9.0.0
|
version: 9.0.0
|
||||||
resolution: "uuid@npm:9.0.0"
|
resolution: "uuid@npm:9.0.0"
|
||||||
bin:
|
bin:
|
||||||
|
Loading…
Reference in New Issue
Block a user