mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-23 18:21:55 +01:00
✨ Add on track change event and automatic track waterfall
This commit is contained in:
parent
feeb09a17d
commit
60df58959a
@ -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],
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
9
src/utils/stringUtils.ts
Normal 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;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user