🔀 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: schedule:
interval: "weekly" interval: "weekly"
target-branch: "dev" 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 - package-ecosystem: "docker" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"
target-branch: "dev" 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", "name": "jellyfin-discord-music-bot",
"version": "0.0.3", "version": "0.0.4",
"description": "", "description": "",
"author": "manuel-rw", "author": "manuel-rw",
"private": true, "private": true,
@ -38,33 +38,33 @@
"joi": "^17.7.0", "joi": "^17.7.0",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^4.1.2",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"ws": "^8.11.0" "ws": "^8.11.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.0.0", "@nestjs/cli": "^9.1.8",
"@nestjs/schematics": "^9.0.0", "@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0", "@nestjs/testing": "^9.0.0",
"@types/cron": "^2.0.0", "@types/cron": "^2.0.0",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "28.1.8", "@types/jest": "28.1.8",
"@types/node": "^18.0.0", "@types/node": "^18.11.18",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.52.0",
"eslint": "^8.0.1", "eslint": "^8.32.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"jest": "28.1.3", "jest": "28.1.3",
"prettier": "^2.3.2", "prettier": "^2.8.4",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.20",
"supertest": "^6.1.3", "supertest": "^6.1.3",
"ts-jest": "28.0.8", "ts-jest": "28.0.8",
"ts-loader": "^9.2.3", "ts-loader": "^9.2.3",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.2",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"jest": { "jest": {

View File

