🔀 Version 0.0.2

This commit is contained in:
Manuel 2022-12-25 22:59:44 +01:00 committed by Manuel Ruwe
commit 9f02a6058e
9 changed files with 132 additions and 7 deletions

View File

@ -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

View File

@ -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;
}
}

View File

@ -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,
});
}
}

View File

@ -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);
},
}),
],
};

View File

@ -111,6 +111,7 @@ export class JellyfinAudioItem implements BaseJellyfinAudioPlayable {
durationInMilliseconds: this.RunTimeTicks / 1000,
jellyfinId: this.Id,
streamUrl: jellyfinStreamBuilder.buildStreamUrl(this.Id, bitrate),
remoteImages: {},
},
];
}

View File

@ -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;
}

View File

@ -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(

View File

@ -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}`,
},

25
src/utils/remoteImages.ts Normal file
View 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);
};