🔀 Version 0.0.4

This commit is contained in:
Manuel 2023-02-14 21:59:19 +01:00 committed by GitHub
commit e665ebec21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1021 additions and 481 deletions

View File

@ -10,8 +10,29 @@ updates:
schedule:
interval: "weekly"
target-branch: "dev"
commit-message:
prefix: "⬆️" # prefix with gitmoji
include: "scope" # list updated dependencies in message
assignees:
- "manuel-rw"
- package-ecosystem: "docker" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
target-branch: "dev"
commit-message:
prefix: "🚀" # prefix with gitmoji
include: "scope" # list updated dependencies in message
assignees:
- "manuel-rw"
- package-ecosystem: "github-actions"
directory: /.github
schedule:
interval: "weekly"
target-branch: "dev"
commit-message:
prefix: "👷" # prefix with gitmoji
include: "scope" # list updated dependencies in message
assignees:
- "manuel-rw"

852
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "jellyfin-discord-music-bot",
"version": "0.0.3",
"version": "0.0.4",
"description": "",
"author": "manuel-rw",
"private": true,
@ -38,33 +38,33 @@
"joi": "^17.7.0",
"libsodium-wrappers": "^0.7.10",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rimraf": "^4.1.2",
"rxjs": "^7.2.0",
"uuid": "^9.0.0",
"ws": "^8.11.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/cli": "^9.1.8",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/cron": "^2.0.0",
"@types/express": "^4.17.13",
"@types/jest": "28.1.8",
"@types/node": "^18.0.0",
"@types/node": "^18.11.18",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.52.0",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3",
"prettier": "^2.3.2",
"prettier": "^2.8.4",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "28.0.8",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "4.1.0",
"tsconfig-paths": "4.1.2",
"typescript": "^4.7.4"
},
"jest": {

View File

@ -19,7 +19,7 @@ import { UpdatesModule } from './updates/updates.module';
ConfigModule.forRoot({
validationSchema: Joi.object({
DISCORD_CLIENT_TOKEN: Joi.string().required(),
JELLYFIN_SERVER_ADDRESS: Joi.string().required(),
JELLYFIN_SERVER_ADDRESS: Joi.string().uri().required(),
JELLYFIN_AUTHENTICATION_USERNAME: Joi.string().required(),
JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(),
UPDATER_DISABLE_NOTIFICATIONS: Joi.boolean(),

View File

@ -58,6 +58,10 @@ export class JellyinPlaystateService {
private async onPlaybackPaused(isPaused: boolean) {
const activeTrack = this.playbackService.getActiveTrack();
if (!activeTrack) {
return;
}
await this.playstateApi.reportPlaybackProgress({
playbackProgressInfo: {
ItemId: activeTrack.track.jellyfinId,
@ -69,6 +73,10 @@ export class JellyinPlaystateService {
@OnEvent('playback.state.stop')
private async onPlaybackStopped() {
const activeTrack = this.playbackService.getActiveTrack();
if (!activeTrack) {
return;
}
await this.playstateApi.reportPlaybackStopped({
playbackStopInfo: {

View File

@ -4,7 +4,6 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({
name: 'disconnect',
@ -17,20 +16,28 @@ export class DisconnectCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(interaction: CommandInteraction): GenericCustomReply {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Disconnecting...',
}),
],
});
const disconnect = this.discordVoiceService.disconnect();
if (!disconnect.success) {
return disconnect.reply;
await interaction.editReply(disconnect.reply);
return;
}
return {
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Disconnected from your channel',
}),
],
};
});
}
}

View File

@ -3,7 +3,6 @@ import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({
name: 'help',
@ -13,9 +12,8 @@ import { GenericCustomReply } from '../models/generic-try-handler';
export class HelpCommand implements DiscordCommand {
constructor(private readonly discordMessageService: DiscordMessageService) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(commandInteraction: CommandInteraction): GenericCustomReply {
return {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Jellyfin Discord Bot',
@ -40,6 +38,6 @@ export class HelpCommand implements DiscordCommand {
},
}),
],
};
});
}
}

View File

@ -1,7 +1,7 @@
import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction, InteractionReplyOptions } from 'discord.js';
import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service';
@ -16,26 +16,23 @@ export class SkipTrackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interactionCommand: CommandInteraction,
): InteractionReplyOptions | string {
async handler(interaction: CommandInteraction): Promise<void> {
if (!this.playbackService.nextTrack()) {
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'There is no next track',
}),
],
};
});
}
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Skipped to the next track',
}),
],
};
});
}
}

