diff --git a/src/clients/jellyfin/jellyfin.search.service.ts b/src/clients/jellyfin/jellyfin.search.service.ts index 1b8d60f..873bd12 100644 --- a/src/clients/jellyfin/jellyfin.search.service.ts +++ b/src/clients/jellyfin/jellyfin.search.service.ts @@ -3,6 +3,7 @@ import { JellyfinService } from './jellyfin.service'; import { SearchHint } from '@jellyfin/sdk/lib/generated-client/models'; import { getSearchApi } from '@jellyfin/sdk/lib/utils/api/search-api'; +import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { Logger } from '@nestjs/common/services'; @Injectable() @@ -28,4 +29,20 @@ export class JellyfinSearchService { return SearchHints; } + + async getById(id: string): Promise { + const api = this.jellyfinService.getApi(); + + const searchApi = getItemsApi(api); + const { data } = await searchApi.getItems({ + ids: [id], + }); + + if (data.Items.length !== 1) { + this.logger.warn(`Failed to retrieve item via id '${id}'`); + return null; + } + + return data.Items[0]; + } } diff --git a/src/commands/enqueue.command.ts b/src/commands/enqueue.command.ts index 3c7eea4..0498257 100644 --- a/src/commands/enqueue.command.ts +++ b/src/commands/enqueue.command.ts @@ -28,7 +28,8 @@ export class EnqueueCommand dto: TrackRequestDto, executionContext: TransformedCommandExecutionContext, ): InteractionReplyOptions | string { - const index = this.playbackService.eneuqueTrack({}); + // const index = this.playbackService.eneuqueTrack({}); + const index = 0; return { embeds: [ this.discordMessageService.buildMessage({ diff --git a/src/commands/search.comands.ts b/src/commands/search.comands.ts index bea924a..4a1e5d9 100644 --- a/src/commands/search.comands.ts +++ b/src/commands/search.comands.ts @@ -3,21 +3,31 @@ import { TransformPipe } from '@discord-nestjs/common'; import { Command, DiscordTransformedCommand, - Param, + On, Payload, TransformedCommandExecutionContext, UsePipes, } from '@discord-nestjs/core'; +import { SearchHint } from '@jellyfin/sdk/lib/generated-client/models'; +import { Logger } from '@nestjs/common/services'; import { - APIEmbedField, ComponentType, EmbedBuilder, + Events, + Interaction, InteractionReplyOptions, } from 'discord.js'; import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service'; import { TrackRequestDto } from '../models/track-request.dto'; import { DefaultJellyfinColor } from '../types/colors'; +import { v4 as uuidv4 } from 'uuid'; +import { DiscordMessageService } from '../clients/discord/discord.message.service'; + +import { formatDuration, intervalToDuration } from 'date-fns'; +import { format } from 'path'; +import { PlaybackService } from '../playback/playback.service'; + @Command({ name: 'search', description: 'Search for an item on your Jellyfin instance', @@ -26,7 +36,13 @@ import { DefaultJellyfinColor } from '../types/colors'; export class SearchItemCommand implements DiscordTransformedCommand { - constructor(private readonly jellyfinSearchService: JellyfinSearchService) {} + private readonly logger: Logger = new Logger(SearchItemCommand.name); + + constructor( + private readonly jellyfinSearchService: JellyfinSearchService, + private readonly discordMessageService: DiscordMessageService, + private readonly playbackService: PlaybackService, + ) {} async handler( @Payload() dto: TrackRequestDto, @@ -88,7 +104,7 @@ export class SearchItemCommand components: [ { type: ComponentType.StringSelect, - customId: 'cool', + customId: 'searchItemSelect', options: selectOptions, }, ], @@ -97,6 +113,61 @@ export class SearchItemCommand }; } + @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, + }); + + 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: [], + }); + } + private markSearchTermOverlap(value: string, searchTerm: string) { const startIndex = value.indexOf(searchTerm); const actualValue = value.substring( diff --git a/src/types/track.ts b/src/types/track.ts index c7f9f8b..2954289 100644 --- a/src/types/track.ts +++ b/src/types/track.ts @@ -1 +1,5 @@ -export interface Track {} \ No newline at end of file +export interface Track { + jellyfinId: string; + name: string; + durationInMilliseconds: number; +}