mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-23 18:21:55 +01:00
✨ Add embed to display playlist
This commit is contained in:
parent
9c23ef293f
commit
feeb09a17d
BIN
images/icons/jellyfin-icon-squared.png
Normal file
BIN
images/icons/jellyfin-icon-squared.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
@ -22,8 +22,7 @@ export class DiscordMessageService {
|
||||
return embedBuilder
|
||||
.setAuthor({
|
||||
name: title,
|
||||
iconURL:
|
||||
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/alert-circle.png?raw=true',
|
||||
iconURL: Constants.Design.Icons.ErrorIcon,
|
||||
})
|
||||
.setFooter({
|
||||
text: `${date} - Report an issue: ${Constants.Links.ReportIssue}`,
|
||||
@ -48,8 +47,7 @@ export class DiscordMessageService {
|
||||
.setColor(DefaultJellyfinColor)
|
||||
.setAuthor({
|
||||
name: title,
|
||||
iconURL:
|
||||
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/circle-check.png?raw=true',
|
||||
iconURL: Constants.Design.Icons.JellyfinLogo,
|
||||
})
|
||||
.setFooter({
|
||||
text: `${date}`,
|
||||
|
@ -10,8 +10,8 @@ import { DisconnectCommand } from './disconnect.command';
|
||||
import { EnqueueCommand } from './enqueue.command';
|
||||
import { HelpCommand } from './help.command';
|
||||
import { PausePlaybackCommand } from './pause.command';
|
||||
import { PlayCommand } from './play.command';
|
||||
import { SearchItemCommand } from './search.comands';
|
||||
import { PreviousTrackCommand } from './previous.command';
|
||||
import { PlayItemCommand } from './play.comands';
|
||||
import { SkipTrackCommand } from './skip.command';
|
||||
import { StatusCommand } from './status.command';
|
||||
import { StopPlaybackCommand } from './stop.command';
|
||||
@ -31,11 +31,11 @@ import { SummonCommand } from './summon.command';
|
||||
DisconnectCommand,
|
||||
EnqueueCommand,
|
||||
PausePlaybackCommand,
|
||||
PlayCommand,
|
||||
SkipTrackCommand,
|
||||
StopPlaybackCommand,
|
||||
SummonCommand,
|
||||
SearchItemCommand,
|
||||
PlayItemCommand,
|
||||
PreviousTrackCommand,
|
||||
DiscordMessageService,
|
||||
PlaybackService,
|
||||
],
|
||||
|
@ -4,6 +4,9 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
|
||||
import { CommandInteraction } from 'discord.js';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { GenericCustomReply } from '../models/generic-try-handler';
|
||||
import { PlaybackService } from '../playback/playback.service';
|
||||
import { Constants } from '../utils/constants';
|
||||
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
|
||||
|
||||
@Command({
|
||||
name: 'current',
|
||||
@ -11,15 +14,55 @@ import { GenericCustomReply } from '../models/generic-try-handler';
|
||||
})
|
||||
@UsePipes(TransformPipe)
|
||||
export class CurrentTrackCommand implements DiscordCommand {
|
||||
constructor(private readonly discordMessageService: DiscordMessageService) {}
|
||||
constructor(
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly playbackService: PlaybackService,
|
||||
) {}
|
||||
|
||||
handler(interaction: CommandInteraction): GenericCustomReply {
|
||||
const playList = this.playbackService.getPlaylist();
|
||||
|
||||
if (playList.tracks.length === 0) {
|
||||
return {
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
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.buildErrorMessage({
|
||||
title: 'NOT IMPLEMENTED',
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Your Playlist',
|
||||
description: tracklist,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private getListPoint(isCurrent: boolean) {
|
||||
if (isCurrent) {
|
||||
return ':black_small_square:';
|
||||
}
|
||||
|
||||
return ':white_small_square:';
|
||||
}
|
||||
}
|
||||
|
@ -28,16 +28,17 @@ import { formatDuration, intervalToDuration } from 'date-fns';
|
||||
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';
|
||||
|
||||
@Command({
|
||||
name: 'search',
|
||||
name: 'play',
|
||||
description: 'Search for an item on your Jellyfin instance',
|
||||
})
|
||||
@UsePipes(TransformPipe)
|
||||
export class SearchItemCommand
|
||||
export class PlayItemCommand
|
||||
implements DiscordTransformedCommand<TrackRequestDto>
|
||||
{
|
||||
private readonly logger: Logger = new Logger(SearchItemCommand.name);
|
||||
private readonly logger: Logger = new Logger(PlayItemCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly jellyfinSearchService: JellyfinSearchService,
|
||||
@ -102,15 +103,15 @@ export class SearchItemCommand
|
||||
|
||||
return {
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: 'Jellyfin Search Results',
|
||||
iconURL:
|
||||
'https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true',
|
||||
})
|
||||
.setColor(DefaultJellyfinColor)
|
||||
.setDescription(description)
|
||||
.toJSON(),
|
||||
this.discordMessageService.buildMessage({
|
||||
title: '',
|
||||
mixin(embedBuilder) {
|
||||
return embedBuilder.setAuthor({
|
||||
name: 'Jellyfin Search Results',
|
||||
iconURL: Constants.Design.Icons.JellyfinLogo,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
components: [
|
||||
{
|
@ -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}`,
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
40
src/commands/previous.command.ts
Normal file
40
src/commands/previous.command.ts
Normal 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',
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
@ -1,23 +1,40 @@
|
||||
import { TransformPipe } from '@discord-nestjs/common';
|
||||
|
||||
import {
|
||||
Command,
|
||||
DiscordTransformedCommand,
|
||||
TransformedCommandExecutionContext,
|
||||
UsePipes,
|
||||
} from '@discord-nestjs/core';
|
||||
import { InteractionReplyOptions } from 'discord.js';
|
||||
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: 'skip',
|
||||
description: 'Skip the current track',
|
||||
})
|
||||
@UsePipes(TransformPipe)
|
||||
export class SkipTrackCommand implements DiscordTransformedCommand<unknown> {
|
||||
export class SkipTrackCommand implements DiscordCommand {
|
||||
constructor(
|
||||
private readonly playbackService: PlaybackService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
handler(
|
||||
dto: unknown,
|
||||
executionContext: TransformedCommandExecutionContext<any>,
|
||||
interactionCommand: CommandInteraction,
|
||||
): 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',
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -25,12 +25,49 @@ export class PlaybackService {
|
||||
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) {
|
||||
const uuid = uuidv4();
|
||||
|
||||
const emptyBefore = this.playlist.tracks.length === 0;
|
||||
|
||||
this.playlist.tracks.push({
|
||||
id: uuid,
|
||||
track: track,
|
||||
});
|
||||
|
||||
if (emptyBefore) {
|
||||
this.setActiveTrack(this.playlist.tracks.find((x) => x.id === uuid).id);
|
||||
}
|
||||
|
||||
return this.playlist.tracks.findIndex((x) => x.id === uuid);
|
||||
}
|
||||
|
||||
@ -45,7 +82,23 @@ export class PlaybackService {
|
||||
this.playlist.tracks = [];
|
||||
}
|
||||
|
||||
hasActiveTrack() {
|
||||
return this.playlist.activeTrack !== null;
|
||||
}
|
||||
|
||||
getPlaylist(): Playlist {
|
||||
return this.playlist;
|
||||
}
|
||||
|
||||
private getTrackById(id: string) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -8,4 +8,15 @@ export const Constants = {
|
||||
ReportIssue:
|
||||
'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
11
src/utils/timeUtils.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user