Add on track change event and automatic track waterfall

This commit is contained in:
Manuel Ruwe 2022-12-17 20:19:25 +01:00
parent feeb09a17d
commit 60df58959a
6 changed files with 71 additions and 19 deletions

View File

@ -1,11 +1,12 @@
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 { PlaybackModule } from '../../playback/playback.module';
import { DiscordConfigService } from './discord.config.service'; import { DiscordConfigService } from './discord.config.service';
import { DiscordMessageService } from './discord.message.service'; import { DiscordMessageService } from './discord.message.service';
import { DiscordVoiceService } from './discord.voice.service'; import { DiscordVoiceService } from './discord.voice.service';
@Module({ @Module({
imports: [], imports: [PlaybackModule],
controllers: [], controllers: [],
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService], providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService], exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],

View File

@ -1,9 +1,9 @@
import { import {
AudioPlayer, AudioPlayer,
AudioPlayerPausedState,
AudioPlayerStatus, AudioPlayerStatus,
AudioResource, AudioResource,
createAudioPlayer, createAudioPlayer,
createAudioResource,
getVoiceConnection, getVoiceConnection,
getVoiceConnections, getVoiceConnections,
joinVoiceChannel, joinVoiceChannel,
@ -11,8 +11,11 @@ import {
} from '@discordjs/voice'; } from '@discordjs/voice';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Logger } from '@nestjs/common/services'; import { Logger } from '@nestjs/common/services';
import { OnEvent } from '@nestjs/event-emitter';
import { GuildMember } from 'discord.js'; import { GuildMember } from 'discord.js';
import { GenericTryHandler } from '../../models/generic-try-handler'; import { GenericTryHandler } from '../../models/generic-try-handler';
import { PlaybackService } from '../../playback/playback.service';
import { Track } from '../../types/track';
import { DiscordMessageService } from './discord.message.service'; import { DiscordMessageService } from './discord.message.service';
@Injectable() @Injectable()
@ -21,7 +24,16 @@ export class DiscordVoiceService {
private audioPlayer: AudioPlayer; private audioPlayer: AudioPlayer;
private voiceConnection: VoiceConnection; 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( tryJoinChannelAndEstablishVoiceConnection(
member: GuildMember, member: GuildMember,
@ -156,6 +168,17 @@ export class DiscordVoiceService {
this.audioPlayer.on('error', (message) => { this.audioPlayer.on('error', (message) => {
this.logger.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); this.voiceConnection.subscribe(this.audioPlayer);
return this.audioPlayer; return this.audioPlayer;
} }

View File

@ -29,6 +29,7 @@ import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service'; import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
import { Constants } from '../utils/constants'; import { Constants } from '../utils/constants';
import { trimStringToFixedLength } from '../utils/stringUtils';
@Command({ @Command({
name: 'play', name: 'play',
@ -69,9 +70,9 @@ export class PlayItemCommand
const lines: string[] = firstItems.map( const lines: string[] = firstItems.map(
(item) => (item) =>
`:white_small_square: ${this.markSearchTermOverlap( `:white_small_square: ${trimStringToFixedLength(
item.Name, this.markSearchTermOverlap(item.Name, dto.search),
dto.search, 30,
)} *(${item.Type})*`, )} *(${item.Type})*`,
); );
@ -104,7 +105,8 @@ export class PlayItemCommand
return { return {
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: '', title: 'a',
description: description,
mixin(embedBuilder) { mixin(embedBuilder) {
return embedBuilder.setAuthor({ return embedBuilder.setAuthor({
name: 'Jellyfin Search Results', name: 'Jellyfin Search Results',
@ -158,12 +160,6 @@ export class PlayItemCommand
const artists = item.Artists.join(', '); 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 guildMember = interaction.member as GuildMember;
const bitrate = guildMember.voice.channel.bitrate; const bitrate = guildMember.voice.channel.bitrate;
@ -171,11 +167,16 @@ export class PlayItemCommand
guildMember, guildMember,
); );
this.jellyfinStreamBuilder const stream = await this.jellyfinStreamBuilder.buildStreamUrl(
.buildStreamUrl(item.Id, bitrate) item.Id,
.then((stream) => { bitrate,
const resource = createAudioResource(stream); );
this.discordVoiceService.playResource(resource);
const addedIndex = this.playbackService.eneuqueTrack({
jellyfinId: item.Id,
name: item.Name,
durationInMilliseconds: milliseconds,
streamUrl: stream,
}); });
await interaction.update({ await interaction.update({

View File

@ -3,6 +3,7 @@ import { Playlist } from '../types/playlist';
import { Track } from '../types/track'; import { Track } from '../types/track';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable() @Injectable()
export class PlaybackService { export class PlaybackService {
@ -11,6 +12,8 @@ export class PlaybackService {
activeTrack: null, activeTrack: null,
}; };
constructor(private readonly eventEmitter: EventEmitter2) {}
getActiveTrack() { getActiveTrack() {
return this.getTrackById(this.playlist.activeTrack); return this.getTrackById(this.playlist.activeTrack);
} }
@ -38,6 +41,7 @@ export class PlaybackService {
const newKey = keys[index + 1]; const newKey = keys[index + 1];
this.setActiveTrack(newKey); this.setActiveTrack(newKey);
this.controlAudioPlayer();
return true; return true;
} }
@ -51,6 +55,7 @@ export class PlaybackService {
const keys = this.getTrackIds(); const keys = this.getTrackIds();
const newKey = keys[index - 1]; const newKey = keys[index - 1];
this.setActiveTrack(newKey); this.setActiveTrack(newKey);
this.controlAudioPlayer();
return true; return true;
} }
@ -66,6 +71,7 @@ export class PlaybackService {
if (emptyBefore) { if (emptyBefore) {
this.setActiveTrack(this.playlist.tracks.find((x) => x.id === uuid).id); this.setActiveTrack(this.playlist.tracks.find((x) => x.id === uuid).id);
this.controlAudioPlayer();
} }
return this.playlist.tracks.findIndex((x) => x.id === uuid); return this.playlist.tracks.findIndex((x) => x.id === uuid);
@ -82,6 +88,10 @@ export class PlaybackService {
this.playlist.tracks = []; this.playlist.tracks = [];
} }
hasNextTrack() {
return this.getActiveIndex() + 1 < this.getTrackIds().length;
}
hasActiveTrack() { hasActiveTrack() {
return this.playlist.activeTrack !== null; return this.playlist.activeTrack !== null;
} }
@ -101,4 +111,11 @@ export class PlaybackService {
private getActiveIndex() { private getActiveIndex() {
return this.getTrackIds().indexOf(this.playlist.activeTrack); 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);
}
} }

View File

@ -2,4 +2,5 @@ export interface Track {
jellyfinId: string; jellyfinId: string;
name: string; name: string;
durationInMilliseconds: number; durationInMilliseconds: number;
streamUrl: string;
} }

9
src/utils/stringUtils.ts Normal file
View File

@ -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;
};