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
|
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}`,
|
||||||
|
@ -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,
|
||||||
],
|
],
|
||||||
|
@ -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:';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: [
|
||||||
{
|
{
|
@ -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 { 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',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
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