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
|
## ⛔ Limitations
|
||||||
|
|
||||||
- Bot does not support shards. This means, you cannot use it in multiple servers concurrently.
|
- 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))
|
- Streaming any video content in voice channels (See [this issue](https://github.com/discordjs/discord.js/issues/4116))
|
||||||
|
|
||||||
## 🚀 Installation
|
## 🚀 Installation
|
||||||
|
@ -3,10 +3,12 @@ import { JellyfinService } from './jellyfin.service';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BaseItemKind,
|
BaseItemKind,
|
||||||
|
RemoteImageResult,
|
||||||
SearchHint,
|
SearchHint,
|
||||||
} from '@jellyfin/sdk/lib/generated-client/models';
|
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
import { getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api/playlists-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 { getSearchApi } from '@jellyfin/sdk/lib/utils/api/search-api';
|
||||||
import { Logger } from '@nestjs/common/services';
|
import { Logger } from '@nestjs/common/services';
|
||||||
import {
|
import {
|
||||||
@ -102,4 +104,28 @@ export class JellyfinSearchService {
|
|||||||
|
|
||||||
return data.Items[0];
|
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,
|
searchResultAsJellyfinAudio,
|
||||||
} from '../models/jellyfinAudioItems';
|
} from '../models/jellyfinAudioItems';
|
||||||
import { PlaybackService } from '../playback/playback.service';
|
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({
|
@Command({
|
||||||
name: 'play',
|
name: 'play',
|
||||||
@ -170,15 +173,28 @@ export class PlayItemCommand
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case 'track':
|
case 'track':
|
||||||
const item = await this.jellyfinSearchService.getById(id);
|
const item = await this.jellyfinSearchService.getById(id);
|
||||||
|
const remoteImagesOfCurrentAlbum =
|
||||||
|
await this.jellyfinSearchService.getRemoteImageById(item.AlbumId);
|
||||||
|
const trackRemoteImage = chooseSuitableRemoteImage(
|
||||||
|
remoteImagesOfCurrentAlbum,
|
||||||
|
);
|
||||||
const addedIndex = this.enqueueSingleTrack(
|
const addedIndex = this.enqueueSingleTrack(
|
||||||
item as BaseJellyfinAudioPlayable,
|
item as BaseJellyfinAudioPlayable,
|
||||||
bitrate,
|
bitrate,
|
||||||
|
remoteImagesOfCurrentAlbum,
|
||||||
);
|
);
|
||||||
interaction.update({
|
interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: item.Name,
|
title: item.Name,
|
||||||
description: `Your track was added to the position ${addedIndex} in the playlist`,
|
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: [],
|
components: [],
|
||||||
@ -186,13 +202,30 @@ export class PlayItemCommand
|
|||||||
break;
|
break;
|
||||||
case 'album':
|
case 'album':
|
||||||
const album = await this.jellyfinSearchService.getItemsByAlbum(id);
|
const album = await this.jellyfinSearchService.getItemsByAlbum(id);
|
||||||
|
const remoteImages =
|
||||||
|
await this.jellyfinSearchService.getRemoteImageById(id);
|
||||||
|
const albumRemoteImage = chooseSuitableRemoteImage(remoteImages);
|
||||||
album.SearchHints.forEach((item) => {
|
album.SearchHints.forEach((item) => {
|
||||||
this.enqueueSingleTrack(item as BaseJellyfinAudioPlayable, bitrate);
|
this.enqueueSingleTrack(
|
||||||
|
item as BaseJellyfinAudioPlayable,
|
||||||
|
bitrate,
|
||||||
|
remoteImages,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
interaction.update({
|
interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: `Added ${album.TotalRecordCount} items from your album`,
|
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: [],
|
components: [],
|
||||||
@ -200,13 +233,34 @@ export class PlayItemCommand
|
|||||||
break;
|
break;
|
||||||
case 'playlist':
|
case 'playlist':
|
||||||
const playlist = await this.jellyfinSearchService.getPlaylistById(id);
|
const playlist = await this.jellyfinSearchService.getPlaylistById(id);
|
||||||
playlist.Items.forEach((item) => {
|
const addedRemoteImages: RemoteImageResult = {};
|
||||||
this.enqueueSingleTrack(item as BaseJellyfinAudioPlayable, bitrate);
|
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({
|
interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: `Added ${playlist.TotalRecordCount} items from your playlist`,
|
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: [],
|
components: [],
|
||||||
@ -231,6 +285,7 @@ export class PlayItemCommand
|
|||||||
private enqueueSingleTrack(
|
private enqueueSingleTrack(
|
||||||
jellyfinPlayable: BaseJellyfinAudioPlayable,
|
jellyfinPlayable: BaseJellyfinAudioPlayable,
|
||||||
bitrate: number,
|
bitrate: number,
|
||||||
|
remoteImageResult: RemoteImageResult,
|
||||||
) {
|
) {
|
||||||
const stream = this.jellyfinStreamBuilder.buildStreamUrl(
|
const stream = this.jellyfinStreamBuilder.buildStreamUrl(
|
||||||
jellyfinPlayable.Id,
|
jellyfinPlayable.Id,
|
||||||
@ -244,6 +299,7 @@ export class PlayItemCommand
|
|||||||
name: jellyfinPlayable.Name,
|
name: jellyfinPlayable.Name,
|
||||||
durationInMilliseconds: milliseconds,
|
durationInMilliseconds: milliseconds,
|
||||||
streamUrl: stream,
|
streamUrl: stream,
|
||||||
|
remoteImages: remoteImageResult,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { DiscordMessageService } from '../clients/discord/discord.message.servic
|
|||||||
import { GenericCustomReply } from '../models/generic-try-handler';
|
import { GenericCustomReply } from '../models/generic-try-handler';
|
||||||
import { PlaybackService } from '../playback/playback.service';
|
import { PlaybackService } from '../playback/playback.service';
|
||||||
import { Constants } from '../utils/constants';
|
import { Constants } from '../utils/constants';
|
||||||
|
import { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages';
|
||||||
import { trimStringToFixedLength } from '../utils/stringUtils';
|
import { trimStringToFixedLength } from '../utils/stringUtils';
|
||||||
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
|
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
|
||||||
|
|
||||||
@ -59,11 +60,21 @@ export class PlaylistCommand implements DiscordCommand {
|
|||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
|
const activeTrack = this.playbackService.getActiveTrack();
|
||||||
|
const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Your Playlist',
|
title: 'Your Playlist',
|
||||||
description: `${tracklist}\n\nUse the /skip and /previous command to select a track`,
|
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,
|
durationInMilliseconds: this.RunTimeTicks / 1000,
|
||||||
jellyfinId: this.Id,
|
jellyfinId: this.Id,
|
||||||
streamUrl: jellyfinStreamBuilder.buildStreamUrl(this.Id, bitrate),
|
streamUrl: jellyfinStreamBuilder.buildStreamUrl(this.Id, bitrate),
|
||||||
|
remoteImages: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
|
||||||
|
|
||||||
export interface Track {
|
export interface Track {
|
||||||
jellyfinId: string;
|
jellyfinId: string;
|
||||||
name: string;
|
name: string;
|
||||||
durationInMilliseconds: number;
|
durationInMilliseconds: number;
|
||||||
streamUrl: string;
|
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