View File

@ -16,18 +16,15 @@ export class PausePlaybackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commandInteraction: CommandInteraction,
): string | InteractionReplyOptions {
async handler(interaction: CommandInteraction): Promise<void> {
const shouldBePaused = this.discordVoiceService.togglePaused();
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: shouldBePaused ? 'Paused' : 'Unpaused',
}),
],
};
});
}
}

View File

@ -21,6 +21,7 @@ import { TrackRequestDto } from '../models/track-request.dto';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import {
@ -28,9 +29,8 @@ 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';
import { chooseSuitableRemoteImage } from '../utils/remoteImages/remoteImages';
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
@Command({
name: 'play',
@ -52,9 +52,10 @@ export class PlayItemCommand
async handler(
@Payload() dto: TrackRequestDto,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
executionContext: TransformedCommandExecutionContext<any>,
): Promise<InteractionReplyOptions | string> {
await executionContext.interaction.deferReply();
const items = await this.jellyfinSearchService.search(dto.search);
const parsedItems = await Promise.all(
items.map(
@ -68,14 +69,15 @@ export class PlayItemCommand
);
if (parsedItems.length === 0) {
return {
await executionContext.interaction.followUp({
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'No results for your search query found',
description: `I was not able to find any matches for your query \`\`${dto.search}\`\`. Please check that I have access to the desired libraries and that your query is not misspelled`,
}),
],
};
});
return;
}
const firstItems = parsedItems.slice(0, 10);
@ -107,7 +109,7 @@ export class PlayItemCommand
emoji: item.getEmoji(),
}));
return {
await executionContext.interaction.followUp({
embeds: [
this.discordMessageService.buildMessage({
title: 'Jellyfin Search Results',
@ -126,7 +128,7 @@ export class PlayItemCommand
],
},
],
};
});
}
@On(Events.InteractionCreate)
@ -144,6 +146,18 @@ export class PlayItemCommand
return;
}
await interaction.deferUpdate();
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Applying your selection to the queue...',
description: `This may take a moment. Please wait`,
}),
],
components: [],
});
const guildMember = interaction.member as GuildMember;
const tryResult =
@ -156,7 +170,7 @@ export class PlayItemCommand
`Unable to process select result because the member was not in a voice channcel`,
);
const replyOptions = tryResult.reply as InteractionReplyOptions;
await interaction.update({
await interaction.editReply({
embeds: replyOptions.embeds,
content: undefined,
components: [],
@ -183,7 +197,7 @@ export class PlayItemCommand
bitrate,
remoteImagesOfCurrentAlbum,
);
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: item.Name,
@ -212,7 +226,7 @@ export class PlayItemCommand
remoteImages,
);
});
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: `Added ${album.TotalRecordCount} items from your album`,
@ -247,7 +261,7 @@ export class PlayItemCommand
}
const bestPlaylistRemoteImage =
chooseSuitableRemoteImage(addedRemoteImages);
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: `Added ${playlist.TotalRecordCount} items from your playlist`,
@ -267,7 +281,7 @@ export class PlayItemCommand
});
break;
default:
interaction.update({
await interaction.editReply({
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'Unable to process your selection',

View File

@ -3,11 +3,10 @@ import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
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 { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages/remoteImages';
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
@Command({
@ -21,12 +20,11 @@ export class PlaylistCommand implements DiscordCommand {
private readonly playbackService: PlaybackService,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(interaction: CommandInteraction): GenericCustomReply {
async handler(interaction: CommandInteraction): Promise<void> {
const playList = this.playbackService.getPlaylist();
if (playList.tracks.length === 0) {
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Your Playlist',
@ -34,7 +32,7 @@ export class PlaylistCommand implements DiscordCommand {
'You do not have any tracks in your playlist.\nUse the play command to add new tracks to your playlist',
}),
],
};
});
}
const tracklist = playList.tracks
@ -63,7 +61,7 @@ export class PlaylistCommand implements DiscordCommand {
const activeTrack = this.playbackService.getActiveTrack();
const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track);
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Your Playlist',
@ -77,7 +75,7 @@ export class PlaylistCommand implements DiscordCommand {
},
}),
],
};
});
}
private getListPoint(isCurrent: boolean, index: number) {

View File

@ -1,7 +1,7 @@
import { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction, InteractionReplyOptions } from 'discord.js';
import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service';
@ -16,26 +16,23 @@ export class PreviousTrackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
dcommandInteraction: CommandInteraction,
): InteractionReplyOptions | string {
async handler(interaction: CommandInteraction): Promise<void> {
if (!this.playbackService.previousTrack()) {
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildErrorMessage({
title: 'There is no previous track',
}),
],
};
});
}
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Went to previous track',
}),
],
};
});
}
}

