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,
|
2022-12-17 16:58:38 +01:00
|
|
|
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';
|
|
|
|
|
2022-12-17 16:58:38 +01:00
|
|
|
import { createAudioResource } from '@discordjs/voice';
|
2022-12-17 14:13:03 +01:00
|
|
|
import { formatDuration, intervalToDuration } from 'date-fns';
|
2022-12-17 16:58:38 +01:00
|
|
|
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,
|
2022-12-17 16:58:38 +01:00
|
|
|
private readonly discordVoiceService: DiscordVoiceService,
|
2022-12-17 14:13:03 +01:00
|
|
|
private readonly playbackService: PlaybackService,
|
2022-12-17 16:58:38 +01:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
|
2022-12-17 16:58:38 +01:00
|
|
|
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,
|
|
|
|
)}`;
|
|
|
|
}
|
|
|
|
}
|