Add embed to display playlist

This commit is contained in:
Manuel Ruwe 2022-12-17 19:52:32 +01:00
parent 9c23ef293f
commit feeb09a17d
11 changed files with 208 additions and 94 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@ -22,8 +22,7 @@ export class DiscordMessageService {
return embedBuilder return embedBuilder
.setAuthor({ .setAuthor({
name: title, name: title,
iconURL: iconURL: Constants.Design.Icons.ErrorIcon,
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/alert-circle.png?raw=true',
}) })
.setFooter({ .setFooter({
text: `${date} - Report an issue: ${Constants.Links.ReportIssue}`, text: `${date} - Report an issue: ${Constants.Links.ReportIssue}`,
@ -48,8 +47,7 @@ export class DiscordMessageService {
.setColor(DefaultJellyfinColor) .setColor(DefaultJellyfinColor)
.setAuthor({ .setAuthor({
name: title, name: title,
iconURL: iconURL: Constants.Design.Icons.JellyfinLogo,
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/circle-check.png?raw=true',
}) })
.setFooter({ .setFooter({
text: `${date}`, text: `${date}`,

View File

@ -10,8 +10,8 @@ import { DisconnectCommand } from './disconnect.command';
import { EnqueueCommand } from './enqueue.command'; import { EnqueueCommand } from './enqueue.command';
import { HelpCommand } from './help.command'; import { HelpCommand } from './help.command';
import { PausePlaybackCommand } from './pause.command'; import { PausePlaybackCommand } from './pause.command';
import { PlayCommand } from './play.command'; import { PreviousTrackCommand } from './previous.command';
import { SearchItemCommand } from './search.comands'; import { PlayItemCommand } from './play.comands';
import { SkipTrackCommand } from './skip.command'; import { SkipTrackCommand } from './skip.command';
import { StatusCommand } from './status.command'; import { StatusCommand } from './status.command';
import { StopPlaybackCommand } from './stop.command'; import { StopPlaybackCommand } from './stop.command';
@ -31,11 +31,11 @@ import { SummonCommand } from './summon.command';
DisconnectCommand, DisconnectCommand,
EnqueueCommand, EnqueueCommand,
PausePlaybackCommand, PausePlaybackCommand,
PlayCommand,
SkipTrackCommand, SkipTrackCommand,
StopPlaybackCommand, StopPlaybackCommand,
SummonCommand, SummonCommand,
SearchItemCommand, PlayItemCommand,
PreviousTrackCommand,
DiscordMessageService, DiscordMessageService,
PlaybackService, PlaybackService,
], ],

View File

@ -4,6 +4,9 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler'; import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service';
import { Constants } from '../utils/constants';
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
@Command({ @Command({
name: 'current', name: 'current',
@ -11,15 +14,55 @@ import { GenericCustomReply } from '../models/generic-try-handler';
}) })
@UsePipes(TransformPipe) @UsePipes(TransformPipe)
export class CurrentTrackCommand implements DiscordCommand { export class CurrentTrackCommand implements DiscordCommand {
constructor(private readonly discordMessageService: DiscordMessageService) {} constructor(
private readonly discordMessageService: DiscordMessageService,
private readonly playbackService: PlaybackService,
) {}
handler(interaction: CommandInteraction): GenericCustomReply { handler(interaction: CommandInteraction): GenericCustomReply {
const playList = this.playbackService.getPlaylist();
if (playList.tracks.length === 0) {
return { return {
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildMessage({
title: 'NOT IMPLEMENTED', title: 'Your Playlist',
description:
'You do not have any tracks in your playlist.\nUse the play command to add new tracks to your playlist',
}), }),
], ],
}; };
} }
const tracklist = playList.tracks
.slice(0, 10)
.map((track) => {
const isCurrent = track.id === playList.activeTrack;
return `${this.getListPoint(isCurrent)} ${
track.track.name
}\n${Constants.Design.InvisibleSpace.repeat(
3,
)}${formatMillisecondsAsHumanReadable(
track.track.durationInMilliseconds,
)} ${isCurrent && ' *(active track)*'}`;
})
.join(',\n');
return {
embeds: [
this.discordMessageService.buildMessage({
title: 'Your Playlist',
description: tracklist,
}),
],
};
}
private getListPoint(isCurrent: boolean) {
if (isCurrent) {
return ':black_small_square:';
}
return ':white_small_square:';
}
} }

View File

@ -28,16 +28,17 @@ import { formatDuration, intervalToDuration } from 'date-fns';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; 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';
@Command({ @Command({
name: 'search', name: 'play',
description: 'Search for an item on your Jellyfin instance', description: 'Search for an item on your Jellyfin instance',
}) })
@UsePipes(TransformPipe) @UsePipes(TransformPipe)
export class SearchItemCommand export class PlayItemCommand
implements DiscordTransformedCommand<TrackRequestDto> implements DiscordTransformedCommand<TrackRequestDto>
{ {
private readonly logger: Logger = new Logger(SearchItemCommand.name); private readonly logger: Logger = new Logger(PlayItemCommand.name);
constructor( constructor(
private readonly jellyfinSearchService: JellyfinSearchService, private readonly jellyfinSearchService: JellyfinSearchService,
@ -102,15 +103,15 @@ export class SearchItemCommand
return { return {
embeds: [ embeds: [
new EmbedBuilder() this.discordMessageService.buildMessage({
.setAuthor({ title: '',
mixin(embedBuilder) {
return embedBuilder.setAuthor({
name: 'Jellyfin Search Results', name: 'Jellyfin Search Results',
iconURL: iconURL: Constants.Design.Icons.JellyfinLogo,
'https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true', });
}) },
.setColor(DefaultJellyfinColor) }),
.setDescription(description)
.toJSON(),
], ],
components: [ components: [
{ {

View File

@ -1,60 +0,0 @@
import { TransformPipe } from '@discord-nestjs/common';
import {
Command,
DiscordTransformedCommand,
Payload,
TransformedCommandExecutionContext,
UsePipes,
} from '@discord-nestjs/core';
import { GuildMember, InteractionReplyOptions } from 'discord.js';
import { createAudioResource } from '@discordjs/voice';
import { Injectable } from '@nestjs/common';
import { Logger } from '@nestjs/common/services';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { TrackRequestDto } from '../models/track-request.dto';
@Command({
name: 'play',
description: 'Immediately play a track',
})
@Injectable()
@UsePipes(TransformPipe)
export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
private readonly logger = new Logger(PlayCommand.name);
constructor(
private readonly discordMessageService: DiscordMessageService,
private readonly discordVoiceService: DiscordVoiceService,
) {}
handler(
@Payload() dto: TrackRequestDto,
executionContext: TransformedCommandExecutionContext<any>,
):
| string
| InteractionReplyOptions
| Promise<string | InteractionReplyOptions> {
const guildMember = executionContext.interaction.member as GuildMember;
const joinVoiceChannel =
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
guildMember,
);
if (!joinVoiceChannel.success) {
return joinVoiceChannel.reply;
}
this.discordVoiceService.playResource(createAudioResource(dto.search));
return {
embeds: [
this.discordMessageService.buildMessage({
title: `Playing ${dto.search}`,
}),
],
};
}
}

View File

@ -0,0 +1,40 @@
import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction, InteractionReplyOptions } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service';
@Command({
name: 'previous',
description: 'Go to the previous track',
})
@UsePipes(TransformPipe)
export class PreviousTrackCommand implements DiscordCommand {
constructor(
private readonly playbackService: PlaybackService,
private readonly discordMessageService: DiscordMessageService,
) {}
handler(
dcommandInteraction: CommandInteraction,
): InteractionReplyOptions | string {
if (!this.playbackService.previousTrack()) {
return {
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'There is no previous track',
}),
],
};
}
return {
embeds: [
this.discordMessageService.buildMessage({
title: 'Went to previous track',
}),
],
};
}
}

View File

@ -1,23 +1,40 @@
import { TransformPipe } from '@discord-nestjs/common'; import { TransformPipe } from '@discord-nestjs/common';
import { import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
Command, import { CommandInteraction, InteractionReplyOptions } from 'discord.js';
DiscordTransformedCommand, import { DiscordMessageService } from '../clients/discord/discord.message.service';
TransformedCommandExecutionContext, import { PlaybackService } from '../playback/playback.service';
UsePipes,
} from '@discord-nestjs/core';
import { InteractionReplyOptions } from 'discord.js';
@Command({ @Command({
name: 'skip', name: 'skip',
description: 'Skip the current track', description: 'Skip the current track',
}) })
@UsePipes(TransformPipe) @UsePipes(TransformPipe)
export class SkipTrackCommand implements DiscordTransformedCommand<unknown> { export class SkipTrackCommand implements DiscordCommand {
constructor(
private readonly playbackService: PlaybackService,
private readonly discordMessageService: DiscordMessageService,
) {}
handler( handler(
dto: unknown, interactionCommand: CommandInteraction,
executionContext: TransformedCommandExecutionContext<any>,
): InteractionReplyOptions | string { ): InteractionReplyOptions | string {
return 'nice'; if (!this.playbackService.nextTrack()) {
return {
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'There is no next track',
}),
],
};
}
return {
embeds: [
this.discordMessageService.buildMessage({
title: 'Skipped to the next track',
}),
],
};
} }
} }

View File

@ -25,12 +25,49 @@ export class PlaybackService {
this.playlist.activeTrack = track.id; this.playlist.activeTrack = track.id;
} }
nextTrack() {
const keys = this.getTrackIds();
const index = this.getActiveIndex();
console.log(keys);
console.log(index);
if (!this.hasActiveTrack() || index >= keys.length) {
return false;
}
const newKey = keys[index + 1];
this.setActiveTrack(newKey);
return true;
}
previousTrack() {
const index = this.getActiveIndex();
if (!this.hasActiveTrack() || index < 1) {
return false;
}
const keys = this.getTrackIds();
const newKey = keys[index - 1];
this.setActiveTrack(newKey);
return true;
}
eneuqueTrack(track: Track) { eneuqueTrack(track: Track) {
const uuid = uuidv4(); const uuid = uuidv4();
const emptyBefore = this.playlist.tracks.length === 0;
this.playlist.tracks.push({ this.playlist.tracks.push({
id: uuid, id: uuid,
track: track, track: track,
}); });
if (emptyBefore) {
this.setActiveTrack(this.playlist.tracks.find((x) => x.id === uuid).id);
}
return this.playlist.tracks.findIndex((x) => x.id === uuid); return this.playlist.tracks.findIndex((x) => x.id === uuid);
} }
@ -45,7 +82,23 @@ export class PlaybackService {
this.playlist.tracks = []; this.playlist.tracks = [];
} }
hasActiveTrack() {
return this.playlist.activeTrack !== null;
}
getPlaylist(): Playlist {
return this.playlist;
}
private getTrackById(id: string) { private getTrackById(id: string) {
return this.playlist.tracks.find((x) => x.id === id); return this.playlist.tracks.find((x) => x.id === id);
} }
private getTrackIds() {
return Object.keys(this.playlist.tracks);
}
private getActiveIndex() {
return this.getTrackIds().indexOf(this.playlist.activeTrack);
}
} }

View File

@ -8,4 +8,15 @@ export const Constants = {
ReportIssue: ReportIssue:
'https://github.com/manuel-rw/jellyfin-discord-music-bot/issues/new/choose', 'https://github.com/manuel-rw/jellyfin-discord-music-bot/issues/new/choose',
}, },
Design: {
InvisibleSpace: '\u1CBC',
Icons: {
JellyfinLogo:
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/jellyfin-icon-squared.png?raw=true',
SuccessIcon:
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/circle-check.png?raw=true',
ErrorIcon:
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/alert-circle.png?raw=true',
},
},
}; };

11
src/utils/timeUtils.ts Normal file
View File

@ -0,0 +1,11 @@
import { formatDuration, intervalToDuration } from 'date-fns';
export const formatMillisecondsAsHumanReadable = (milliseconds: number) => {
const duration = formatDuration(
intervalToDuration({
start: milliseconds,
end: 0,
}),
);
return duration;
};