View File

@ -6,12 +6,7 @@ import {
InjectDiscordClient,
UsePipes,
} from '@discord-nestjs/core';
import {
Client,
CommandInteraction,
InteractionReplyOptions,
Status,
} from 'discord.js';
import { Client, CommandInteraction, Status } from 'discord.js';
import { formatDuration, intervalToDuration } from 'date-fns';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
@ -33,10 +28,15 @@ export class StatusCommand implements DiscordCommand {
private readonly jellyfinService: JellyfinService,
) {}
async handler(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commandInteraction: CommandInteraction,
): Promise<string | InteractionReplyOptions> {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Retrieving status information...',
}),
],
});
const ping = this.client.ws.ping;
const status = Status[this.client.ws.status];
@ -49,7 +49,7 @@ export class StatusCommand implements DiscordCommand {
const jellyfinSystemApi = getSystemApi(this.jellyfinService.getApi());
const jellyfinSystemInformation = await jellyfinSystemApi.getSystemInfo();
return {
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Discord Bot Status',
@ -90,6 +90,6 @@ export class StatusCommand implements DiscordCommand {
},
}),
],
};
});
}
}

View File

@ -4,7 +4,6 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service';
@Command({
@ -19,18 +18,28 @@ export class StopPlaybackCommand implements DiscordCommand {
private readonly discordVoiceService: DiscordVoiceService,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(CommandInteraction: CommandInteraction): GenericCustomReply {
this.playbackService.clear();
this.discordVoiceService.stop(false);
async handler(interaction: CommandInteraction): Promise<void> {
const hasActiveTrack = this.playbackService.hasActiveTrack();
const title = hasActiveTrack
? 'Playback stopped successfully'
: 'Playback failed to stop';
const description = hasActiveTrack
? 'In addition, your playlist has been cleared'
: 'There is no active track in the queue';
if (hasActiveTrack) {
this.playbackService.clear();
this.discordVoiceService.stop(false);
}
return {
await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Playlist cleared',
description:
'Playback was stopped and your playlist has been cleared',
this.discordMessageService[
hasActiveTrack ? 'buildMessage' : 'buildErrorMessage'
]({
title: title,
description: description,
}),
],
};
});
}
}

View File

@ -5,7 +5,6 @@ import { Logger } from '@nestjs/common';
import { CommandInteraction, GuildMember } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({
name: 'summon',
@ -20,7 +19,9 @@ export class SummonCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService,
) {}
handler(interaction: CommandInteraction): GenericCustomReply {
async handler(interaction: CommandInteraction): Promise<void> {
await interaction.deferReply();
const guildMember = interaction.member as GuildMember;
const tryResult =
@ -29,10 +30,11 @@ export class SummonCommand implements DiscordCommand {
);
if (!tryResult.success) {
return tryResult.reply;
await interaction.editReply(tryResult.reply);
return;
}
return {
await interaction.editReply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Joined your voicehannel',
@ -40,6 +42,6 @@ export class SummonCommand implements DiscordCommand {
"I'm ready to play media. Use ``Cast to device`` in Jellyfin or the ``/play`` command to get started.",
}),
],
};
});
}
}

View File

@ -5,7 +5,7 @@ import {
} from '@nestjs/terminus';
import { HealthCheckExecutor } from '@nestjs/terminus/dist/health-check/health-check-executor.service';
import { Test } from '@nestjs/testing';
import { useDefaultMockerToken } from '../utils/tests';
import { useDefaultMockerToken } from '../utils/tests/defaultMockerToken';
import { HealthController } from './health.controller';
import { DiscordHealthIndicator } from './indicators/discord.indicator';
import { JellyfinHealthIndicator } from './indicators/jellyfin.indicator';

View File

