mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-24 18:41:57 +01:00
♻️ Playlist and playback manager (#89)
This commit is contained in:
parent
6c75861c49
commit
3adec06df7
@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@discord-nestjs/common": "^5.1.5",
|
||||
"@discord-nestjs/core": "^4.3.1",
|
||||
"@discord-nestjs/core": "^5.3.0",
|
||||
"@discordjs/opus": "^0.9.0",
|
||||
"@discordjs/voice": "^0.14.0",
|
||||
"@jellyfin/sdk": "^0.7.0",
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { registerFilterGlobally } from '@discord-nestjs/core';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks';
|
||||
import { CommandExecutionError } from '../../middleware/command-execution-filter';
|
||||
import { PlaybackModule } from '../../playback/playback.module';
|
||||
|
||||
import { JellyfinClientModule } from '../jellyfin/jellyfin.module';
|
||||
import { PlaybackModule } from '../../playback/playback.module';
|
||||
|
||||
import { DiscordConfigService } from './discord.config.service';
|
||||
import { DiscordMessageService } from './discord.message.service';
|
||||
import { DiscordVoiceService } from './discord.voice.service';
|
||||
@ -11,15 +11,7 @@ import { DiscordVoiceService } from './discord.voice.service';
|
||||
@Module({
|
||||
imports: [PlaybackModule, JellyfinClientModule],
|
||||
controllers: [],
|
||||
providers: [
|
||||
DiscordConfigService,
|
||||
DiscordVoiceService,
|
||||
DiscordMessageService,
|
||||
{
|
||||
provide: registerFilterGlobally(),
|
||||
useClass: CommandExecutionError,
|
||||
},
|
||||
],
|
||||
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||
exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||
})
|
||||
export class DiscordClientModule implements OnModuleDestroy {
|
||||
|
@ -9,14 +9,19 @@ import {
|
||||
joinVoiceChannel,
|
||||
VoiceConnection,
|
||||
} from '@discordjs/voice';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services';
|
||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { GuildMember } from 'discord.js';
|
||||
|
||||
import { JellyfinStreamBuilderService } from '../jellyfin/jellyfin.stream.builder.service';
|
||||
import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service';
|
||||
import { GenericTryHandler } from '../../models/generic-try-handler';
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import { Track } from '../../types/track';
|
||||
import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service';
|
||||
import { GenericTrack } from '../../models/shared/GenericTrack';
|
||||
|
||||
import { DiscordMessageService } from './discord.message.service';
|
||||
|
||||
@Injectable()
|
||||
@ -29,12 +34,15 @@ export class DiscordVoiceService {
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly playbackService: PlaybackService,
|
||||
private readonly jellyfinWebSocketService: JellyfinWebSocketService,
|
||||
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
) {}
|
||||
|
||||
@OnEvent('playback.newTrack')
|
||||
handleOnNewTrack(newTrack: Track) {
|
||||
const resource = createAudioResource(newTrack.streamUrl);
|
||||
@OnEvent('internal.audio.announce')
|
||||
handleOnNewTrack(track: GenericTrack) {
|
||||
const resource = createAudioResource(
|
||||
track.getStreamUrl(this.jellyfinStreamBuilder),
|
||||
);
|
||||
this.playResource(resource);
|
||||
}
|
||||
|
||||
@ -96,7 +104,6 @@ export class DiscordVoiceService {
|
||||
/**
|
||||
* Pauses the current audio player
|
||||
*/
|
||||
@OnEvent('playback.control.pause')
|
||||
pause() {
|
||||
this.createAndReturnOrGetAudioPlayer().pause();
|
||||
this.eventEmitter.emit('playback.state.pause', true);
|
||||
@ -105,7 +112,6 @@ export class DiscordVoiceService {
|
||||
/**
|
||||
* Stops the audio player
|
||||
*/
|
||||
@OnEvent('playback.control.stop')
|
||||
stop(force: boolean): boolean {
|
||||
const stopped = this.createAndReturnOrGetAudioPlayer().stop(force);
|
||||
this.eventEmitter.emit('playback.state.stop');
|
||||
@ -143,7 +149,6 @@ export class DiscordVoiceService {
|
||||
* Checks if the current state is paused or not and toggles the states to the opposite.
|
||||
* @returns The new paused state - true: paused, false: unpaused
|
||||
*/
|
||||
@OnEvent('playback.control.togglePause')
|
||||
togglePaused(): boolean {
|
||||
if (this.isPaused()) {
|
||||
this.unpause();
|
||||
@ -193,7 +198,7 @@ export class DiscordVoiceService {
|
||||
private createAndReturnOrGetAudioPlayer() {
|
||||
if (this.audioPlayer === undefined) {
|
||||
this.logger.debug(
|
||||
`Initialized new instance of Audio Player because it has not been defined yet`,
|
||||
`Initialized new instance of AudioPlayer because it has not been defined yet`,
|
||||
);
|
||||
this.audioPlayer = createAudioPlayer();
|
||||
this.attachEventListenersToAudioPlayer();
|
||||
@ -220,7 +225,9 @@ export class DiscordVoiceService {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNextTrack = this.playbackService.hasNextTrack();
|
||||
const hasNextTrack = this.playbackService
|
||||
.getPlaylistOrDefault()
|
||||
.hasNextTrackInPlaylist();
|
||||
|
||||
this.logger.debug(
|
||||
`Deteced audio player status change from ${previousState.status} to ${
|
||||
@ -229,11 +236,11 @@ export class DiscordVoiceService {
|
||||
);
|
||||
|
||||
if (!hasNextTrack) {
|
||||
this.logger.debug(`Audio Player has reached the end of the playlist`);
|
||||
this.logger.debug(`Reached the end of the playlist`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.playbackService.nextTrack();
|
||||
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { Api } from '@jellyfin/sdk';
|
||||
import { PlaystateApi } from '@jellyfin/sdk/lib/generated-client/api/playstate-api';
|
||||
import { SessionApi } from '@jellyfin/sdk/lib/generated-client/api/session-api';
|
||||
@ -9,9 +7,12 @@ import {
|
||||
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
|
||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Track } from '../../types/track';
|
||||
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import { Track } from '../../types/track';
|
||||
|
||||
@Injectable()
|
||||
export class JellyinPlaystateService {
|
||||
@ -53,35 +54,4 @@ export class JellyinPlaystateService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent('playback.state.pause')
|
||||
private async onPlaybackPaused(isPaused: boolean) {
|
||||
const activeTrack = this.playbackService.getActiveTrack();
|
||||
|
||||
if (!activeTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.playstateApi.reportPlaybackProgress({
|
||||
playbackProgressInfo: {
|
||||
ItemId: activeTrack.track.jellyfinId,
|
||||
IsPaused: isPaused,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent('playback.state.stop')
|
||||
private async onPlaybackStopped() {
|
||||
const activeTrack = this.playbackService.getActiveTrack();
|
||||
|
||||
if (!activeTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.playstateApi.reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
ItemId: activeTrack.track.jellyfinId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { JellyfinService } from './jellyfin.service';
|
||||
|
||||
@Injectable()
|
||||
@ -11,7 +12,7 @@ export class JellyfinStreamBuilderService {
|
||||
const api = this.jellyfinService.getApi();
|
||||
|
||||
this.logger.debug(
|
||||
`Attempting to build stream resource for item ${jellyfinItemId} with bitrate ${bitrate}`,
|
||||
`Building stream for '${jellyfinItemId}' with bitrate ${bitrate}`,
|
||||
);
|
||||
|
||||
const accessToken = this.jellyfinService.getApi().accessToken;
|
||||
|
@ -1,19 +1,25 @@
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { JellyfinService } from './jellyfin.service';
|
||||
|
||||
import {
|
||||
PlaystateCommand,
|
||||
SessionMessageType,
|
||||
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||
import { WebSocket } from 'ws';
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import { JellyfinSearchService } from './jellyfin.search.service';
|
||||
import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service';
|
||||
import { Track } from '../../types/track';
|
||||
import { PlayNowCommand, SessionApiSendPlaystateCommandRequest } from '../../types/websocket';
|
||||
|
||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import {
|
||||
PlayNowCommand,
|
||||
SessionApiSendPlaystateCommandRequest,
|
||||
} from '../../types/websocket';
|
||||
import { GenericTrack } from '../../models/shared/GenericTrack';
|
||||
|
||||
import { JellyfinSearchService } from './jellyfin.search.service';
|
||||
import { JellyfinService } from './jellyfin.service';
|
||||
import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service';
|
||||
|
||||
@Injectable()
|
||||
export class JellyfinWebSocketService implements OnModuleDestroy {
|
||||
private webSocket: WebSocket;
|
||||
@ -82,7 +88,7 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
|
||||
return this.webSocket.readyState;
|
||||
}
|
||||
|
||||
protected messageHandler(data: any) {
|
||||
protected async messageHandler(data: any) {
|
||||
const msg: JellyMessage<unknown> = JSON.parse(data);
|
||||
|
||||
switch (msg.MessageType) {
|
||||
@ -102,38 +108,22 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
|
||||
`Adding ${ids.length} ids to the queue using controls from the websocket`,
|
||||
);
|
||||
|
||||
ids.forEach((id, index) => {
|
||||
this.jellyfinSearchService
|
||||
.getById(id)
|
||||
.then((response) => {
|
||||
const track: Track = {
|
||||
name: response.Name,
|
||||
durationInMilliseconds: response.RunTimeTicks / 10000,
|
||||
jellyfinId: response.Id,
|
||||
streamUrl: this.jellyfinStreamBuilderService.buildStreamUrl(
|
||||
response.Id,
|
||||
96000,
|
||||
),
|
||||
remoteImages: {
|
||||
Images: [],
|
||||
Providers: [],
|
||||
TotalRecordCount: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const trackId = this.playbackService.enqueueTrack(track);
|
||||
|
||||
if (index !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.playbackService.setActiveTrack(trackId);
|
||||
this.playbackService.getActiveTrackAndEmitEvent();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error(err);
|
||||
});
|
||||
const tracks = ids.map(async (id) => {
|
||||
try {
|
||||
const hint = await this.jellyfinSearchService.getById(id);
|
||||
return {
|
||||
id: id,
|
||||
name: hint.Name,
|
||||
duration: hint.RunTimeTicks / 10000,
|
||||
remoteImages: {},
|
||||
} as GenericTrack;
|
||||
} catch (err) {
|
||||
this.logger.error('TODO');
|
||||
}
|
||||
});
|
||||
const resolvedTracks = await Promise.all(tracks);
|
||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||
playlist.enqueueTracks(resolvedTracks);
|
||||
break;
|
||||
case SessionMessageType[SessionMessageType.Playstate]:
|
||||
const sendPlaystateCommandRequest =
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common/decorators';
|
||||
|
||||
@ -7,18 +7,19 @@ import { CommandInteraction } from 'discord.js';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'disconnect',
|
||||
description: 'Join your current voice channel',
|
||||
})
|
||||
@Injectable()
|
||||
export class DisconnectCommand implements DiscordCommand {
|
||||
export class DisconnectCommand {
|
||||
constructor(
|
||||
private readonly discordVoiceService: DiscordVoiceService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@ -6,15 +6,16 @@ import { CommandInteraction } from 'discord.js';
|
||||
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'help',
|
||||
description: 'Get help if you're having problems with this bot',
|
||||
})
|
||||
@Injectable()
|
||||
export class HelpCommand implements DiscordCommand {
|
||||
export class HelpCommand {
|
||||
constructor(private readonly discordMessageService: DiscordMessageService) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@ -12,14 +12,15 @@ import { DiscordMessageService } from '../clients/discord/discord.message.servic
|
||||
description: 'Go to the next track in the playlist',
|
||||
})
|
||||
@Injectable()
|
||||
export class SkipTrackCommand implements DiscordCommand {
|
||||
export class SkipTrackCommand {
|
||||
constructor(
|
||||
private readonly playbackService: PlaybackService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
if (!this.playbackService.nextTrack()) {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
@ -27,8 +28,10 @@ export class SkipTrackCommand implements DiscordCommand {
|
||||
}),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@ -7,18 +7,19 @@ import { CommandInteraction } from 'discord.js';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'pause',
|
||||
description: 'Pause or resume the playback of the current track',
|
||||
})
|
||||
@Injectable()
|
||||
export class PausePlaybackCommand implements DiscordCommand {
|
||||
export class PausePlaybackCommand {
|
||||
constructor(
|
||||
private readonly discordVoiceService: DiscordVoiceService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
const shouldBePaused = this.discordVoiceService.togglePaused();
|
||||
|
||||
await interaction.reply({
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { SlashCommandPipe } from '@discord-nestjs/common';
|
||||
import {
|
||||
Command,
|
||||
DiscordTransformedCommand,
|
||||
Handler,
|
||||
IA,
|
||||
InteractionEvent,
|
||||
On,
|
||||
Payload,
|
||||
TransformedCommandExecutionContext,
|
||||
} from '@discord-nestjs/core';
|
||||
|
||||
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { Logger } from '@nestjs/common/services';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services';
|
||||
|
||||
import {
|
||||
CommandInteraction,
|
||||
ComponentType,
|
||||
Events,
|
||||
GuildMember,
|
||||
@ -29,17 +31,16 @@ import { DiscordMessageService } from '../clients/discord/discord.message.servic
|
||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service';
|
||||
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
|
||||
import { GenericTrack } from '../models/shared/GenericTrack';
|
||||
import { chooseSuitableRemoteImage } from '../utils/remoteImages/remoteImages';
|
||||
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'play',
|
||||
description: 'Search for an item on your Jellyfin instance',
|
||||
})
|
||||
@Injectable()
|
||||
export class PlayItemCommand
|
||||
implements DiscordTransformedCommand<TrackRequestDto>
|
||||
{
|
||||
export class PlayItemCommand {
|
||||
private readonly logger: Logger = new Logger(PlayItemCommand.name);
|
||||
|
||||
constructor(
|
||||
@ -50,11 +51,12 @@ export class PlayItemCommand
|
||||
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
|
||||
) {}
|
||||
|
||||
@Handler()
|
||||
async handler(
|
||||
@Payload() dto: TrackRequestDto,
|
||||
executionContext: TransformedCommandExecutionContext<any>,
|
||||
@InteractionEvent(SlashCommandPipe) dto: TrackRequestDto,
|
||||
@IA() interaction: CommandInteraction,
|
||||
): Promise<InteractionReplyOptions | string> {
|
||||
await executionContext.interaction.deferReply();
|
||||
await interaction.deferReply();
|
||||
|
||||
const items = await this.jellyfinSearchService.search(dto.search);
|
||||
const parsedItems = await Promise.all(
|
||||
@ -69,7 +71,7 @@ export class PlayItemCommand
|
||||
);
|
||||
|
||||
if (parsedItems.length === 0) {
|
||||
await executionContext.interaction.followUp({
|
||||
await interaction.followUp({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
title: 'No results for your search query found',
|
||||
@ -109,7 +111,7 @@ export class PlayItemCommand
|
||||
emoji: item.getEmoji(),
|
||||
}));
|
||||
|
||||
await executionContext.interaction.followUp({
|
||||
await interaction.followUp({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Jellyfin Search Results',
|
||||
@ -184,8 +186,6 @@ export class PlayItemCommand
|
||||
|
||||
this.logger.debug('Successfully joined the voice channel');
|
||||
|
||||
const bitrate = guildMember.voice.channel.bitrate;
|
||||
|
||||
const valueParts = interaction.values[0].split('_');
|
||||
const type = valueParts[0];
|
||||
const id = valueParts[1];
|
||||
@ -206,7 +206,6 @@ export class PlayItemCommand
|
||||
);
|
||||
const addedIndex = this.enqueueSingleTrack(
|
||||
item as BaseJellyfinAudioPlayable,
|
||||
bitrate,
|
||||
remoteImagesOfCurrentAlbum,
|
||||
);
|
||||
await interaction.editReply({
|
||||
@ -234,7 +233,6 @@ export class PlayItemCommand
|
||||
album.SearchHints.forEach((item) => {
|
||||
this.enqueueSingleTrack(
|
||||
item as BaseJellyfinAudioPlayable,
|
||||
bitrate,
|
||||
remoteImages,
|
||||
);
|
||||
});
|
||||
@ -267,7 +265,6 @@ export class PlayItemCommand
|
||||
addedRemoteImages.Images.concat(remoteImages.Images);
|
||||
this.enqueueSingleTrack(
|
||||
item as BaseJellyfinAudioPlayable,
|
||||
bitrate,
|
||||
remoteImages,
|
||||
);
|
||||
}
|
||||
@ -310,22 +307,15 @@ export class PlayItemCommand
|
||||
|
||||
private enqueueSingleTrack(
|
||||
jellyfinPlayable: BaseJellyfinAudioPlayable,
|
||||
bitrate: number,
|
||||
remoteImageResult: RemoteImageResult,
|
||||
) {
|
||||
const stream = this.jellyfinStreamBuilder.buildStreamUrl(
|
||||
jellyfinPlayable.Id,
|
||||
bitrate,
|
||||
);
|
||||
|
||||
const milliseconds = jellyfinPlayable.RunTimeTicks / 10000;
|
||||
|
||||
return this.playbackService.enqueueTrack({
|
||||
jellyfinId: jellyfinPlayable.Id,
|
||||
name: jellyfinPlayable.Name,
|
||||
durationInMilliseconds: milliseconds,
|
||||
streamUrl: stream,
|
||||
remoteImages: remoteImageResult,
|
||||
});
|
||||
return this.playbackService
|
||||
.getPlaylistOrDefault()
|
||||
.enqueueTracks([
|
||||
GenericTrack.constructFromJellyfinPlayable(
|
||||
jellyfinPlayable,
|
||||
remoteImageResult,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@ -8,24 +8,24 @@ import { PlaybackService } from '../playback/playback.service';
|
||||
import { Constants } from '../utils/constants';
|
||||
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages/remoteImages';
|
||||
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'playlist',
|
||||
description: 'Print the current track information',
|
||||
})
|
||||
@Injectable()
|
||||
export class PlaylistCommand implements DiscordCommand {
|
||||
export class PlaylistCommand {
|
||||
constructor(
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly playbackService: PlaybackService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
const playList = this.playbackService.getPlaylist();
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||
|
||||
if (playList.tracks.length === 0) {
|
||||
if (!playlist || playlist.tracks.length === 0) {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
@ -35,15 +35,16 @@ export class PlaylistCommand implements DiscordCommand {
|
||||
}),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tracklist = playList.tracks
|
||||
const tracklist = playlist.tracks
|
||||
.slice(0, 10)
|
||||
.map((track, index) => {
|
||||
const isCurrent = track.id === playList.activeTrack;
|
||||
const isCurrent = track === playlist.getActiveTrack();
|
||||
|
||||
let point = this.getListPoint(isCurrent, index);
|
||||
point += `**${trimStringToFixedLength(track.track.name, 30)}**`;
|
||||
point += `**${trimStringToFixedLength(track.name, 30)}**`;
|
||||
|
||||
if (isCurrent) {
|
||||
point += ' :loud_sound:';
|
||||
@ -52,16 +53,13 @@ export class PlaylistCommand implements DiscordCommand {
|
||||
point += '\n';
|
||||
point += Constants.Design.InvisibleSpace.repeat(2);
|
||||
point += 'Duration: ';
|
||||
point += formatMillisecondsAsHumanReadable(
|
||||
track.track.durationInMilliseconds,
|
||||
);
|
||||
point += formatMillisecondsAsHumanReadable(track.getDuration());
|
||||
|
||||
return point;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const activeTrack = this.playbackService.getActiveTrack();
|
||||
const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track);
|
||||
// const remoteImage = chooseSuitableRemoteImageFromTrack(playlist.getActiveTrack());
|
||||
const remoteImage = undefined;
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common/decorators';
|
||||
|
||||
@ -7,19 +7,20 @@ import { CommandInteraction } from 'discord.js';
|
||||
import { PlaybackService } from '../playback/playback.service';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'previous',
|
||||
description: 'Go to the previous track',
|
||||
})
|
||||
@Injectable()
|
||||
export class PreviousTrackCommand implements DiscordCommand {
|
||||
export class PreviousTrackCommand {
|
||||
constructor(
|
||||
private readonly playbackService: PlaybackService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
if (!this.playbackService.previousTrack()) {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
@ -27,8 +28,10 @@ export class PreviousTrackCommand implements DiscordCommand {
|
||||
}),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.playbackService.getPlaylistOrDefault().setPreviousTrackAsActiveTrack();
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
Command,
|
||||
DiscordCommand,
|
||||
InjectDiscordClient,
|
||||
} from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA, InjectDiscordClient } from '@discord-nestjs/core';
|
||||
|
||||
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
|
||||
|
||||
@ -21,7 +17,7 @@ import { JellyfinService } from '../clients/jellyfin/jellyfin.service';
|
||||
description: 'Display the current status for troubleshooting',
|
||||
})
|
||||
@Injectable()
|
||||
export class StatusCommand implements DiscordCommand {
|
||||
export class StatusCommand {
|
||||
constructor(
|
||||
@InjectDiscordClient()
|
||||
private readonly client: Client,
|
||||
@ -29,7 +25,8 @@ export class StatusCommand implements DiscordCommand {
|
||||
private readonly jellyfinService: JellyfinService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@ -13,15 +13,16 @@ import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
description: 'Stop playback entirely and clear the current playlist',
|
||||
})
|
||||
@Injectable()
|
||||
export class StopPlaybackCommand implements DiscordCommand {
|
||||
export class StopPlaybackCommand {
|
||||
constructor(
|
||||
private readonly playbackService: PlaybackService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly discordVoiceService: DiscordVoiceService,
|
||||
) {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
const hasActiveTrack = this.playbackService.hasActiveTrack();
|
||||
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
const hasActiveTrack = this.playbackService.getPlaylistOrDefault();
|
||||
const title = hasActiveTrack
|
||||
? 'Playback stopped successfully'
|
||||
: 'Playback failed to stop';
|
||||
@ -29,7 +30,7 @@ export class StopPlaybackCommand implements DiscordCommand {
|
||||
? 'In addition, your playlist has been cleared'
|
||||
: 'There is no active track in the queue';
|
||||
if (hasActiveTrack) {
|
||||
this.playbackService.clear();
|
||||
this.playbackService.getPlaylistOrDefault().clear();
|
||||
this.discordVoiceService.stop(false);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Command, DiscordCommand } from '@discord-nestjs/core';
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
@ -7,12 +7,12 @@ import { CommandInteraction, GuildMember } from 'discord.js';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'summon',
|
||||
description: 'Join your current voice channel',
|
||||
})
|
||||
@Injectable()
|
||||
export class SummonCommand implements DiscordCommand {
|
||||
export class SummonCommand {
|
||||
private readonly logger = new Logger(SummonCommand.name);
|
||||
|
||||
constructor(
|
||||
@ -20,7 +20,8 @@ export class SummonCommand implements DiscordCommand {
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
async handler(interaction: CommandInteraction): Promise<void> {
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.deferReply();
|
||||
|
||||
const guildMember = interaction.member as GuildMember;
|
||||
|
@ -1,29 +1,18 @@
|
||||
import {
|
||||
Catch,
|
||||
DiscordArgumentMetadata,
|
||||
DiscordExceptionFilter,
|
||||
} from '@discord-nestjs/core';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
CommandInteraction,
|
||||
} from 'discord.js';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common';
|
||||
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction } from 'discord.js';
|
||||
|
||||
import { Constants } from '../utils/constants';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
|
||||
@Catch(Error)
|
||||
export class CommandExecutionError implements DiscordExceptionFilter {
|
||||
export class CommandExecutionError implements ExceptionFilter {
|
||||
private readonly logger = new Logger(CommandExecutionError.name);
|
||||
|
||||
constructor(private readonly discordMessageService: DiscordMessageService) {}
|
||||
|
||||
async catch(
|
||||
exception: Error,
|
||||
metadata: DiscordArgumentMetadata<string, any>,
|
||||
): Promise<void> {
|
||||
const interaction: CommandInteraction = metadata.eventArgs[0];
|
||||
async catch(exception: Error, host: ArgumentsHost): Promise<void> {
|
||||
const interaction = host.getArgByIndex(0) as CommandInteraction;
|
||||
|
||||
if (!interaction.isCommand()) {
|
||||
return;
|
||||
@ -34,6 +23,10 @@ export class CommandExecutionError implements DiscordExceptionFilter {
|
||||
exception.stack,
|
||||
);
|
||||
|
||||
if (!interaction.isRepliable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setLabel('Report this issue')
|
||||
|
129
src/models/shared/GenericPlaylist.ts
Normal file
129
src/models/shared/GenericPlaylist.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
|
||||
import { GenericTrack } from './GenericTrack';
|
||||
|
||||
export class GenericPlaylist {
|
||||
tracks: GenericTrack[];
|
||||
activeTrackIndex?: number;
|
||||
|
||||
constructor(private readonly eventEmitter: EventEmitter2) {
|
||||
this.tracks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the playlist has been started.
|
||||
* Does not indicate if it's paused.
|
||||
* @returns if the playlist has been started and has an active track
|
||||
*/
|
||||
hasStarted() {
|
||||
return this.activeTrackIndex !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the active track is out of bounds
|
||||
* @returns active track or undefined if there's none
|
||||
*/
|
||||
getActiveTrack(): GenericTrack | undefined {
|
||||
if (this.isActiveTrackOutOfSync()) {
|
||||
return undefined;
|
||||
}
|
||||
return this.tracks[this.activeTrackIndex];
|
||||
}
|
||||
|
||||
hasActiveTrack(): boolean {
|
||||
return (
|
||||
this.activeTrackIndex !== undefined && !this.isActiveTrackOutOfSync()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the next track in the playlist
|
||||
* @returns if the track has been changed successfully
|
||||
*/
|
||||
setNextTrackAsActiveTrack(): boolean {
|
||||
if (this.activeTrackIndex >= this.tracks.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.activeTrackIndex++;
|
||||
this.eventEmitter.emit('controls.playlist.tracks.next', {
|
||||
newActive: this.activeTrackIndex,
|
||||
});
|
||||
this.announceTrackChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to the previous track in the playlist
|
||||
* @returns if the track has been changed successfully
|
||||
*/
|
||||
setPreviousTrackAsActiveTrack(): boolean {
|
||||
if (this.activeTrackIndex <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.activeTrackIndex--;
|
||||
this.eventEmitter.emit('controls.playlist.tracks.previous', {
|
||||
newActive: this.activeTrackIndex,
|
||||
});
|
||||
this.announceTrackChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new track(-s) to the playlist
|
||||
* @param tracks the tracks that should be added
|
||||
* @returns the new lendth of the tracks in the playlist
|
||||
*/
|
||||
enqueueTracks(tracks: GenericTrack[]) {
|
||||
this.eventEmitter.emit('controls.playlist.tracks.enqueued', {
|
||||
count: tracks.length,
|
||||
activeTrack: this.activeTrackIndex,
|
||||
});
|
||||
const length = this.tracks.push(...tracks);
|
||||
this.announceTrackChange();
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a next track
|
||||
* @returns if there is a track next in the playlist
|
||||
*/
|
||||
hasNextTrackInPlaylist() {
|
||||
return this.activeTrackIndex < this.tracks.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a previous track
|
||||
* @returns if there is a previous track in the playlist
|
||||
*/
|
||||
hasPreviousTrackInPlaylist() {
|
||||
return this.activeTrackIndex > 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.eventEmitter.emit('controls.playlist.tracks.clear');
|
||||
this.tracks = [];
|
||||
this.activeTrackIndex = undefined;
|
||||
}
|
||||
|
||||
private announceTrackChange() {
|
||||
if (!this.activeTrackIndex) {
|
||||
this.activeTrackIndex = 0;
|
||||
}
|
||||
|
||||
this.eventEmitter.emit('internal.audio.announce', this.getActiveTrack());
|
||||
}
|
||||
|
||||
private isActiveTrackOutOfSync(): boolean {
|
||||
return (
|
||||
this.activeTrackIndex < 0 || this.activeTrackIndex >= this.tracks.length
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type PlaylistPlaybackType =
|
||||
| 'once'
|
||||
| 'repeat-once'
|
||||
| 'repeat-indefinetly'
|
||||
| 'shuffle';
|
59
src/models/shared/GenericTrack.ts
Normal file
59
src/models/shared/GenericTrack.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { BaseJellyfinAudioPlayable } from '../jellyfinAudioItems';
|
||||
import { JellyfinStreamBuilderService } from '../../clients/jellyfin/jellyfin.stream.builder.service';
|
||||
|
||||
export class GenericTrack {
|
||||
/**
|
||||
* The identifier of this track, structured as a UID.
|
||||
* This id can be used to build a stream url and send more API requests to Jellyfin
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The name of the track
|
||||
*/
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* The duration of the track
|
||||
*/
|
||||
readonly duration: number;
|
||||
|
||||
/**
|
||||
* A result object that contains a collection of images that are available outside the current network.
|
||||
*/
|
||||
readonly remoteImages?: RemoteImageResult;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
name: string,
|
||||
duration: number,
|
||||
remoteImages?: RemoteImageResult,
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.duration = duration;
|
||||
this.remoteImages = remoteImages;
|
||||
}
|
||||
|
||||
getDuration() {
|
||||
return this.duration;
|
||||
}
|
||||
|
||||
getStreamUrl(streamBuilder: JellyfinStreamBuilderService) {
|
||||
return streamBuilder.buildStreamUrl(this.id, 96000);
|
||||
}
|
||||
|
||||
static constructFromJellyfinPlayable(
|
||||
playable: BaseJellyfinAudioPlayable,
|
||||
remoteImages: RemoteImageResult | undefined,
|
||||
): GenericTrack {
|
||||
return new GenericTrack(
|
||||
playable.Id,
|
||||
playable.Name,
|
||||
playable.RunTimeTicks / 1000,
|
||||
remoteImages,
|
||||
);
|
||||
}
|
||||
}
|
@ -1,143 +1,21 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Playlist } from '../types/playlist';
|
||||
import { Track } from '../types/track';
|
||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { GenericPlaylist } from '../models/shared/GenericPlaylist';
|
||||
|
||||
@Injectable()
|
||||
export class PlaybackService {
|
||||
private readonly logger = new Logger(PlaybackService.name);
|
||||
|
||||
private readonly playlist: Playlist = {
|
||||
tracks: [],
|
||||
activeTrack: null,
|
||||
};
|
||||
private playlist: GenericPlaylist | undefined = undefined;
|
||||
|
||||
constructor(private readonly eventEmitter: EventEmitter2) {}
|
||||
|
||||
getActiveTrack() {
|
||||
return this.getTrackById(this.playlist.activeTrack);
|
||||
}
|
||||
|
||||
setActiveTrack(trackId: string) {
|
||||
const track = this.getTrackById(trackId);
|
||||
|
||||
if (!track) {
|
||||
throw Error('track is not in playlist');
|
||||
getPlaylistOrDefault(): GenericPlaylist {
|
||||
if (this.playlist) {
|
||||
return this.playlist;
|
||||
}
|
||||
|
||||
this.playlist.activeTrack = track.id;
|
||||
}
|
||||
|
||||
nextTrack() {
|
||||
const keys = this.getTrackIds();
|
||||
const index = this.getActiveIndex();
|
||||
|
||||
if (!this.hasActiveTrack() || index + 1 >= keys.length) {
|
||||
this.logger.debug(
|
||||
`Unable to go to next track, because playback has reached end of the playlist`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const newKey = keys[index + 1];
|
||||
this.setActiveTrack(newKey);
|
||||
this.getActiveTrackAndEmitEvent();
|
||||
return true;
|
||||
}
|
||||
|
||||
previousTrack() {
|
||||
const index = this.getActiveIndex();
|
||||
|
||||
if (!this.hasActiveTrack() || index < 1) {
|
||||
this.logger.debug(
|
||||
`Unable to go to previous track, because there is no previous track in the playlist`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = this.getTrackIds();
|
||||
const newKey = keys[index - 1];
|
||||
this.setActiveTrack(newKey);
|
||||
this.getActiveTrackAndEmitEvent();
|
||||
return true;
|
||||
}
|
||||
|
||||
enqueueTrack(track: Track) {
|
||||
const uuid = uuidv4();
|
||||
|
||||
const emptyBefore = this.playlist.tracks.length === 0;
|
||||
|
||||
this.playlist.tracks.push({
|
||||
id: uuid,
|
||||
track: track,
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
`Added the track '${track.jellyfinId}' to the current playlist`,
|
||||
);
|
||||
|
||||
if (emptyBefore) {
|
||||
this.setActiveTrack(this.playlist.tracks.find((x) => x.id === uuid).id);
|
||||
this.getActiveTrackAndEmitEvent();
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
enqueTrackAndInstantyPlay(track: Track) {
|
||||
const uuid = uuidv4();
|
||||
|
||||
this.playlist.tracks.push({
|
||||
id: uuid,
|
||||
track: track,
|
||||
});
|
||||
|
||||
this.setActiveTrack(uuid);
|
||||
this.getActiveTrackAndEmitEvent();
|
||||
}
|
||||
|
||||
set(tracks: Track[]) {
|
||||
this.playlist.tracks = tracks.map((t) => ({
|
||||
id: uuidv4(),
|
||||
track: t,
|
||||
}));
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.playlist.tracks = [];
|
||||
}
|
||||
|
||||
hasNextTrack() {
|
||||
return this.getActiveIndex() + 1 < this.getTrackIds().length;
|
||||
}
|
||||
|
||||
hasActiveTrack() {
|
||||
return this.playlist.activeTrack !== null;
|
||||
}
|
||||
|
||||
getPlaylist(): Playlist {
|
||||
this.playlist = new GenericPlaylist(this.eventEmitter);
|
||||
return this.playlist;
|
||||
}
|
||||
|
||||
private getTrackById(id: string) {
|
||||
return this.playlist.tracks.find((x) => x.id === id);
|
||||
}
|
||||
|
||||
private getTrackIds() {
|
||||
return this.playlist.tracks.map((item) => item.id);
|
||||
}
|
||||
|
||||
private getActiveIndex() {
|
||||
return this.getTrackIds().indexOf(this.playlist.activeTrack);
|
||||
}
|
||||
|
||||
getActiveTrackAndEmitEvent() {
|
||||
const activeTrack = this.getActiveTrack();
|
||||
this.logger.debug(
|
||||
`A new track (${activeTrack.id}) was requested and will be emmitted as an event`,
|
||||
);
|
||||
this.eventEmitter.emit('playback.newTrack', activeTrack.track);
|
||||
}
|
||||
}
|
||||
|
@ -368,10 +368,10 @@
|
||||
class-transformer "0.5.1"
|
||||
class-validator "0.14.0"
|
||||
|
||||
"@discord-nestjs/core@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-4.3.1.tgz#b0a71834147d2bfac8efe37b7091667ce7f146d6"
|
||||
integrity sha512-38Bk7V0W+LF2qUbE6K+CfqRR309jU/tbj6ZZLN97nFHcYCcsteKQ7HxJeM15pw09Vvb7xbB+3DIls/YHpq3lRA==
|
||||
"@discord-nestjs/core@^5.3.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.0.tgz#8e93b0310e8cc2c0cde74c6317d949d6e9d28d2d"
|
||||
integrity sha512-eHVzuPCu3EbQyTln+ZEH0/Jwe0xPG7Z1eZV655jZoCSpq7RmvxWcTG/REf3XMSgtYFN27HaXa9vPCsgUOnp9xQ==
|
||||
dependencies:
|
||||
class-transformer "0.5.1"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user