discord-jellyfin-bot/src/commands/play.comands.ts

225 lines
6.3 KiB
TypeScript
Raw Normal View History

2022-12-17 01:25:45 +01:00
import { TransformPipe } from '@discord-nestjs/common';
import {
Command,
DiscordTransformedCommand,
2022-12-17 14:13:03 +01:00
On,
2022-12-17 01:25:45 +01:00
Payload,
TransformedCommandExecutionContext,
UsePipes,
} from '@discord-nestjs/core';
2022-12-17 14:13:03 +01:00
import { Logger } from '@nestjs/common/services';
2022-12-17 01:25:45 +01:00
import {
ComponentType,
2022-12-17 14:13:03 +01:00
Events,
GuildMember,
2022-12-17 14:13:03 +01:00
Interaction,
2022-12-17 01:25:45 +01:00
InteractionReplyOptions,
} from 'discord.js';
import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service';
import { TrackRequestDto } from '../models/track-request.dto';
2022-12-17 14:13:03 +01:00
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { formatDuration, intervalToDuration } from 'date-fns';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
2022-12-17 14:13:03 +01:00
import { PlaybackService } from '../playback/playback.service';
2022-12-17 19:52:32 +01:00
import { Constants } from '../utils/constants';
import { trimStringToFixedLength } from '../utils/stringUtils';
2022-12-17 14:13:03 +01:00
2022-12-17 01:25:45 +01:00
@Command({
2022-12-17 19:52:32 +01:00
name: 'play',
2022-12-17 01:25:45 +01:00
description: 'Search for an item on your Jellyfin instance',
})
@UsePipes(TransformPipe)
2022-12-17 19:52:32 +01:00
export class PlayItemCommand
2022-12-17 01:25:45 +01:00
implements DiscordTransformedCommand<TrackRequestDto>
{
2022-12-17 19:52:32 +01:00
private readonly logger: Logger = new Logger(PlayItemCommand.name);
2022-12-17 14:13:03 +01:00
constructor(
private readonly jellyfinSearchService: JellyfinSearchService,
private readonly discordMessageService: DiscordMessageService,
private readonly discordVoiceService: DiscordVoiceService,
2022-12-17 14:13:03 +01:00
private readonly playbackService: PlaybackService,
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
2022-12-17 14:13:03 +01:00
) {}
2022-12-17 01:25:45 +01:00
async handler(
@Payload() dto: TrackRequestDto,
executionContext: TransformedCommandExecutionContext<any>,
): Promise<InteractionReplyOptions | string> {
const items = await this.jellyfinSearchService.search(dto.search);
if (items.length < 1) {
return {
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'No results for your search query found',
description: `I was not able to find any matches for your query \`\`${dto.search}\`\`. Please check that I have access to the desired libraries and that your query is not misspelled`,
}),
],
};
}
2022-12-17 01:25:45 +01:00
const firstItems = items.slice(0, 10);
const lines: string[] = firstItems.map(
(item) =>
`:white_small_square: ${trimStringToFixedLength(
this.markSearchTermOverlap(item.Name, dto.search),
30,
)} [${item.Artists.join(', ')}] *(${item.Type})*`,
2022-12-17 01:25:45 +01:00
);
let description =
'I have found **' +
items.length +
'** results for your search ``' +
dto.search +
'``.';
if (items.length > 10) {
description +=
'\nSince the results exceed 10 items, I truncated them for better readability.';
}
description += '\n\n' + lines.join('\n');
2022-12-17 01:25:45 +01:00
const emojiForType = (type: string) => {
switch (type) {
case 'Audio':
return '🎵';
case 'Playlist':
return '📚';
default:
return undefined;
}
};
const selectOptions: { label: string; value: string; emoji?: string }[] =
firstItems.map((item) => ({
label: `${item.Name} [${item.Artists.join(', ')}]`,
2022-12-17 01:25:45 +01:00
value: item.Id,
emoji: emojiForType(item.Type),
}));
return {
embeds: [
2022-12-17 19:52:32 +01:00
this.discordMessageService.buildMessage({
title: 'a',
description: description,
2022-12-17 19:52:32 +01:00
mixin(embedBuilder) {
return embedBuilder.setAuthor({
name: 'Jellyfin Search Results',
iconURL: Constants.Design.Icons.JellyfinLogo,
});
},
}),
2022-12-17 01:25:45 +01:00
],
components: [
{
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.StringSelect,
2022-12-17 14:13:03 +01:00
customId: 'searchItemSelect',
2022-12-17 01:25:45 +01:00
options: selectOptions,
},
],
},
],
};
}
2022-12-17 14:13:03 +01:00
@On(Events.InteractionCreate)
async onStringSelect(interaction: Interaction) {
if (!interaction.isStringSelectMenu()) return;
if (interaction.customId !== 'searchItemSelect') {
return;
}
if (interaction.values.length !== 1) {
this.logger.warn(
`Failed to process interaction select with values [${interaction.values.length}]`,
);
return;
}
const item = await this.jellyfinSearchService.getById(
interaction.values[0],
);
const guildMember = interaction.member as GuildMember;
const tryResult =
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
guildMember,
);
if (!tryResult.success) {
this.logger.warn(
`Unable to process select result because the member was not in a voice channcel`,
);
const replyOptions = tryResult.reply as InteractionReplyOptions;
await interaction.update({
embeds: replyOptions.embeds,
content: undefined,
components: [],
});
return;
}
const bitrate = guildMember.voice.channel.bitrate;
const stream = await this.jellyfinStreamBuilder.buildStreamUrl(
item.Id,
bitrate,
);
const milliseconds = item.RunTimeTicks / 10000;
const duration = formatDuration(
intervalToDuration({
start: milliseconds,
end: 0,
}),
);
const addedIndex = this.playbackService.eneuqueTrack({
jellyfinId: item.Id,
name: item.Name,
durationInMilliseconds: milliseconds,
streamUrl: stream,
});
const artists = item.Artists.join(', ');
2022-12-17 14:13:03 +01:00
await interaction.update({
embeds: [
this.discordMessageService.buildMessage({
title: 'Jellyfin Search',
description: `**Duration**: ${duration}\n**Artists**: ${artists}\n\nTrack was added to the queue at position ${addedIndex}`,
}),
2022-12-17 14:13:03 +01:00
],
components: [],
});
}
2022-12-17 01:25:45 +01:00
private markSearchTermOverlap(value: string, searchTerm: string) {
const startIndex = value.indexOf(searchTerm);
const actualValue = value.substring(
startIndex,
startIndex + 1 + searchTerm.length,
);
return `${value.substring(
0,
startIndex,
)}**${actualValue}**${value.substring(
startIndex + 1 + actualValue.length,
)}`;
}
}