@ -1,7 +1,7 @@
import { HealthIndicatorResult } from '@nestjs/terminus';
import { Test } from '@nestjs/testing';
import { JellyfinService } from '../../clients/jellyfin/jellyfin.service';
import { useDefaultMockerToken } from '../../utils/tests';
import { useDefaultMockerToken } from '../../utils/tests/defaultMockerToken';
import { JellyfinHealthIndicator } from './jellyfin.indicator';
describe('JellyfinHealthIndicator', () => {

View File

@ -1,11 +1,6 @@
import { InteractionReplyOptions } from 'discord.js';
import { InteractionEditReplyOptions, MessagePayload } from 'discord.js';
export interface GenericTryHandler {
success: boolean;
reply: GenericCustomReply;
reply: string | MessagePayload | InteractionEditReplyOptions;
}
export type GenericCustomReply =
| string
| InteractionReplyOptions
| Promise<string | InteractionReplyOptions>;

View File

@ -4,7 +4,7 @@ import {
} from '@jellyfin/sdk/lib/generated-client/models';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import { Track } from '../types/track';
import { trimStringToFixedLength } from '../utils/stringUtils';
import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
import { Logger } from '@nestjs/common';
import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service';

View File

@ -0,0 +1,110 @@
import { Test } from '@nestjs/testing';
import axios from 'axios';
import { Client, GuildMember } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GithubRelease } from '../models/github-release';
import { useDefaultMockerToken } from '../utils/tests/defaultMockerToken';
import { UpdatesService } from './updates.service';
// mock axios: https://stackoverflow.com/questions/51275434/type-of-axios-mock-using-jest-typescript/55351900#55351900
jest.mock('axios');
const mockedAxios = axios as jest.MockedFunction<typeof axios>;
describe('UpdatesService', () => {
const OLD_ENV = process.env;
let updatesService: UpdatesService;
let discordClient: Client;
let discordMessageService: DiscordMessageService;
beforeEach(async () => {
jest.resetModules();
process.env = { ...OLD_ENV };
const moduleRef = await Test.createTestingModule({
providers: [UpdatesService],
})
.useMocker((token) => {
if (token === DiscordMessageService) {
return {
client: jest.fn().mockReturnValue({}),
buildMessage: jest.fn(),
buildErrorMessage: jest.fn(),
} as DiscordMessageService;
}
if (token === Client || token == '__inject_discord_client__') {
return {
guilds: {
cache: [
{
fetchOwner: () =>
({
send: jest.fn(),
user: { tag: 'test' },
} as unknown as GuildMember),
},
],
},
};
}
return useDefaultMockerToken(token);
})
.compile();
updatesService = moduleRef.get<UpdatesService>(UpdatesService);
discordClient = moduleRef.get<Client>('__inject_discord_client__');
discordMessageService = moduleRef.get<DiscordMessageService>(
DiscordMessageService,
);
});
afterAll(() => {
process.env = OLD_ENV;
});
it('handleCronShouldNotNotifyWhenDisabledViaEnvironmentVariable', async () => {
process.env.UPDATER_DISABLE_NOTIFICATIONS = 'true';
mockedAxios.mockResolvedValue({
data: {
html_url: 'https://github.com',
name: 'testing release',
tag_name: '0.0.6',
published_at: '2023-01-09T22:11:25Z',
} as GithubRelease,
status: 200,
statusText: 'Ok',
headers: {},
config: {},
});
await updatesService.handleCron();
expect(mockedAxios).not.toHaveBeenCalled();
expect(discordMessageService.buildMessage).not.toHaveBeenCalled();
expect(discordMessageService.buildErrorMessage).not.toHaveBeenCalled();
});
it('handleCronShouldNotifyWhenNewRelease', async () => {
process.env.UPDATER_DISABLE_NOTIFICATIONS = 'false';
mockedAxios.mockResolvedValue({
data: {
html_url: 'https://github.com',
name: 'testing release',
tag_name: '0.0.6',
published_at: '2023-01-09T22:11:25Z',
} as GithubRelease,
status: 200,
statusText: 'Ok',
headers: {},
config: {},
});
await updatesService.handleCron();
expect(mockedAxios).toHaveBeenCalled();
expect(discordMessageService.buildMessage).toHaveBeenCalled();
});
});

View File

@ -3,7 +3,7 @@ export const Constants = {
Version: {
Major: 0,
Minor: 0,
Patch: 3,
Patch: 4,
All: () =>
`${Constants.Metadata.Version.Major}.${Constants.Metadata.Version.Minor}.${Constants.Metadata.Version.Patch}`,
},

View File

@ -0,0 +1,29 @@
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models';
import { chooseSuitableRemoteImageFromTrack } from './remoteImages';
describe('remoteImages', () => {
it('chooseSuitableRemoteImageFromTrack', () => {
const remoteImage = chooseSuitableRemoteImageFromTrack({
name: 'Testing Music',
durationInMilliseconds: 6969,
jellyfinId: '7384783',
remoteImages: {
Images: [
{
Type: ImageType.Primary,
Url: 'nice picture.png',
},
{
Type: ImageType.Screenshot,
Url: 'not nice picture',
},
],
},
streamUrl: 'http://jellyfin/example-stream',
});
expect(remoteImage).not.toBeNull();
expect(remoteImage.Type).toBe(ImageType.Primary);
expect(remoteImage.Url).toBe('nice picture.png');
});
});

View File

@ -3,7 +3,7 @@ import {
RemoteImageInfo,
RemoteImageResult,
} from '@jellyfin/sdk/lib/generated-client/models';
import { Track } from '../types/track';
import { Track } from '../../types/track';
export const chooseSuitableRemoteImage = (
remoteImageResult: RemoteImageResult,

View File

@ -0,0 +1,23 @@
import { trimStringToFixedLength } from './stringUtils';
describe('stringUtils', () => {
it('trimStringToFixedLengthShouldNotTrim', () => {
const trimmedString = trimStringToFixedLength('test', 20);
expect(trimmedString).toBe('test');
});
it('trimStringToFixedLengthShouldThrowError', () => {
const action = () => {
trimStringToFixedLength('testing value', 0);
};
expect(action).toThrow(Error);
});
it('trimStringToFixedLengthShouldTrimWhenLengthExceeded', () => {
const trimmedString = trimStringToFixedLength('hello world', 5);
expect(trimmedString).toBe('he...');
});
});

View File

@ -3,7 +3,11 @@ export const trimStringToFixedLength = (value: string, maxLength: number) => {
throw new Error('max length must be positive');
}
return value.length > maxLength
? value.substring(0, maxLength - 3) + '...'
: value;
if (value.length <= maxLength) {
return value;
}
const upperBound = maxLength - 3;
return value.substring(0, upperBound) + '...';
};

View File

@ -0,0 +1,17 @@
import { useDefaultMockerToken } from './defaultMockerToken';
describe('defaultMockerToken', () => {
it('useDefaultMockerTokenShouldbeNull', () => {
const mockerToken = useDefaultMockerToken('test');
expect(mockerToken).toBeNull();
});
it('useDefaultMockerTokenShouldReturnNull', () => {
const mockerToken = useDefaultMockerToken(() => ({
test: () => jest.fn(),
}));
expect(mockerToken).not.toBeNull();
});
});

204
yarn.lock
View File

@ -978,7 +978,7 @@ __metadata:
languageName: node
linkType: hard
"@nestjs/cli@npm:^9.0.0":
"@nestjs/cli@npm:^9.1.8":
version: 9.1.8
resolution: "@nestjs/cli@npm:9.1.8"
dependencies:
@ -1598,7 +1598,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:^18.0.0":
"@types/node@npm:*, @types/node@npm:^18.11.18":
version: 18.11.18
resolution: "@types/node@npm:18.11.18"
checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d
@ -1701,14 +1701,15 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:^5.0.0":
version: 5.48.1
resolution: "@typescript-eslint/eslint-plugin@npm:5.48.1"
"@typescript-eslint/eslint-plugin@npm:^5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/eslint-plugin@npm:5.51.0"
dependencies:
"@typescript-eslint/scope-manager": 5.48.1
"@typescript-eslint/type-utils": 5.48.1
"@typescript-eslint/utils": 5.48.1
"@typescript-eslint/scope-manager": 5.51.0
"@typescript-eslint/type-utils": 5.51.0
"@typescript-eslint/utils": 5.51.0
debug: ^4.3.4
grapheme-splitter: ^1.0.4
ignore: ^5.2.0
natural-compare-lite: ^1.4.0
regexpp: ^3.2.0
@ -1720,43 +1721,53 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: d8d73d123d16fc9b50b500ef21816dcabdffe0d2dcfdb15089dc5a1015d57cbad709de565d1c830f5058c0d7b410069e2554c0b53d1485fe7b237ea8089e58be
checksum: 5351d8cec13bd9867ce4aaf7052aa31c9ca867fc89c620fc0fe5718ac2cbc165903275db59974324d98e45df0d33a73a4367d236668772912731031a672cfdcd
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:^5.0.0":
version: 5.48.1
resolution: "@typescript-eslint/parser@npm:5.48.1"
"@typescript-eslint/parser@npm:^5.52.0":
version: 5.52.0
resolution: "@typescript-eslint/parser@npm:5.52.0"
dependencies:
"@typescript-eslint/scope-manager": 5.48.1
"@typescript-eslint/types": 5.48.1
"@typescript-eslint/typescript-estree": 5.48.1
"@typescript-eslint/scope-manager": 5.52.0
"@typescript-eslint/types": 5.52.0
"@typescript-eslint/typescript-estree": 5.52.0
debug: ^4.3.4
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
typescript:
optional: true
checksum: c624d24eb209b4ce7f0a6c8116712363f10a9c9a5138f240e254ff265526ee4b0fd73b7b6b4b6a0e7611bd9934c42036350dd27f96ae2fa4efdade1a7ebd0e9e
checksum: 1d8ff6e932f9c9db8d24b16ce89fd963f0982c38559e500aa1f8dc5cd66abd02f1659dd1a1361ce550def05331803caa69a69a039b54c94fc0f22919a2305c12
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:5.48.1":
version: 5.48.1
resolution: "@typescript-eslint/scope-manager@npm:5.48.1"
"@typescript-eslint/scope-manager@npm:5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/scope-manager@npm:5.51.0"
dependencies:
"@typescript-eslint/types": 5.48.1
"@typescript-eslint/visitor-keys": 5.48.1
checksum: f60a7efe917798cccf8652925de6be58b023ded6c6ee44ce74d074f0c2a1927680398a6d73bab33d500c69474ad8c54d63b90fcc6e13256712707d12a60e0a64
"@typescript-eslint/types": 5.51.0
"@typescript-eslint/visitor-keys": 5.51.0
checksum: b3c9f48b6b7a7ae2ebcad4745ef91e4727776b2cf56d31be6456b1aa063aa649539e20f9fffa83cad9ccaaa9c492f2354a1c15526a2b789e235ec58b3a82d22c
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:5.48.1":
version: 5.48.1
resolution: "@typescript-eslint/type-utils@npm:5.48.1"
"@typescript-eslint/scope-manager@npm:5.52.0":
version: 5.52.0
resolution: "@typescript-eslint/scope-manager@npm:5.52.0"
dependencies:
"@typescript-eslint/typescript-estree": 5.48.1
"@typescript-eslint/utils": 5.48.1
"@typescript-eslint/types": 5.52.0
"@typescript-eslint/visitor-keys": 5.52.0
checksum: 9a03fe30f8e90a5106c482478f213eefdd09f2f74e24d9dc59b453885466a758fe6d1cd24d706aed6188fb03c84b16ca6491cf20da6b16b8fc53cad8b8c327f2
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/type-utils@npm:5.51.0"
dependencies:
"@typescript-eslint/typescript-estree": 5.51.0
"@typescript-eslint/utils": 5.51.0
debug: ^4.3.4
tsutils: ^3.21.0
peerDependencies:
@ -1764,23 +1775,30 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 2739b35caf48c9edbeab82936de58ce0759ab34955ce7eec1786690d6a63146ae0a6c5d9c76034605d9fe200c87a73ede0772c6244c5df6e66df992d9ebbab72
checksum: ab9747b0c629cfaaab903eed8ce1e39d34d69a402ce5faf2f1fff2bbb461bdbe034044b1368ba67ba8e5c1c512172e07d83c8a563635d8de811bf148d95c7dec
languageName: node
linkType: hard
"@typescript-eslint/types@npm:5.48.1":
version: 5.48.1
resolution: "@typescript-eslint/types@npm:5.48.1"
checksum: 8437986e9d86d792b23327517ae2f9861ec55992d5a9cd55991e525409b6244169436cd708f3987ab7c579e45e59b6eab5a9d3583f7729219e25691164293094
"@typescript-eslint/types@npm:5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/types@npm:5.51.0"
checksum: b31021a0866f41ba5d71b6c4c7e20cc9b99d49c93bb7db63b55b2e51542fb75b4e27662ee86350da3c1318029e278a5a807facaf4cb5aeea724be8b0e021e836
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:5.48.1":
version: 5.48.1
resolution: "@typescript-eslint/typescript-estree@npm:5.48.1"
"@typescript-eslint/types@npm:5.52.0":
version: 5.52.0
resolution: "@typescript-eslint/types@npm:5.52.0"
checksum: 018940d61aebf7cf3f7de1b9957446e2ea01f08fe950bef4788c716a3a88f7c42765fe7d80152b0d0428fcd4bd3ace2dfa8c459ba1c59d9a84e951642180f869
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/typescript-estree@npm:5.51.0"
dependencies:
"@typescript-eslint/types": 5.48.1
"@typescript-eslint/visitor-keys": 5.48.1
"@typescript-eslint/types": 5.51.0
"@typescript-eslint/visitor-keys": 5.51.0
debug: ^4.3.4
globby: ^11.1.0
is-glob: ^4.0.3
@ -1789,35 +1807,63 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 2b26e5848ef131e1bb99ed54d8c0efa8279cf8e8f7d8b72de00c2ca6cf2799d96c20f5bbbcf26e14e81b7b9d1035ba509bff30f2d852c174815879e8f14c27ed
checksum: aec23e5cab48ee72fefa6d1ac266639ebabf6cebec1e0207ad47011d3a48186ac9a632c8e34c3bac896155f54895a497230c11d789fd81263b08eb267d7113ce
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:5.48.1":
version: 5.48.1
resolution: "@typescript-eslint/utils@npm:5.48.1"
"@typescript-eslint/typescript-estree@npm:5.52.0":
version: 5.52.0
resolution: "@typescript-eslint/typescript-estree@npm:5.52.0"
dependencies:
"@typescript-eslint/types": 5.52.0
"@typescript-eslint/visitor-keys": 5.52.0
debug: ^4.3.4
globby: ^11.1.0
is-glob: ^4.0.3
semver: ^7.3.7
tsutils: ^3.21.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 67d396907fee3d6894e26411a5098a37f07e5d50343189e6361ff7db91c74a7ffe2abd630d11f14c2bda1f4af13edf52b80b11cbccb55b44079c7cec14c9e108
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/utils@npm:5.51.0"
dependencies:
"@types/json-schema": ^7.0.9
"@types/semver": ^7.3.12
"@typescript-eslint/scope-manager": 5.48.1
"@typescript-eslint/types": 5.48.1
"@typescript-eslint/typescript-estree": 5.48.1
"@typescript-eslint/scope-manager": 5.51.0
"@typescript-eslint/types": 5.51.0
"@typescript-eslint/typescript-estree": 5.51.0
eslint-scope: ^5.1.1
eslint-utils: ^3.0.0
semver: ^7.3.7
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
checksum: 2d112cbb6a920f147c6c3322e404ca3c56c1170e1ede3bcbf16fb779960dc24cdba688b1f2d06acd242859fc1dbc8702da5f8fa8bbf53e7081e41d80bec4c236
checksum: c6e28c942fbac5500f0e8ed67ef304b484ba296486e55306f78fb090dc9d5bb1f25a0bedc065e14680041eadce5e95fa10aab618cb0c316599ec987e6ea72442
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:5.48.1":
version: 5.48.1
resolution: "@typescript-eslint/visitor-keys@npm:5.48.1"
"@typescript-eslint/visitor-keys@npm:5.51.0":
version: 5.51.0
resolution: "@typescript-eslint/visitor-keys@npm:5.51.0"
dependencies:
"@typescript-eslint/types": 5.48.1
"@typescript-eslint/types": 5.51.0
eslint-visitor-keys: ^3.3.0
checksum: 2bda10cf4e6bc48b0d463767617e48a832d708b9434665dff6ed101f7d33e0d592f02af17a2259bde1bd17e666246448ae78d0fe006148cb93d897fff9b1d134
checksum: b49710f3c6b3b62a846a163afffd81be5eb2b1f44e25bec51ff3c9f4c3b579d74aa4cbd3753b4fc09ea3dbc64a7062f9c658c08d22bb2740a599cb703d876220
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:5.52.0":
version: 5.52.0
resolution: "@typescript-eslint/visitor-keys@npm:5.52.0"
dependencies:
"@typescript-eslint/types": 5.52.0
eslint-visitor-keys: ^3.3.0
checksum: 33b44f0cd35b7b47f34e89d52e47b8d8200f55af306b22db4de104d79f65907458ea022e548f50d966e32fea150432ac9c1ae65b3001b0ad2ac8a17c0211f370
languageName: node
linkType: hard
@ -3317,7 +3363,7 @@ __metadata:
languageName: node
linkType: hard
"eslint-config-prettier@npm:^8.3.0":
"eslint-config-prettier@npm:^8.6.0":
version: 8.6.0
resolution: "eslint-config-prettier@npm:8.6.0"
peerDependencies:
@ -3388,9 +3434,9 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:^8.0.1":
version: 8.31.0
resolution: "eslint@npm:8.31.0"
"eslint@npm:^8.32.0":
version: 8.32.0
resolution: "eslint@npm:8.32.0"
dependencies:
"@eslint/eslintrc": ^1.4.1
"@humanwhocodes/config-array": ^0.11.8
@ -3433,7 +3479,7 @@ __metadata:
text-table: ^0.2.0
bin:
eslint: bin/eslint.js
checksum: 5e5688bb864edc6b12d165849994812eefa67fb3fc44bb26f53659b63edcd8bcc68389d27cc6cc9e5b79ee22f24b6f311fa3ed047bddcafdec7d84c1b5561e4f
checksum: 23c8fb3c57291eecd9c1448faf603226a8f885022a2cd96e303459bf72e39b7f54987c6fb948f0f9eecaf7085600e6eb0663482a35ea83da12e9f9141a22b91e
languageName: node
linkType: hard
@ -4551,7 +4597,7 @@ __metadata:
"@discordjs/opus": ^0.9.0
"@discordjs/voice": ^0.14.0
"@jellyfin/sdk": ^0.7.0
"@nestjs/cli": ^9.0.0
"@nestjs/cli": ^9.1.8
"@nestjs/common": ^9.0.0
"@nestjs/config": ^2.2.0
"@nestjs/core": ^9.0.0
@ -4564,28 +4610,28 @@ __metadata:
"@types/cron": ^2.0.0
"@types/express": ^4.17.13
"@types/jest": 28.1.8
"@types/node": ^18.0.0
"@types/node": ^18.11.18
"@types/supertest": ^2.0.11
"@typescript-eslint/eslint-plugin": ^5.0.0
"@typescript-eslint/parser": ^5.0.0
"@typescript-eslint/eslint-plugin": ^5.51.0
"@typescript-eslint/parser": ^5.52.0
date-fns: ^2.29.3
discord.js: ^14.7.1
eslint: ^8.0.1
eslint-config-prettier: ^8.3.0
eslint: ^8.32.0
eslint-config-prettier: ^8.6.0
eslint-plugin-prettier: ^4.0.0
jest: 28.1.3
joi: ^17.7.0
libsodium-wrappers: ^0.7.10
prettier: ^2.3.2
prettier: ^2.8.4
reflect-metadata: ^0.1.13
rimraf: ^3.0.2
rimraf: ^4.1.2
rxjs: ^7.2.0
source-map-support: ^0.5.20
supertest: ^6.1.3
ts-jest: 28.0.8
ts-loader: ^9.2.3
ts-node: ^10.0.0
tsconfig-paths: 4.1.0
tsconfig-paths: 4.1.2
typescript: ^4.7.4
uuid: ^9.0.0
ws: ^8.11.0
@ -6082,12 +6128,12 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^2.3.2":
version: 2.8.2
resolution: "prettier@npm:2.8.2"
"prettier@npm:^2.8.4":
version: 2.8.4
resolution: "prettier@npm:2.8.4"
bin:
prettier: bin-prettier.js
checksum: 740c56c2128d587d656ea1dde9bc9c3503dfc94db4f3ac387259215eeb2e216680bdad9d18a0c9feecc6b42cfa188d6fa777df4c36c1d00cedd4199074fbfbd2
checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e
languageName: node
linkType: hard
@ -6408,6 +6454,15 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^4.1.2":
version: 4.1.2
resolution: "rimraf@npm:4.1.2"
bin:
rimraf: dist/cjs/src/bin.js
checksum: 480b8147fd9bcbef3ac118f88a7b1169c3872977a3411a0c84df838bfc30e175a394c0db6f9619fc8b8a886a18c6d779d5e74f380a0075ecc710afaf81b3f50c
languageName: node
linkType: hard
"run-async@npm:^2.4.0":
version: 2.4.1
resolution: "run-async@npm:2.4.1"
@ -7145,17 +7200,6 @@ __metadata:
languageName: node
linkType: hard
"tsconfig-paths@npm:4.1.0":
version: 4.1.0
resolution: "tsconfig-paths@npm:4.1.0"
dependencies:
json5: ^2.2.1
minimist: ^1.2.6
strip-bom: ^3.0.0
checksum: e4b101f81b2abd95499d8145e0aa73144e857c2c359191058486cef101b7accae22a69114e5d5814a13d5ab3b0bae70dd0c85bcdb7e829bbe1bfda5c9067c9b1
languageName: node
linkType: hard
"tsconfig-paths@npm:4.1.1":
version: 4.1.1
resolution: "tsconfig-paths@npm:4.1.1"
@ -7167,7 +7211,7 @@ __metadata:
languageName: node
linkType: hard
"tsconfig-paths@npm:^4.0.0":
"tsconfig-paths@npm:4.1.2, tsconfig-paths@npm:^4.0.0":
version: 4.1.2
resolution: "tsconfig-paths@npm:4.1.2"
dependencies: