diff --git a/src/clients/jellyfin/jellyfin.search.service.ts b/src/clients/jellyfin/jellyfin.search.service.ts index dc3a56b..b9776d6 100644 --- a/src/clients/jellyfin/jellyfin.search.service.ts +++ b/src/clients/jellyfin/jellyfin.search.service.ts @@ -1,12 +1,17 @@ import { Injectable } from '@nestjs/common'; import { JellyfinService } from './jellyfin.service'; -import { SearchHint } from '@jellyfin/sdk/lib/generated-client/models'; +import { + BaseItemDto, + BaseItemKind, + SearchHint, + SearchHintResult, +} from '@jellyfin/sdk/lib/generated-client/models'; import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-api'; import { getSearchApi } from '@jellyfin/sdk/lib/utils/api/search-api'; import { Logger } from '@nestjs/common/services'; -import { JellyfinAudioPlaylist } from '../../models/jellyfinAudioItems'; +import { JellyfinAudioPlaylist, JellyfinMusicAlbum } from '../../models/jellyfinAudioItems'; @Injectable() export class JellyfinSearchService { @@ -25,9 +30,15 @@ export class JellyfinSearchService { status, } = await searchApi.get({ searchTerm: searchTerm, - mediaTypes: ['Audio', 'MusicAlbum', 'Playlist'], + includeItemTypes: [ + BaseItemKind.Audio, + BaseItemKind.MusicAlbum, + BaseItemKind.Playlist, + ], }); + console.log(SearchHints); + if (status !== 200) { this.logger.error(`Jellyfin Search failed with status code ${status}`); return []; @@ -48,13 +59,35 @@ export class JellyfinSearchService { }); if (axiosResponse.status !== 200) { - this.logger.error(`Jellyfin Search failed with status code ${status}`); + this.logger.error( + `Jellyfin Search failed with status code ${axiosResponse.status}`, + ); return new JellyfinAudioPlaylist(); } return axiosResponse.data as JellyfinAudioPlaylist; } + async getItemsByAlbum(albumId: string): Promise { + const api = this.jellyfinService.getApi(); + const searchApi = getSearchApi(api); + const axiosResponse = await searchApi.get({ + parentId: albumId, + userId: this.jellyfinService.getUserId(), + mediaTypes: [BaseItemKind[BaseItemKind.Audio]], + searchTerm: '%', + }); + + if (axiosResponse.status !== 200) { + this.logger.error( + `Jellyfin Search failed with status code ${axiosResponse.status}`, + ); + return new JellyfinMusicAlbum(); + } + + return axiosResponse.data as JellyfinMusicAlbum; + } + async getById(id: string): Promise { const api = this.jellyfinService.getApi(); diff --git a/src/commands/play.comands.ts b/src/commands/play.comands.ts index ce23dfb..dbc1140 100644 --- a/src/commands/play.comands.ts +++ b/src/commands/play.comands.ts @@ -190,6 +190,21 @@ export class PlayItemCommand components: [], }); break; + case 'album': + const album = await this.jellyfinSearchService.getItemsByAlbum(id); + console.log(album); + album.SearchHints.forEach((item) => { + this.enqueueSingleTrack(item as BaseJellyfinAudioPlayable, bitrate); + }); + interaction.update({ + embeds: [ + this.discordMessageService.buildMessage({ + title: `Added ${album.TotalRecordCount} items from your album`, + }), + ], + components: [], + }); + break; case 'playlist': const playlist = await this.jellyfinSearchService.getPlaylistById(id); playlist.Items.forEach((item) => { @@ -209,7 +224,7 @@ export class PlayItemCommand embeds: [ this.discordMessageService.buildErrorMessage({ title: 'Unable to process your selection', - description: `Sorry. I don't know the type you selected: \`\`${type}\`\`. Please report this bug to the developers.\n\nDebug Information:\`\`${interaction.values.join( + description: `Sorry. I don't know the type you selected: \`\`${type}\`\`. Please report this bug to the developers.\n\nDebug Information: \`\`${interaction.values.join( ', ', )}\`\``, }), diff --git a/src/commands/playlist.command.ts b/src/commands/playlist.command.ts index 4d2aeb6..138db8c 100644 --- a/src/commands/playlist.command.ts +++ b/src/commands/playlist.command.ts @@ -56,7 +56,7 @@ export class PlaylistCommand implements DiscordCommand { return point; }) - .join(',\n'); + .join('\n'); return { embeds: [ diff --git a/src/models/jellyfinAudioItems.ts b/src/models/jellyfinAudioItems.ts index c358c19..5e83f15 100644 --- a/src/models/jellyfinAudioItems.ts +++ b/src/models/jellyfinAudioItems.ts @@ -1,4 +1,7 @@ -import { SearchHint } from '@jellyfin/sdk/lib/generated-client/models'; +import { + BaseItemKind, + SearchHint, +} from '@jellyfin/sdk/lib/generated-client/models'; import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service'; import { Track } from '../types/track'; import { trimStringToFixedLength } from '../utils/stringUtils'; @@ -161,22 +164,70 @@ export class JellyfinAudioPlaylist implements BaseJellyfinAudioPlayable { TotalRecordCount: number; } +export class JellyfinMusicAlbum implements BaseJellyfinAudioPlayable { + Id: string; + Name: string; + RunTimeTicks: number; + SearchHints: JellyfinAudioItem[]; + TotalRecordCount: number; + + async fromSearchHint( + jellyfinSearchService: JellyfinSearchService, + searchHint: SearchHint, + ): Promise { + this.Id = searchHint.Id; + this.Name = searchHint.Name; + this.RunTimeTicks = searchHint.RunTimeTicks; + const album = await jellyfinSearchService.getItemsByAlbum(searchHint.Id); + this.SearchHints = album.SearchHints; + this.TotalRecordCount = album.TotalRecordCount; + return this; + } + fetchTracks( + jellyfinStreamBuilder: JellyfinStreamBuilderService, + bitrate: number, + ): Track[] { + return this.SearchHints.flatMap((item) => + item.fetchTracks(jellyfinStreamBuilder, bitrate), + ); + } + prettyPrint(search: string): string { + return `${markSearchTermOverlap(this.Name, search)} (${ + this.TotalRecordCount + } items) (Album)`; + } + getId(): string { + return this.Id; + } + getValueId(): string { + return `album_${this.getId()}`; + } + getEmoji(): string { + return '📀'; + } +} + export const searchResultAsJellyfinAudio = async ( logger: Logger, jellyfinSearchService: JellyfinSearchService, searchHint: SearchHint, ) => { switch (searchHint.Type) { - case 'Audio': + case BaseItemKind[BaseItemKind.Audio]: return await new JellyfinAudioItem().fromSearchHint( jellyfinSearchService, searchHint, ); - case 'Playlist': + case BaseItemKind[BaseItemKind.Playlist]: return await new JellyfinAudioPlaylist().fromSearchHint( jellyfinSearchService, searchHint, ); + case BaseItemKind[BaseItemKind.MusicAlbum]: + return await new JellyfinMusicAlbum().fromSearchHint( + jellyfinSearchService, + searchHint, + ); default: logger.error( `Failed to parse Jellyfin response for item type ${searchHint.Type}`,