mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-25 02:51:57 +01:00
parent
92021dd94e
commit
3d722dbc9c
@ -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
|
||||
|
@ -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<RemoteImageResult> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
@ -111,6 +111,7 @@ export class JellyfinAudioItem implements BaseJellyfinAudioPlayable {
|
||||
durationInMilliseconds: this.RunTimeTicks / 1000,
|
||||
jellyfinId: this.Id,
|
||||
streamUrl: jellyfinStreamBuilder.buildStreamUrl(this.Id, bitrate),
|
||||
remoteImages: {},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
25
src/utils/remoteImages.ts
Normal file
25
src/utils/remoteImages.ts
Normal file
@ -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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user