From 3d722dbc9cd716e0b9d57cd67ea2fe21267f320a Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Sun, 25 Dec 2022 21:57:37 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Remote=20images=20api=20#17=20(?= =?UTF-8?q?#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- .../jellyfin/jellyfin.search.service.ts | 26 ++++++++ src/commands/play.comands.ts | 64 +++++++++++++++++-- src/commands/playlist.command.ts | 11 ++++ src/models/jellyfinAudioItems.ts | 1 + src/types/track.ts | 3 + src/utils/remoteImages.ts | 25 ++++++++ 7 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 src/utils/remoteImages.ts diff --git a/README.md b/README.md index 649b327..e4abd87 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This project was originally started by [KGT1 on Github](https://github.com/KGT1/ ## ⛔ Limitations - Bot does not support shards. This means, you cannot use it in multiple servers concurrently. -- Displaying media covers or images in Discord (Jellyfin is self hosted, and other users woudln't be able to see those images) +- Album covers are not visible, unless they are remote (eg. provided by external metadata provider) - Streaming any video content in voice channels (See [this issue](https://github.com/discordjs/discord.js/issues/4116)) ## 🚀 Installation diff --git a/src/clients/jellyfin/jellyfin.search.service.ts b/src/clients/jellyfin/jellyfin.search.service.ts index 047d16c..ddf47bf 100644 --- a/src/clients/jellyfin/jellyfin.search.service.ts +++ b/src/clients/jellyfin/jellyfin.search.service.ts @@ -3,10 +3,12 @@ import { JellyfinService } from './jellyfin.service'; import { BaseItemKind, + RemoteImageResult, SearchHint, } 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 { getRemoteImageApi } from '@jellyfin/sdk/lib/utils/api/remote-image-api'; import { getSearchApi } from '@jellyfin/sdk/lib/utils/api/search-api'; import { Logger } from '@nestjs/common/services'; import { @@ -102,4 +104,28 @@ export class JellyfinSearchService { return data.Items[0]; } + + async getRemoteImageById(id: string): Promise { + const api = this.jellyfinService.getApi(); + const remoteImageApi = getRemoteImageApi(api); + + const axiosReponse = await remoteImageApi.getRemoteImages({ + itemId: id, + includeAllLanguages: true, + limit: 20, + }); + + if (axiosReponse.status !== 200) { + this.logger.warn( + `Failed to retrieve remote images. Response has status ${axiosReponse.status}`, + ); + return { + Images: [], + Providers: [], + TotalRecordCount: 0, + }; + } + + return axiosReponse.data; + } } diff --git a/src/commands/play.comands.ts b/src/commands/play.comands.ts index 6e17997..8ac1708 100644 --- a/src/commands/play.comands.ts +++ b/src/commands/play.comands.ts @@ -28,6 +28,9 @@ import { searchResultAsJellyfinAudio, } from '../models/jellyfinAudioItems'; import { PlaybackService } from '../playback/playback.service'; +import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models'; +import { chooseSuitableRemoteImage } from '../utils/remoteImages'; +import { trimStringToFixedLength } from '../utils/stringUtils'; @Command({ name: 'play', @@ -170,15 +173,28 @@ export class PlayItemCommand switch (type) { case 'track': const item = await this.jellyfinSearchService.getById(id); + const remoteImagesOfCurrentAlbum = + await this.jellyfinSearchService.getRemoteImageById(item.AlbumId); + const trackRemoteImage = chooseSuitableRemoteImage( + remoteImagesOfCurrentAlbum, + ); const addedIndex = this.enqueueSingleTrack( item as BaseJellyfinAudioPlayable, bitrate, + remoteImagesOfCurrentAlbum, ); interaction.update({ embeds: [ this.discordMessageService.buildMessage({ title: item.Name, description: `Your track was added to the position ${addedIndex} in the playlist`, + mixin(embedBuilder) { + if (trackRemoteImage === undefined) { + return embedBuilder; + } + + return embedBuilder.setThumbnail(trackRemoteImage.Url); + }, }), ], components: [], @@ -186,13 +202,30 @@ export class PlayItemCommand break; case 'album': const album = await this.jellyfinSearchService.getItemsByAlbum(id); + const remoteImages = + await this.jellyfinSearchService.getRemoteImageById(id); + const albumRemoteImage = chooseSuitableRemoteImage(remoteImages); album.SearchHints.forEach((item) => { - this.enqueueSingleTrack(item as BaseJellyfinAudioPlayable, bitrate); + this.enqueueSingleTrack( + item as BaseJellyfinAudioPlayable, + bitrate, + remoteImages, + ); }); interaction.update({ embeds: [ this.discordMessageService.buildMessage({ title: `Added ${album.TotalRecordCount} items from your album`, + description: `${album.SearchHints.map((item) => + trimStringToFixedLength(item.Name, 20), + ).join(', ')}`, + mixin(embedBuilder) { + if (albumRemoteImage === undefined) { + return embedBuilder; + } + + return embedBuilder.setThumbnail(albumRemoteImage.Url); + }, }), ], components: [], @@ -200,13 +233,34 @@ export class PlayItemCommand break; case 'playlist': const playlist = await this.jellyfinSearchService.getPlaylistById(id); - playlist.Items.forEach((item) => { - this.enqueueSingleTrack(item as BaseJellyfinAudioPlayable, bitrate); - }); + const addedRemoteImages: RemoteImageResult = {}; + for (let index = 0; index < playlist.Items.length; index++) { + const item = playlist.Items[index]; + const remoteImages = + await this.jellyfinSearchService.getRemoteImageById(id); + addedRemoteImages.Images.concat(remoteImages.Images); + this.enqueueSingleTrack( + item as BaseJellyfinAudioPlayable, + bitrate, + remoteImages, + ); + } + const bestPlaylistRemoteImage = + chooseSuitableRemoteImage(addedRemoteImages); interaction.update({ embeds: [ this.discordMessageService.buildMessage({ title: `Added ${playlist.TotalRecordCount} items from your playlist`, + description: `${playlist.Items.map((item) => + trimStringToFixedLength(item.Name, 20), + ).join(', ')}`, + mixin(embedBuilder) { + if (bestPlaylistRemoteImage === undefined) { + return embedBuilder; + } + + return embedBuilder.setThumbnail(bestPlaylistRemoteImage.Url); + }, }), ], components: [], @@ -231,6 +285,7 @@ export class PlayItemCommand private enqueueSingleTrack( jellyfinPlayable: BaseJellyfinAudioPlayable, bitrate: number, + remoteImageResult: RemoteImageResult, ) { const stream = this.jellyfinStreamBuilder.buildStreamUrl( jellyfinPlayable.Id, @@ -244,6 +299,7 @@ export class PlayItemCommand name: jellyfinPlayable.Name, durationInMilliseconds: milliseconds, streamUrl: stream, + remoteImages: remoteImageResult, }); } } diff --git a/src/commands/playlist.command.ts b/src/commands/playlist.command.ts index fbd1f2a..41a306d 100644 --- a/src/commands/playlist.command.ts +++ b/src/commands/playlist.command.ts @@ -6,6 +6,7 @@ import { DiscordMessageService } from '../clients/discord/discord.message.servic import { GenericCustomReply } from '../models/generic-try-handler'; import { PlaybackService } from '../playback/playback.service'; import { Constants } from '../utils/constants'; +import { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages'; import { trimStringToFixedLength } from '../utils/stringUtils'; import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils'; @@ -59,11 +60,21 @@ export class PlaylistCommand implements DiscordCommand { }) .join('\n'); + const activeTrack = this.playbackService.getActiveTrack(); + const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track); + return { embeds: [ this.discordMessageService.buildMessage({ title: 'Your Playlist', description: `${tracklist}\n\nUse the /skip and /previous command to select a track`, + mixin(embedBuilder) { + if (remoteImage === undefined) { + return embedBuilder; + } + + return embedBuilder.setThumbnail(remoteImage.Url); + }, }), ], }; diff --git a/src/models/jellyfinAudioItems.ts b/src/models/jellyfinAudioItems.ts index 0dcf77b..3e592f9 100644 --- a/src/models/jellyfinAudioItems.ts +++ b/src/models/jellyfinAudioItems.ts @@ -111,6 +111,7 @@ export class JellyfinAudioItem implements BaseJellyfinAudioPlayable { durationInMilliseconds: this.RunTimeTicks / 1000, jellyfinId: this.Id, streamUrl: jellyfinStreamBuilder.buildStreamUrl(this.Id, bitrate), + remoteImages: {}, }, ]; } diff --git a/src/types/track.ts b/src/types/track.ts index bc60421..9fd9b94 100644 --- a/src/types/track.ts +++ b/src/types/track.ts @@ -1,6 +1,9 @@ +import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models'; + export interface Track { jellyfinId: string; name: string; durationInMilliseconds: number; streamUrl: string; + remoteImages: RemoteImageResult; } diff --git a/src/utils/remoteImages.ts b/src/utils/remoteImages.ts new file mode 100644 index 0000000..707e2ed --- /dev/null +++ b/src/utils/remoteImages.ts @@ -0,0 +1,25 @@ +import { + ImageType, + RemoteImageInfo, + RemoteImageResult, +} from '@jellyfin/sdk/lib/generated-client/models'; +import { Track } from '../types/track'; + +export const chooseSuitableRemoteImage = ( + remoteImageResult: RemoteImageResult, +): RemoteImageInfo | undefined => { + const primaryImages: RemoteImageInfo[] | undefined = + remoteImageResult.Images.filter((x) => x.Type === ImageType.Primary); + + if (primaryImages.length > 0) { + return primaryImages[0]; + } + + if (remoteImageResult.Images.length > 0) { + return remoteImageResult.Images[0]; + } +}; + +export const chooseSuitableRemoteImageFromTrack = (track: Track) => { + return chooseSuitableRemoteImage(track.remoteImages); +}; From f29c22349f234d411d4dbdf921bc68191f230900 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Sun, 25 Dec 2022 22:54:31 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20repeated=20update=20an?= =?UTF-8?q?nouncement=20messages=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/updates/updates.service.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/updates/updates.service.ts b/src/updates/updates.service.ts index 3c237ec..8c8c970 100644 --- a/src/updates/updates.service.ts +++ b/src/updates/updates.service.ts @@ -12,6 +12,7 @@ import { Constants } from '../utils/constants'; @Injectable() export class UpdatesService { private readonly logger = new Logger(UpdatesService.name); + private hasAlreadyNotified: boolean; constructor( @InjectDiscordClient() private readonly client: Client, @@ -22,7 +23,7 @@ export class UpdatesService { async handleCron() { const isDisabled = process.env.UPDATER_DISABLE_NOTIFICATIONS; - if (isDisabled === 'true') { + if (isDisabled === 'true' || this.hasAlreadyNotified) { return; } @@ -36,6 +37,8 @@ export class UpdatesService { } await this.contactOwnerAboutUpdate(currentVersion, latestGitHubRelease); + + this.hasAlreadyNotified = true; } private async contactOwnerAboutUpdate( @@ -93,6 +96,12 @@ export class UpdatesService { } private async fetchLatestGithubRelease(): Promise { + return { + tag_name: '0.0.2', + name: 'fefeifheuf', + html_url: 'https://github.com', + published_at: '2022-12-19T08:52:30Z', + }; return axios({ method: 'GET', url: Constants.Links.Api.GetLatestRelease, From d37f01e5c45e312331d0cde5168099c5b55e4087 Mon Sep 17 00:00:00 2001 From: Manuel Ruwe Date: Sun, 25 Dec 2022 22:56:07 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=94=96=20Upgrade=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 2db6990..a49d7ab 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -3,7 +3,7 @@ export const Constants = { Version: { Major: 0, Minor: 0, - Patch: 1, + Patch: 2, All: () => `${Constants.Metadata.Version.Major}.${Constants.Metadata.Version.Minor}.${Constants.Metadata.Version.Patch}`, },