@ -19,7 +19,7 @@ import { UpdatesModule } from './updates/updates.module';
ConfigModule.forRoot({ ConfigModule.forRoot({
validationSchema: Joi.object({ validationSchema: Joi.object({
DISCORD_CLIENT_TOKEN: Joi.string().required(), 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_USERNAME: Joi.string().required(),
JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(), JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(),
UPDATER_DISABLE_NOTIFICATIONS: Joi.boolean(), UPDATER_DISABLE_NOTIFICATIONS: Joi.boolean(),

View File

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

View File

@ -4,7 +4,6 @@ import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({ @Command({
name: 'disconnect', name: 'disconnect',
@ -17,20 +16,28 @@ export class DisconnectCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars async handler(interaction: CommandInteraction): Promise<void> {
handler(interaction: CommandInteraction): GenericCustomReply { await interaction.reply({
embeds: [
this.discordMessageService.buildMessage({
title: 'Disconnecting...',
}),
],
});
const disconnect = this.discordVoiceService.disconnect(); const disconnect = this.discordVoiceService.disconnect();
if (!disconnect.success) { if (!disconnect.success) {
return disconnect.reply; await interaction.editReply(disconnect.reply);
return;
} }
return { await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Disconnected from your channel', 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 { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({ @Command({
name: 'help', name: 'help',
@ -13,9 +12,8 @@ import { GenericCustomReply } from '../models/generic-try-handler';
export class HelpCommand implements DiscordCommand { export class HelpCommand implements DiscordCommand {
constructor(private readonly discordMessageService: DiscordMessageService) {} constructor(private readonly discordMessageService: DiscordMessageService) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars async handler(interaction: CommandInteraction): Promise<void> {
handler(commandInteraction: CommandInteraction): GenericCustomReply { await interaction.reply({
return {
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Jellyfin Discord Bot', 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 { TransformPipe } from '@discord-nestjs/common';
import { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core'; 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 { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
@ -16,26 +16,23 @@ export class SkipTrackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interactionCommand: CommandInteraction,
): InteractionReplyOptions | string {
if (!this.playbackService.nextTrack()) { if (!this.playbackService.nextTrack()) {
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'There is no next track', title: 'There is no next track',
}), }),
], ],
}; });
} }
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Skipped to the next track', title: 'Skipped to the next track',
}), }),
], ],
}; });
} }
} }

View File

@ -16,18 +16,15 @@ export class PausePlaybackCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commandInteraction: CommandInteraction,
): string | InteractionReplyOptions {
const shouldBePaused = this.discordVoiceService.togglePaused(); const shouldBePaused = this.discordVoiceService.togglePaused();
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: shouldBePaused ? 'Paused' : 'Unpaused', 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 { 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 { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service'; import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import { import {
@ -28,9 +29,8 @@ 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/remoteImages';
import { chooseSuitableRemoteImage } from '../utils/remoteImages'; import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
import { trimStringToFixedLength } from '../utils/stringUtils';
@Command({ @Command({
name: 'play', name: 'play',
@ -52,9 +52,10 @@ export class PlayItemCommand
async handler( async handler(
@Payload() dto: TrackRequestDto, @Payload() dto: TrackRequestDto,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
executionContext: TransformedCommandExecutionContext<any>, executionContext: TransformedCommandExecutionContext<any>,
): Promise<InteractionReplyOptions | string> { ): Promise<InteractionReplyOptions | string> {
await executionContext.interaction.deferReply();
const items = await this.jellyfinSearchService.search(dto.search); const items = await this.jellyfinSearchService.search(dto.search);
const parsedItems = await Promise.all( const parsedItems = await Promise.all(
items.map( items.map(
@ -68,14 +69,15 @@ export class PlayItemCommand
); );
if (parsedItems.length === 0) { if (parsedItems.length === 0) {
return { await executionContext.interaction.followUp({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'No results for your search query found', 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`, 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); const firstItems = parsedItems.slice(0, 10);
@ -107,7 +109,7 @@ export class PlayItemCommand
emoji: item.getEmoji(), emoji: item.getEmoji(),
})); }));
return { await executionContext.interaction.followUp({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Jellyfin Search Results', title: 'Jellyfin Search Results',
@ -126,7 +128,7 @@ export class PlayItemCommand
], ],
}, },
], ],
}; });
} }
@On(Events.InteractionCreate) @On(Events.InteractionCreate)
@ -144,6 +146,18 @@ export class PlayItemCommand
return; 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 guildMember = interaction.member as GuildMember;
const tryResult = const tryResult =
@ -156,7 +170,7 @@ export class PlayItemCommand
`Unable to process select result because the member was not in a voice channcel`, `Unable to process select result because the member was not in a voice channcel`,
); );
const replyOptions = tryResult.reply as InteractionReplyOptions; const replyOptions = tryResult.reply as InteractionReplyOptions;
await interaction.update({ await interaction.editReply({
embeds: replyOptions.embeds, embeds: replyOptions.embeds,
content: undefined, content: undefined,
components: [], components: [],
@ -183,7 +197,7 @@ export class PlayItemCommand
bitrate, bitrate,
remoteImagesOfCurrentAlbum, remoteImagesOfCurrentAlbum,
); );
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: item.Name, title: item.Name,
@ -212,7 +226,7 @@ export class PlayItemCommand
remoteImages, remoteImages,
); );
}); });
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: `Added ${album.TotalRecordCount} items from your album`, title: `Added ${album.TotalRecordCount} items from your album`,
@ -247,7 +261,7 @@ export class PlayItemCommand
} }
const bestPlaylistRemoteImage = const bestPlaylistRemoteImage =
chooseSuitableRemoteImage(addedRemoteImages); chooseSuitableRemoteImage(addedRemoteImages);
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: `Added ${playlist.TotalRecordCount} items from your playlist`, title: `Added ${playlist.TotalRecordCount} items from your playlist`,
@ -267,7 +281,7 @@ export class PlayItemCommand
}); });
break; break;
default: default:
interaction.update({ await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildErrorMessage({ this.discordMessageService.buildErrorMessage({
title: 'Unable to process your selection', 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 { Command, DiscordCommand, UsePipes } from '@discord-nestjs/core';
import { CommandInteraction } from 'discord.js'; import { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
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 { chooseSuitableRemoteImageFromTrack } from '../utils/remoteImages/remoteImages';
import { trimStringToFixedLength } from '../utils/stringUtils'; import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils'; import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils';
@Command({ @Command({
@ -21,12 +20,11 @@ export class PlaylistCommand implements DiscordCommand {
private readonly playbackService: PlaybackService, private readonly playbackService: PlaybackService,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars async handler(interaction: CommandInteraction): Promise<void> {
handler(interaction: CommandInteraction): GenericCustomReply {
const playList = this.playbackService.getPlaylist(); const playList = this.playbackService.getPlaylist();
if (playList.tracks.length === 0) { if (playList.tracks.length === 0) {
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Your Playlist', 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', 'You do not have any tracks in your playlist.\nUse the play command to add new tracks to your playlist',
}), }),
], ],
}; });
} }
const tracklist = playList.tracks const tracklist = playList.tracks
@ -63,7 +61,7 @@ export class PlaylistCommand implements DiscordCommand {
const activeTrack = this.playbackService.getActiveTrack(); const activeTrack = this.playbackService.getActiveTrack();
const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track); const remoteImage = chooseSuitableRemoteImageFromTrack(activeTrack.track);
return { await interaction.reply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Your Playlist', title: 'Your Playlist',
@ -77,7 +75,7 @@ export class PlaylistCommand implements DiscordCommand {
}, },
}), }),
], ],
}; });
} }
private getListPoint(isCurrent: boolean, index: number) { private getListPoint(isCurrent: boolean, index: number) {

View File

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

View File

@ -6,12 +6,7 @@ import {
InjectDiscordClient, InjectDiscordClient,
UsePipes, UsePipes,
} from '@discord-nestjs/core'; } from '@discord-nestjs/core';
import { import { Client, CommandInteraction, Status } from 'discord.js';
Client,
CommandInteraction,
InteractionReplyOptions,
Status,
} from 'discord.js';
import { formatDuration, intervalToDuration } from 'date-fns'; import { formatDuration, intervalToDuration } from 'date-fns';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
@ -33,10 +28,15 @@ export class StatusCommand implements DiscordCommand {
private readonly jellyfinService: JellyfinService, private readonly jellyfinService: JellyfinService,
) {} ) {}
async handler( async handler(interaction: CommandInteraction): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars await interaction.reply({
commandInteraction: CommandInteraction, embeds: [
): Promise<string | InteractionReplyOptions> { this.discordMessageService.buildMessage({
title: 'Retrieving status information...',
}),
],
});
const ping = this.client.ws.ping; const ping = this.client.ws.ping;
const status = Status[this.client.ws.status]; const status = Status[this.client.ws.status];
@ -49,7 +49,7 @@ export class StatusCommand implements DiscordCommand {
const jellyfinSystemApi = getSystemApi(this.jellyfinService.getApi()); const jellyfinSystemApi = getSystemApi(this.jellyfinService.getApi());
const jellyfinSystemInformation = await jellyfinSystemApi.getSystemInfo(); const jellyfinSystemInformation = await jellyfinSystemApi.getSystemInfo();
return { await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Discord Bot Status', 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 { CommandInteraction } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
import { PlaybackService } from '../playback/playback.service'; import { PlaybackService } from '../playback/playback.service';
@Command({ @Command({
@ -19,18 +18,28 @@ export class StopPlaybackCommand implements DiscordCommand {
private readonly discordVoiceService: DiscordVoiceService, private readonly discordVoiceService: DiscordVoiceService,
) {} ) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
handler(CommandInteraction: CommandInteraction): GenericCustomReply { async handler(interaction: CommandInteraction): Promise<void> {
this.playbackService.clear(); const hasActiveTrack = this.playbackService.hasActiveTrack();
this.discordVoiceService.stop(false); 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: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService[
title: 'Playlist cleared', hasActiveTrack ? 'buildMessage' : 'buildErrorMessage'
description: ]({
'Playback was stopped and your playlist has been cleared', title: title,
description: description,
}), }),
], ],
}; });
} }
} }

View File

@ -5,7 +5,6 @@ import { Logger } from '@nestjs/common';
import { CommandInteraction, GuildMember } from 'discord.js'; import { CommandInteraction, GuildMember } from 'discord.js';
import { DiscordMessageService } from '../clients/discord/discord.message.service'; import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { DiscordVoiceService } from '../clients/discord/discord.voice.service'; import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
import { GenericCustomReply } from '../models/generic-try-handler';
@Command({ @Command({
name: 'summon', name: 'summon',
@ -20,7 +19,9 @@ export class SummonCommand implements DiscordCommand {
private readonly discordMessageService: DiscordMessageService, private readonly discordMessageService: DiscordMessageService,
) {} ) {}
handler(interaction: CommandInteraction): GenericCustomReply { async handler(interaction: CommandInteraction): Promise<void> {
await interaction.deferReply();
const guildMember = interaction.member as GuildMember; const guildMember = interaction.member as GuildMember;
const tryResult = const tryResult =
@ -29,10 +30,11 @@ export class SummonCommand implements DiscordCommand {
); );
if (!tryResult.success) { if (!tryResult.success) {
return tryResult.reply; await interaction.editReply(tryResult.reply);
return;
} }
return { await interaction.editReply({
embeds: [ embeds: [
this.discordMessageService.buildMessage({ this.discordMessageService.buildMessage({
title: 'Joined your voicehannel', 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.", "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'; } from '@nestjs/terminus';
import { HealthCheckExecutor } from '@nestjs/terminus/dist/health-check/health-check-executor.service'; import { HealthCheckExecutor } from '@nestjs/terminus/dist/health-check/health-check-executor.service';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
import { useDefaultMockerToken } from '../utils/tests'; import { useDefaultMockerToken } from '../utils/tests/defaultMockerToken';
import { HealthController } from './health.controller'; import { HealthController } from './health.controller';
import { DiscordHealthIndicator } from './indicators/discord.indicator'; import { DiscordHealthIndicator } from './indicators/discord.indicator';
import { JellyfinHealthIndicator } from './indicators/jellyfin.indicator'; import { JellyfinHealthIndicator } from './indicators/jellyfin.indicator';

View File

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

View File

@ -1,11 +1,6 @@
import { InteractionReplyOptions } from 'discord.js'; import { InteractionEditReplyOptions, MessagePayload } from 'discord.js';
export interface GenericTryHandler { export interface GenericTryHandler {
success: boolean; 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'; } from '@jellyfin/sdk/lib/generated-client/models';
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service'; import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
import { Track } from '../types/track'; import { Track } from '../types/track';
import { trimStringToFixedLength } from '../utils/stringUtils'; import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils';
import { Logger } from '@nestjs/common'; import { Logger } from '@nestjs/common';
import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service'; 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: { Version: {
Major: 0, Major: 0,
Minor: 0, Minor: 0,
Patch: 3, Patch: 4,
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}`,
}, },

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, RemoteImageInfo,
RemoteImageResult, RemoteImageResult,
} from '@jellyfin/sdk/lib/generated-client/models'; } from '@jellyfin/sdk/lib/generated-client/models';
import { Track } from '../types/track'; import { Track } from '../../types/track';
export const chooseSuitableRemoteImage = ( export const chooseSuitableRemoteImage = (
remoteImageResult: RemoteImageResult, 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'); throw new Error('max length must be positive');
} }
return value.length > maxLength if (value.length <= maxLength) {
? value.substring(0, maxLength - 3) + '...' return value;
: 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 languageName: node
linkType: hard linkType: hard
"@nestjs/cli@npm:^9.0.0": "@nestjs/cli@npm:^9.1.8":
version: 9.1.8 version: 9.1.8
resolution: "@nestjs/cli@npm:9.1.8" resolution: "@nestjs/cli@npm:9.1.8"
dependencies: dependencies:
@ -1598,7 +1598,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:*, @types/node@npm:^18.0.0": "@types/node@npm:*, @types/node@npm:^18.11.18":
version: 18.11.18 version: 18.11.18
resolution: "@types/node@npm:18.11.18" resolution: "@types/node@npm:18.11.18"
checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d
@ -1701,14 +1701,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/eslint-plugin@npm:^5.0.0": "@typescript-eslint/eslint-plugin@npm:^5.51.0":
version: 5.48.1 version: 5.51.0
resolution: "@typescript-eslint/eslint-plugin@npm:5.48.1" resolution: "@typescript-eslint/eslint-plugin@npm:5.51.0"
dependencies: dependencies:
"@typescript-eslint/scope-manager": 5.48.1 "@typescript-eslint/scope-manager": 5.51.0
"@typescript-eslint/type-utils": 5.48.1 "@typescript-eslint/type-utils": 5.51.0
"@typescript-eslint/utils": 5.48.1 "@typescript-eslint/utils": 5.51.0
debug: ^4.3.4 debug: ^4.3.4
grapheme-splitter: ^1.0.4
ignore: ^5.2.0 ignore: ^5.2.0
natural-compare-lite: ^1.4.0 natural-compare-lite: ^1.4.0
regexpp: ^3.2.0 regexpp: ^3.2.0
@ -1720,43 +1721,53 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: d8d73d123d16fc9b50b500ef21816dcabdffe0d2dcfdb15089dc5a1015d57cbad709de565d1c830f5058c0d7b410069e2554c0b53d1485fe7b237ea8089e58be checksum: 5351d8cec13bd9867ce4aaf7052aa31c9ca867fc89c620fc0fe5718ac2cbc165903275db59974324d98e45df0d33a73a4367d236668772912731031a672cfdcd
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/parser@npm:^5.0.0": "@typescript-eslint/parser@npm:^5.52.0":
version: 5.48.1 version: 5.52.0
resolution: "@typescript-eslint/parser@npm:5.48.1" resolution: "@typescript-eslint/parser@npm:5.52.0"
dependencies: dependencies:
"@typescript-eslint/scope-manager": 5.48.1 "@typescript-eslint/scope-manager": 5.52.0
"@typescript-eslint/types": 5.48.1 "@typescript-eslint/types": 5.52.0
"@typescript-eslint/typescript-estree": 5.48.1 "@typescript-eslint/typescript-estree": 5.52.0
debug: ^4.3.4 debug: ^4.3.4
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: c624d24eb209b4ce7f0a6c8116712363f10a9c9a5138f240e254ff265526ee4b0fd73b7b6b4b6a0e7611bd9934c42036350dd27f96ae2fa4efdade1a7ebd0e9e checksum: 1d8ff6e932f9c9db8d24b16ce89fd963f0982c38559e500aa1f8dc5cd66abd02f1659dd1a1361ce550def05331803caa69a69a039b54c94fc0f22919a2305c12
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/scope-manager@npm:5.48.1": "@typescript-eslint/scope-manager@npm:5.51.0":
version: 5.48.1 version: 5.51.0
resolution: "@typescript-eslint/scope-manager@npm:5.48.1" resolution: "@typescript-eslint/scope-manager@npm:5.51.0"
dependencies: dependencies:
"@typescript-eslint/types": 5.48.1 "@typescript-eslint/types": 5.51.0
"@typescript-eslint/visitor-keys": 5.48.1 "@typescript-eslint/visitor-keys": 5.51.0
checksum: f60a7efe917798cccf8652925de6be58b023ded6c6ee44ce74d074f0c2a1927680398a6d73bab33d500c69474ad8c54d63b90fcc6e13256712707d12a60e0a64 checksum: b3c9f48b6b7a7ae2ebcad4745ef91e4727776b2cf56d31be6456b1aa063aa649539e20f9fffa83cad9ccaaa9c492f2354a1c15526a2b789e235ec58b3a82d22c
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/type-utils@npm:5.48.1": "@typescript-eslint/scope-manager@npm:5.52.0":
version: 5.48.1 version: 5.52.0
resolution: "@typescript-eslint/type-utils@npm:5.48.1" resolution: "@typescript-eslint/scope-manager@npm:5.52.0"
dependencies: dependencies:
"@typescript-eslint/typescript-estree": 5.48.1 "@typescript-eslint/types": 5.52.0
"@typescript-eslint/utils": 5.48.1 "@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 debug: ^4.3.4
tsutils: ^3.21.0 tsutils: ^3.21.0
peerDependencies: peerDependencies:
@ -1764,23 +1775,30 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 2739b35caf48c9edbeab82936de58ce0759ab34955ce7eec1786690d6a63146ae0a6c5d9c76034605d9fe200c87a73ede0772c6244c5df6e66df992d9ebbab72 checksum: ab9747b0c629cfaaab903eed8ce1e39d34d69a402ce5faf2f1fff2bbb461bdbe034044b1368ba67ba8e5c1c512172e07d83c8a563635d8de811bf148d95c7dec
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/types@npm:5.48.1": "@typescript-eslint/types@npm:5.51.0":
version: 5.48.1 version: 5.51.0
resolution: "@typescript-eslint/types@npm:5.48.1" resolution: "@typescript-eslint/types@npm:5.51.0"
checksum: 8437986e9d86d792b23327517ae2f9861ec55992d5a9cd55991e525409b6244169436cd708f3987ab7c579e45e59b6eab5a9d3583f7729219e25691164293094 checksum: b31021a0866f41ba5d71b6c4c7e20cc9b99d49c93bb7db63b55b2e51542fb75b4e27662ee86350da3c1318029e278a5a807facaf4cb5aeea724be8b0e021e836
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/typescript-estree@npm:5.48.1": "@typescript-eslint/types@npm:5.52.0":
version: 5.48.1 version: 5.52.0
resolution: "@typescript-eslint/typescript-estree@npm:5.48.1" 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: dependencies:
"@typescript-eslint/types": 5.48.1 "@typescript-eslint/types": 5.51.0
"@typescript-eslint/visitor-keys": 5.48.1 "@typescript-eslint/visitor-keys": 5.51.0
debug: ^4.3.4 debug: ^4.3.4
globby: ^11.1.0 globby: ^11.1.0
is-glob: ^4.0.3 is-glob: ^4.0.3
@ -1789,35 +1807,63 @@ __metadata:
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
checksum: 2b26e5848ef131e1bb99ed54d8c0efa8279cf8e8f7d8b72de00c2ca6cf2799d96c20f5bbbcf26e14e81b7b9d1035ba509bff30f2d852c174815879e8f14c27ed checksum: aec23e5cab48ee72fefa6d1ac266639ebabf6cebec1e0207ad47011d3a48186ac9a632c8e34c3bac896155f54895a497230c11d789fd81263b08eb267d7113ce
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/utils@npm:5.48.1": "@typescript-eslint/typescript-estree@npm:5.52.0":
version: 5.48.1 version: 5.52.0
resolution: "@typescript-eslint/utils@npm:5.48.1" 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: dependencies:
"@types/json-schema": ^7.0.9 "@types/json-schema": ^7.0.9
"@types/semver": ^7.3.12 "@types/semver": ^7.3.12
"@typescript-eslint/scope-manager": 5.48.1 "@typescript-eslint/scope-manager": 5.51.0
"@typescript-eslint/types": 5.48.1 "@typescript-eslint/types": 5.51.0
"@typescript-eslint/typescript-estree": 5.48.1 "@typescript-eslint/typescript-estree": 5.51.0
eslint-scope: ^5.1.1 eslint-scope: ^5.1.1
eslint-utils: ^3.0.0 eslint-utils: ^3.0.0
semver: ^7.3.7 semver: ^7.3.7
peerDependencies: peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
checksum: 2d112cbb6a920f147c6c3322e404ca3c56c1170e1ede3bcbf16fb779960dc24cdba688b1f2d06acd242859fc1dbc8702da5f8fa8bbf53e7081e41d80bec4c236 checksum: c6e28c942fbac5500f0e8ed67ef304b484ba296486e55306f78fb090dc9d5bb1f25a0bedc065e14680041eadce5e95fa10aab618cb0c316599ec987e6ea72442
languageName: node languageName: node
linkType: hard linkType: hard
"@typescript-eslint/visitor-keys@npm:5.48.1": "@typescript-eslint/visitor-keys@npm:5.51.0":
version: 5.48.1 version: 5.51.0
resolution: "@typescript-eslint/visitor-keys@npm:5.48.1" resolution: "@typescript-eslint/visitor-keys@npm:5.51.0"
dependencies: dependencies:
"@typescript-eslint/types": 5.48.1 "@typescript-eslint/types": 5.51.0
eslint-visitor-keys: ^3.3.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 languageName: node
linkType: hard linkType: hard
@ -3317,7 +3363,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint-config-prettier@npm:^8.3.0": "eslint-config-prettier@npm:^8.6.0":
version: 8.6.0 version: 8.6.0
resolution: "eslint-config-prettier@npm:8.6.0" resolution: "eslint-config-prettier@npm:8.6.0"
peerDependencies: peerDependencies:
@ -3388,9 +3434,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"eslint@npm:^8.0.1": "eslint@npm:^8.32.0":
version: 8.31.0 version: 8.32.0
resolution: "eslint@npm:8.31.0" resolution: "eslint@npm:8.32.0"
dependencies: dependencies:
"@eslint/eslintrc": ^1.4.1 "@eslint/eslintrc": ^1.4.1
"@humanwhocodes/config-array": ^0.11.8 "@humanwhocodes/config-array": ^0.11.8
@ -3433,7 +3479,7 @@ __metadata:
text-table: ^0.2.0 text-table: ^0.2.0
bin: bin:
eslint: bin/eslint.js eslint: bin/eslint.js
checksum: 5e5688bb864edc6b12d165849994812eefa67fb3fc44bb26f53659b63edcd8bcc68389d27cc6cc9e5b79ee22f24b6f311fa3ed047bddcafdec7d84c1b5561e4f checksum: 23c8fb3c57291eecd9c1448faf603226a8f885022a2cd96e303459bf72e39b7f54987c6fb948f0f9eecaf7085600e6eb0663482a35ea83da12e9f9141a22b91e
languageName: node languageName: node
linkType: hard linkType: hard
@ -4551,7 +4597,7 @@ __metadata:
"@discordjs/opus": ^0.9.0 "@discordjs/opus": ^0.9.0
"@discordjs/voice": ^0.14.0 "@discordjs/voice": ^0.14.0
"@jellyfin/sdk": ^0.7.0 "@jellyfin/sdk": ^0.7.0
"@nestjs/cli": ^9.0.0 "@nestjs/cli": ^9.1.8
"@nestjs/common": ^9.0.0 "@nestjs/common": ^9.0.0
"@nestjs/config": ^2.2.0 "@nestjs/config": ^2.2.0
"@nestjs/core": ^9.0.0 "@nestjs/core": ^9.0.0
@ -4564,28 +4610,28 @@ __metadata:
"@types/cron": ^2.0.0 "@types/cron": ^2.0.0
"@types/express": ^4.17.13 "@types/express": ^4.17.13
"@types/jest": 28.1.8 "@types/jest": 28.1.8
"@types/node": ^18.0.0 "@types/node": ^18.11.18
"@types/supertest": ^2.0.11 "@types/supertest": ^2.0.11
"@typescript-eslint/eslint-plugin": ^5.0.0 "@typescript-eslint/eslint-plugin": ^5.51.0
"@typescript-eslint/parser": ^5.0.0 "@typescript-eslint/parser": ^5.52.0
date-fns: ^2.29.3 date-fns: ^2.29.3
discord.js: ^14.7.1 discord.js: ^14.7.1
eslint: ^8.0.1 eslint: ^8.32.0
eslint-config-prettier: ^8.3.0 eslint-config-prettier: ^8.6.0
eslint-plugin-prettier: ^4.0.0 eslint-plugin-prettier: ^4.0.0
jest: 28.1.3 jest: 28.1.3
joi: ^17.7.0 joi: ^17.7.0
libsodium-wrappers: ^0.7.10 libsodium-wrappers: ^0.7.10
prettier: ^2.3.2 prettier: ^2.8.4
reflect-metadata: ^0.1.13 reflect-metadata: ^0.1.13
rimraf: ^3.0.2 rimraf: ^4.1.2
rxjs: ^7.2.0 rxjs: ^7.2.0
source-map-support: ^0.5.20 source-map-support: ^0.5.20
supertest: ^6.1.3 supertest: ^6.1.3
ts-jest: 28.0.8 ts-jest: 28.0.8
ts-loader: ^9.2.3 ts-loader: ^9.2.3
ts-node: ^10.0.0 ts-node: ^10.0.0
tsconfig-paths: 4.1.0 tsconfig-paths: 4.1.2
typescript: ^4.7.4 typescript: ^4.7.4
uuid: ^9.0.0 uuid: ^9.0.0
ws: ^8.11.0 ws: ^8.11.0
@ -6082,12 +6128,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier@npm:^2.3.2": "prettier@npm:^2.8.4":
version: 2.8.2 version: 2.8.4
resolution: "prettier@npm:2.8.2" resolution: "prettier@npm:2.8.4"
bin: bin:
prettier: bin-prettier.js prettier: bin-prettier.js
checksum: 740c56c2128d587d656ea1dde9bc9c3503dfc94db4f3ac387259215eeb2e216680bdad9d18a0c9feecc6b42cfa188d6fa777df4c36c1d00cedd4199074fbfbd2 checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e
languageName: node languageName: node
linkType: hard linkType: hard
@ -6408,6 +6454,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "run-async@npm:^2.4.0":
version: 2.4.1 version: 2.4.1
resolution: "run-async@npm:2.4.1" resolution: "run-async@npm:2.4.1"
@ -7145,17 +7200,6 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "tsconfig-paths@npm:4.1.1":
version: 4.1.1 version: 4.1.1
resolution: "tsconfig-paths@npm:4.1.1" resolution: "tsconfig-paths@npm:4.1.1"
@ -7167,7 +7211,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tsconfig-paths@npm:^4.0.0": "tsconfig-paths@npm:4.1.2, tsconfig-paths@npm:^4.0.0":
version: 4.1.2 version: 4.1.2
resolution: "tsconfig-paths@npm:4.1.2" resolution: "tsconfig-paths@npm:4.1.2"
dependencies: dependencies: