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

202 lines
5.6 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,
EmbedBuilder,
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';
import { DefaultJellyfinColor } from '../types/colors';
2022-12-17 14:13:03 +01:00
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { createAudioResource } from '@discordjs/voice';
2022-12-17 14:13:03 +01:00
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 01:25:45 +01:00
@Command({
name: 'search',
description: 'Search for an item on your Jellyfin instance',
})
@UsePipes(TransformPipe)
export class SearchItemCommand
implements DiscordTransformedCommand<TrackRequestDto>
{
2022-12-17 14:13:03 +01:00
private readonly logger: Logger = new Logger(SearchItemCommand.name);
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);
const firstItems = items.slice(0, 10);
const lines: string[] = firstItems.map(
(item) =>
`:white_small_square: ${this.markSearchTermOverlap(
item.Name,
dto.search,
)} *(${item.Type})*`,
);
const description = `I have found **${
items.length
}** results for your search \`\`${
dto.search
}\`\`.\nFor better readability, I have limited the search results to 10\n\n ${lines.join(
'\n',
)}`;
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,
value: item.Id,
emoji: emojiForType(item.Type),
}));
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(),
],
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 milliseconds = item.RunTimeTicks / 10000;
const duration = formatDuration(
intervalToDuration({
start: milliseconds,
end: 0,
}),
);
const artists = item.Artists.join(', ');
const addedIndex = this.playbackService.eneuqueTrack({
jellyfinId: item.Id,
name: item.Name,
durationInMilliseconds: milliseconds,
});
const guildMember = interaction.member as GuildMember;
const bitrate = guildMember.voice.channel.bitrate;
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
guildMember,
);
this.jellyfinStreamBuilder
.buildStreamUrl(item.Id, bitrate)
.then((stream) => {
const resource = createAudioResource(stream);
this.discordVoiceService.playResource(resource);
});
2022-12-17 14:13:03 +01:00
await interaction.update({
embeds: [
new EmbedBuilder()
.setAuthor({
name: 'Jellyfin Search',
iconURL:
'https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true',
})
.setTitle(item.Name)
.setDescription(
`**Duration**: ${duration}\n**Artists**: ${artists}\n\nTrack was added to the queue at position ${addedIndex}`,
)
.setColor(DefaultJellyfinColor)
.toJSON(),
],
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,
)}`;
}
}