diff --git a/src/clients/discord/discord.module.ts b/src/clients/discord/discord.module.ts index e59427c..ba703a3 100644 --- a/src/clients/discord/discord.module.ts +++ b/src/clients/discord/discord.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks'; +import { PlaybackModule } from '../../playback/playback.module'; import { DiscordConfigService } from './discord.config.service'; import { DiscordMessageService } from './discord.message.service'; import { DiscordVoiceService } from './discord.voice.service'; @Module({ - imports: [], + imports: [PlaybackModule], controllers: [], providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService], exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService], diff --git a/src/clients/discord/discord.voice.service.ts b/src/clients/discord/discord.voice.service.ts index 9489091..ed204e1 100644 --- a/src/clients/discord/discord.voice.service.ts +++ b/src/clients/discord/discord.voice.service.ts @@ -1,9 +1,9 @@ import { AudioPlayer, - AudioPlayerPausedState, AudioPlayerStatus, AudioResource, createAudioPlayer, + createAudioResource, getVoiceConnection, getVoiceConnections, joinVoiceChannel, @@ -11,8 +11,11 @@ import { } from '@discordjs/voice'; import { Injectable } from '@nestjs/common'; import { Logger } from '@nestjs/common/services'; +import { OnEvent } from '@nestjs/event-emitter'; import { GuildMember } from 'discord.js'; import { GenericTryHandler } from '../../models/generic-try-handler'; +import { PlaybackService } from '../../playback/playback.service'; +import { Track } from '../../types/track'; import { DiscordMessageService } from './discord.message.service'; @Injectable() @@ -21,7 +24,16 @@ export class DiscordVoiceService { private audioPlayer: AudioPlayer; private voiceConnection: VoiceConnection; - constructor(private readonly discordMessageService: DiscordMessageService) {} + constructor( + private readonly discordMessageService: DiscordMessageService, + private readonly playbackService: PlaybackService, + ) {} + + @OnEvent('playback.newTrack') + handleOnNewTrack(newTrack: Track) { + const resource = createAudioResource(newTrack.streamUrl); + this.playResource(resource); + } tryJoinChannelAndEstablishVoiceConnection( member: GuildMember, @@ -156,6 +168,17 @@ export class DiscordVoiceService { this.audioPlayer.on('error', (message) => { this.logger.error(message); }); + this.audioPlayer.on('stateChange', (statusChange) => { + if (statusChange.status !== AudioPlayerStatus.AutoPaused) { + return; + } + + if (!this.playbackService.hasNextTrack()) { + return; + } + + this.playbackService.nextTrack(); + }); this.voiceConnection.subscribe(this.audioPlayer); return this.audioPlayer; } diff --git a/src/commands/play.comands.ts b/src/commands/play.comands.ts index 0bb6fb2..0f3f52c 100644 --- a/src/commands/play.comands.ts +++ b/src/commands/play.comands.ts @@ -29,6 +29,7 @@ import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service'; import { PlaybackService } from '../playback/playback.service'; import { Constants } from '../utils/constants'; +import { trimStringToFixedLength } from '../utils/stringUtils'; @Command({ name: 'play', @@ -69,9 +70,9 @@ export class PlayItemCommand const lines: string[] = firstItems.map( (item) => - `:white_small_square: ${this.markSearchTermOverlap( - item.Name, - dto.search, + `:white_small_square: ${trimStringToFixedLength( + this.markSearchTermOverlap(item.Name, dto.search), + 30, )} *(${item.Type})*`, ); @@ -104,7 +105,8 @@ export class PlayItemCommand return { embeds: [ this.discordMessageService.buildMessage({ - title: '', + title: 'a', + description: description, mixin(embedBuilder) { return embedBuilder.setAuthor({ name: 'Jellyfin Search Results', @@ -158,12 +160,6 @@ export class PlayItemCommand const artists = item.Artists.join(', '); - const addedIndex = this.playbackService.eneuqueTrack({ - jellyfinId: item.Id, - name: item.Name, - durationInMilliseconds: milliseconds, - }); - const guildMember = interaction.member as GuildMember; const bitrate = guildMember.voice.channel.bitrate; @@ -171,12 +167,17 @@ export class PlayItemCommand guildMember, ); - this.jellyfinStreamBuilder - .buildStreamUrl(item.Id, bitrate) - .then((stream) => { - const resource = createAudioResource(stream); - this.discordVoiceService.playResource(resource); - }); + const stream = await this.jellyfinStreamBuilder.buildStreamUrl( + item.Id, + bitrate, + ); + + const addedIndex = this.playbackService.eneuqueTrack({ + jellyfinId: item.Id, + name: item.Name, + durationInMilliseconds: milliseconds, + streamUrl: stream, + }); await interaction.update({ embeds: [ diff --git a/src/playback/playback.service.ts b/src/playback/playback.service.ts index 7a3819f..27cc2cb 100644 --- a/src/playback/playback.service.ts +++ b/src/playback/playback.service.ts @@ -3,6 +3,7 @@ import { Playlist } from '../types/playlist'; import { Track } from '../types/track'; import { v4 as uuidv4 } from 'uuid'; +import { EventEmitter2 } from '@nestjs/event-emitter'; @Injectable() export class PlaybackService { @@ -11,6 +12,8 @@ export class PlaybackService { activeTrack: null, }; + constructor(private readonly eventEmitter: EventEmitter2) {} + getActiveTrack() { return this.getTrackById(this.playlist.activeTrack); } @@ -38,6 +41,7 @@ export class PlaybackService { const newKey = keys[index + 1]; this.setActiveTrack(newKey); + this.controlAudioPlayer(); return true; } @@ -51,6 +55,7 @@ export class PlaybackService { const keys = this.getTrackIds(); const newKey = keys[index - 1]; this.setActiveTrack(newKey); + this.controlAudioPlayer(); return true; } @@ -66,6 +71,7 @@ export class PlaybackService { if (emptyBefore) { this.setActiveTrack(this.playlist.tracks.find((x) => x.id === uuid).id); + this.controlAudioPlayer(); } return this.playlist.tracks.findIndex((x) => x.id === uuid); @@ -82,6 +88,10 @@ export class PlaybackService { this.playlist.tracks = []; } + hasNextTrack() { + return this.getActiveIndex() + 1 < this.getTrackIds().length; + } + hasActiveTrack() { return this.playlist.activeTrack !== null; } @@ -101,4 +111,11 @@ export class PlaybackService { private getActiveIndex() { return this.getTrackIds().indexOf(this.playlist.activeTrack); } + + private controlAudioPlayer() { + const activeTrack = this.getActiveTrack(); + console.log('received track change'); + console.log(activeTrack.track); + this.eventEmitter.emit('playback.newTrack', activeTrack.track); + } } diff --git a/src/types/track.ts b/src/types/track.ts index 2954289..bc60421 100644 --- a/src/types/track.ts +++ b/src/types/track.ts @@ -2,4 +2,5 @@ export interface Track { jellyfinId: string; name: string; durationInMilliseconds: number; + streamUrl: string; } diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts new file mode 100644 index 0000000..9c34684 --- /dev/null +++ b/src/utils/stringUtils.ts @@ -0,0 +1,9 @@ +export const trimStringToFixedLength = (value: string, maxLength: number) => { + if (maxLength < 1) { + throw new Error('max length must be positive'); + } + + return value.length > maxLength + ? value.substring(0, maxLength - 3) + '...' + : value; +};