🔀 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 ## ⛔ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import { Constants } from '../utils/constants';
@Injectable() @Injectable()
export class UpdatesService { export class UpdatesService {
private readonly logger = new Logger(UpdatesService.name); private readonly logger = new Logger(UpdatesService.name);
private hasAlreadyNotified: boolean;
constructor( constructor(
@InjectDiscordClient() private readonly client: Client, @InjectDiscordClient() private readonly client: Client,
@ -22,7 +23,7 @@ export class UpdatesService {
async handleCron() { async handleCron() {
const isDisabled = process.env.UPDATER_DISABLE_NOTIFICATIONS; const isDisabled = process.env.UPDATER_DISABLE_NOTIFICATIONS;
if (isDisabled === 'true') { if (isDisabled === 'true' || this.hasAlreadyNotified) {
return; return;
} }
@ -36,6 +37,8 @@ export class UpdatesService {
} }
await this.contactOwnerAboutUpdate(currentVersion, latestGitHubRelease); await this.contactOwnerAboutUpdate(currentVersion, latestGitHubRelease);
this.hasAlreadyNotified = true;
} }
private async contactOwnerAboutUpdate( private async contactOwnerAboutUpdate(

View File

@ -3,7 +3,7 @@ export const Constants = {
Version: { Version: {
Major: 0, Major: 0,
Minor: 0, Minor: 0,
Patch: 1, Patch: 2,
All: () => All: () =>
`${Constants.Metadata.Version.Major}.${Constants.Metadata.Version.Minor}.${Constants.Metadata.Version.Patch}`, `${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);
};