Queue & Search commands
General improvements to everything else
This commit is contained in:
parent
699ba21b88
commit
2e291c514b
1
@types/DiscordClient.d.ts
vendored
1
@types/DiscordClient.d.ts
vendored
@ -35,6 +35,7 @@ export type CommandDefinition = {
|
||||
guildOnly?: boolean,
|
||||
help?: string,
|
||||
limited?: Snowflake[],
|
||||
showUsage?: boolean
|
||||
} & Omit<ComponentOptions, 'type'>
|
||||
|
||||
export type ObserverOptions = {
|
||||
|
15
@types/MusicPlayer.d.ts
vendored
15
@types/MusicPlayer.d.ts
vendored
@ -15,4 +15,19 @@ export type MusicIndexEntry = {
|
||||
album?: string,
|
||||
year?: number,
|
||||
file: string,
|
||||
stats: {
|
||||
plays: number,
|
||||
skips: number
|
||||
}
|
||||
}
|
||||
|
||||
export type MusicQuery = {
|
||||
title?: string
|
||||
artist?: string,
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export type QueueOrder = {
|
||||
artist?: string,
|
||||
title: string
|
||||
}
|
@ -22,11 +22,13 @@
|
||||
"@navy.gif/logger": "^2.5.4",
|
||||
"@navy.gif/timestring": "^6.0.6",
|
||||
"bufferutil": "^4.0.8",
|
||||
"common-tags": "^1.8.2",
|
||||
"discord.js": "^14.14.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"ffmpeg": "^0.0.4",
|
||||
"humanize-duration": "^3.31.0",
|
||||
"music-metadata": "^7.14.0",
|
||||
"similarity": "^1.2.1",
|
||||
"sodium-native": "^4.1.1",
|
||||
"typescript": "^5.4.3",
|
||||
"utf-8-validate": "^6.0.3",
|
||||
@ -38,8 +40,10 @@
|
||||
"@babel/preset-typescript": "^7.24.1",
|
||||
"@types/babel__core": "^7",
|
||||
"@types/babel__preset-env": "^7",
|
||||
"@types/common-tags": "^1",
|
||||
"@types/eslint": "^8",
|
||||
"@types/humanize-duration": "^3",
|
||||
"@types/similarity": "^1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||
"@typescript-eslint/parser": "^7.3.1",
|
||||
"eslint": "^8.57.0"
|
||||
|
@ -7,8 +7,8 @@ import Util from '../../utilities/Util.js';
|
||||
import Initialisable from '../../interfaces/Initialisable.js';
|
||||
import DiscordClient from '../DiscordClient.js';
|
||||
import { Collection } from 'discord.js';
|
||||
import { MusicIndexEntry } from '../../../@types/MusicPlayer.js';
|
||||
|
||||
import { MusicIndexEntry, MusicQuery } from '../../../@types/MusicPlayer.js';
|
||||
import similarity from 'similarity';
|
||||
|
||||
class MusicLibrary implements Initialisable
|
||||
{
|
||||
@ -47,7 +47,43 @@ class MusicLibrary implements Initialisable
|
||||
|
||||
stop (): void | Promise<void>
|
||||
{
|
||||
throw new Error('Method not implemented.');
|
||||
this.saveIndex();
|
||||
}
|
||||
|
||||
search (query: MusicQuery)
|
||||
{
|
||||
if (!Object.keys(query).length)
|
||||
throw new Error('Invalid query');
|
||||
const results: MusicIndexEntry[] = [];
|
||||
for (const entry of this.#index.values())
|
||||
{
|
||||
if (query.artist && !entry.arist.toLowerCase().includes(query.artist.toLowerCase()))
|
||||
continue;
|
||||
if (query.title && !entry.title.toLowerCase().includes(query.title.toLowerCase()))
|
||||
continue;
|
||||
if (query.keyword
|
||||
&& !entry.title.toLowerCase().includes(query.keyword?.toLowerCase())
|
||||
&& !entry.arist.toLowerCase().includes(query.keyword.toLowerCase()))
|
||||
continue;
|
||||
results.push(entry);
|
||||
}
|
||||
return results.sort((a, b) =>
|
||||
{
|
||||
if (query.artist)
|
||||
{
|
||||
if (similarity(a.arist, query.artist) > similarity(b.arist, query.artist))
|
||||
return 1;
|
||||
return -1;
|
||||
}
|
||||
if (query.title)
|
||||
{
|
||||
if (similarity(a.title, query.title) > similarity(b.title, query.title))
|
||||
return 1;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
getRandom ()
|
||||
@ -61,6 +97,18 @@ class MusicLibrary implements Initialisable
|
||||
return Util.shuffle(entries);
|
||||
}
|
||||
|
||||
countPlay (fp: string)
|
||||
{
|
||||
if (this.#index.has(fp))
|
||||
this.#index.get(fp)!.stats.plays++;
|
||||
}
|
||||
|
||||
countSkip (fp: string)
|
||||
{
|
||||
if (this.#index.has(fp))
|
||||
this.#index.get(fp)!.stats.skips++;
|
||||
}
|
||||
|
||||
async scanLibrary (full = false)
|
||||
{
|
||||
this.#logger.info('Starting library scan');
|
||||
@ -95,13 +143,17 @@ class MusicLibrary implements Initialisable
|
||||
title: common.title ?? title,
|
||||
album: common.album,
|
||||
year: common.year,
|
||||
file: fp
|
||||
file: fp,
|
||||
stats: {
|
||||
plays: 0,
|
||||
skips: 0
|
||||
}
|
||||
};
|
||||
this.#index.set(fp, entry);
|
||||
}
|
||||
const end = Date.now();
|
||||
const newFiles = this.#index.size - initialSize;
|
||||
this.#logger.info(`Library scan took ${end - start} ms, ${this.#index.size} (new ${newFiles}) files indexed`);
|
||||
this.#logger.info(`Library scan took ${end - start} ms, ${this.#index.size} (${newFiles} new) files indexed`);
|
||||
this.saveIndex();
|
||||
return newFiles;
|
||||
}
|
||||
@ -115,7 +167,12 @@ class MusicLibrary implements Initialisable
|
||||
const raw = fs.readFileSync(indexPath, { encoding: 'utf-8' });
|
||||
const parsed = JSON.parse(raw) as MusicIndexEntry[];
|
||||
for (const entry of parsed)
|
||||
{
|
||||
if (typeof entry.stats === 'undefined')
|
||||
entry.stats = { plays: 0, skips: 0 };
|
||||
this.#index.set(entry.file, entry);
|
||||
}
|
||||
this.#logger.info(`Index loaded with ${this.#index.size} entries`);
|
||||
}
|
||||
|
||||
saveIndex ()
|
||||
|
@ -4,7 +4,7 @@ import { LoggerClient } from '@navy.gif/logger';
|
||||
|
||||
import Initialisable from '../../interfaces/Initialisable.js';
|
||||
import DiscordClient from '../DiscordClient.js';
|
||||
import { MusicIndexEntry, MusicPlayerOptions } from '../../../@types/MusicPlayer.js';
|
||||
import { MusicIndexEntry, MusicPlayerOptions, QueueOrder } from '../../../@types/MusicPlayer.js';
|
||||
import MusicLibrary from './MusicLibrary.js';
|
||||
|
||||
type ConnectionDetails = {
|
||||
@ -27,9 +27,13 @@ class MusicPlayer implements Initialisable
|
||||
#options: MusicPlayerOptions;
|
||||
#volume: number;
|
||||
#library: MusicLibrary;
|
||||
|
||||
#shuffleIdx: number;
|
||||
#shuffleList: MusicIndexEntry[];
|
||||
|
||||
#queue: MusicIndexEntry[];
|
||||
#currentSong: MusicIndexEntry | null;
|
||||
|
||||
constructor (client: DiscordClient, options: MusicPlayerOptions)
|
||||
{
|
||||
this.#client = client;
|
||||
@ -45,6 +49,8 @@ class MusicPlayer implements Initialisable
|
||||
this.#library = new MusicLibrary(client, this.#options.library);
|
||||
this.#shuffleList = [];
|
||||
this.#shuffleIdx = 0;
|
||||
this.#queue = [];
|
||||
this.#currentSong = null;
|
||||
|
||||
this.#player.on(AudioPlayerStatus.Idle, this.playNext.bind(this));
|
||||
}
|
||||
@ -59,10 +65,30 @@ class MusicPlayer implements Initialisable
|
||||
return this.#library;
|
||||
}
|
||||
|
||||
queue (order: QueueOrder)
|
||||
{
|
||||
const [ result ] = this.library.search(order);
|
||||
if (!result)
|
||||
return null;
|
||||
this.#queue.push(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
playNext ()
|
||||
{
|
||||
const info = this.#shuffleList[this.#shuffleIdx];
|
||||
if (!this.#ready)
|
||||
return;
|
||||
|
||||
if (this.#player.state.status !== AudioPlayerStatus.Idle && this.#currentSong)
|
||||
this.library.countSkip(this.#currentSong.file);
|
||||
|
||||
let info: MusicIndexEntry | null = null;
|
||||
if (this.#queue.length)
|
||||
info = this.#queue.shift()!;
|
||||
else
|
||||
info = this.#shuffleList[this.#shuffleIdx];
|
||||
|
||||
this.#currentSong = info;
|
||||
this.#currentResource = createAudioResource(info.file, {
|
||||
inlineVolume: true,
|
||||
metadata: {
|
||||
@ -73,11 +99,16 @@ class MusicPlayer implements Initialisable
|
||||
|
||||
this.#logger.info(`Now playing ${info.arist} - ${info.title}`);
|
||||
this.#player.play(this.#currentResource);
|
||||
this.#library.countPlay(info.file);
|
||||
|
||||
this.#shuffleIdx++;
|
||||
if (this.#shuffleIdx === this.#shuffleList.length)
|
||||
this.#shuffleIdx = 0;
|
||||
|
||||
this.#client.user?.setPresence({
|
||||
activities: [{
|
||||
name: `${info.arist} - ${info.title}`,
|
||||
type: ActivityType.Playing
|
||||
type: ActivityType.Listening
|
||||
}]
|
||||
});
|
||||
|
||||
@ -92,7 +123,7 @@ class MusicPlayer implements Initialisable
|
||||
await channel.send({
|
||||
embeds: [{
|
||||
title: 'Now playing :notes:',
|
||||
description: `**${info.title}** by ${info.arist}`,
|
||||
description: `**${info!.title}** by ${info!.arist}`,
|
||||
color: 0xffafff
|
||||
}]
|
||||
});
|
||||
@ -119,6 +150,8 @@ class MusicPlayer implements Initialisable
|
||||
|
||||
stop (): void | Promise<void>
|
||||
{
|
||||
this.#ready = false;
|
||||
this.#logger.info('Stopping music player');
|
||||
this.#logger.info('Disconnecting all guilds');
|
||||
for (const [ guildId, { connection, subscription }] of this.#connections)
|
||||
{
|
||||
@ -126,6 +159,7 @@ class MusicPlayer implements Initialisable
|
||||
subscription.unsubscribe();
|
||||
connection.disconnect();
|
||||
}
|
||||
this.#library.stop();
|
||||
}
|
||||
|
||||
async initialise ()
|
||||
@ -140,6 +174,7 @@ class MusicPlayer implements Initialisable
|
||||
|
||||
this.initialiseVoiceChannels();
|
||||
|
||||
this.#ready = true;
|
||||
this.playNext();
|
||||
}
|
||||
|
||||
|
40
src/client/components/commands/Queue.ts
Normal file
40
src/client/components/commands/Queue.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Message } from 'discord.js';
|
||||
import Command from '../../../interfaces/Command.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import { CommandOpts } from '@navy.gif/commandparser';
|
||||
|
||||
class QueueCommand extends Command
|
||||
{
|
||||
constructor (client: DiscordClient)
|
||||
{
|
||||
super(client, {
|
||||
name: 'queue',
|
||||
showUsage: true,
|
||||
options: [{
|
||||
name: 'artist',
|
||||
flag: true
|
||||
}, {
|
||||
name: 'song',
|
||||
required: true
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async execute (message: Message<true>, { args }: CommandOpts)
|
||||
{
|
||||
const { member, guild } = message;
|
||||
const { me } = guild.members;
|
||||
if (!member?.voice || member.voice.channelId !== me?.voice.channelId)
|
||||
return 'Only vc participants can queue songs';
|
||||
const query = {
|
||||
title: args.song!.value as string,
|
||||
artist: args.artist?.value as string | undefined,
|
||||
};
|
||||
const result = this.client.musicPlayer.queue(query);
|
||||
if (!result)
|
||||
return 'Query yielded no results';
|
||||
return `Song **${result.title}** by ${result.arist} queued`;
|
||||
}
|
||||
}
|
||||
|
||||
export default QueueCommand;
|
@ -13,7 +13,7 @@ class PingCommand extends Command
|
||||
|
||||
async execute ()
|
||||
{
|
||||
const diff = this.client.musicPlayer.library.scanLibrary();
|
||||
const diff = await this.client.musicPlayer.library.scanLibrary();
|
||||
const songs = this.client.musicPlayer.library.size;
|
||||
return `Found ${songs} tracks with ${diff} new ones`;
|
||||
}
|
||||
|
42
src/client/components/commands/Search.ts
Normal file
42
src/client/components/commands/Search.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Message } from 'discord.js';
|
||||
import Command from '../../../interfaces/Command.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import { CommandOpts } from '@navy.gif/commandparser';
|
||||
|
||||
class SearchCommand extends Command
|
||||
{
|
||||
constructor (client: DiscordClient)
|
||||
{
|
||||
super(client, {
|
||||
name: 'search',
|
||||
showUsage: true,
|
||||
options: [{
|
||||
name: 'keyword',
|
||||
}, {
|
||||
name: 'artist',
|
||||
flag: true
|
||||
}, {
|
||||
name: 'song',
|
||||
flag: true
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async execute (_message: Message, { args }: CommandOpts)
|
||||
{
|
||||
const query = {
|
||||
title: args.song?.value as string | undefined,
|
||||
artist: args.artist?.value as string | undefined,
|
||||
keyword: args.keyword?.value as string | undefined
|
||||
};
|
||||
const results = this.client.musicPlayer.library.search(query);
|
||||
|
||||
if (!results.length)
|
||||
return 'No results found';
|
||||
return `
|
||||
**Search results:**\n${results.map(result => `\t\\- **${result.arist}** - ${result.title} (${result.album ?? 'Unknown album'}) [${result.year ?? 'Unknown year'}]`).join('\n')}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchCommand;
|
41
src/client/components/commands/SetAvatar.ts
Normal file
41
src/client/components/commands/SetAvatar.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { ArgsResult, CommandOpts, OptionType } from '@navy.gif/commandparser';
|
||||
import Command from '../../../interfaces/Command.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
import { Message } from 'discord.js';
|
||||
|
||||
class SetAvatarCommand extends Command
|
||||
{
|
||||
constructor (client: DiscordClient)
|
||||
{
|
||||
super(client, {
|
||||
name: 'set',
|
||||
options: [{
|
||||
name: 'avatar',
|
||||
type: OptionType.SUB_COMMAND,
|
||||
options: [{
|
||||
name: 'asset',
|
||||
required: true
|
||||
}]
|
||||
}],
|
||||
restricted: true
|
||||
});
|
||||
}
|
||||
|
||||
async execute (message: Message, { subcommand, args }: CommandOpts)
|
||||
{
|
||||
if (subcommand === 'avatar')
|
||||
return this.#setAvatar(message, args);
|
||||
return 'Unknown subcommand';
|
||||
}
|
||||
|
||||
async #setAvatar (_message: Message, args : ArgsResult)
|
||||
{
|
||||
const { asset } = args;
|
||||
if (!asset?.value)
|
||||
return 'Missing value';
|
||||
await this.client.user?.setAvatar(asset.value as string);
|
||||
return 'Avatar successfully set';
|
||||
}
|
||||
}
|
||||
|
||||
export default SetAvatarCommand;
|
@ -1,3 +1,4 @@
|
||||
import { Message } from 'discord.js';
|
||||
import Command from '../../../interfaces/Command.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
|
||||
@ -7,12 +8,19 @@ class SkipCommand extends Command
|
||||
{
|
||||
super(client, {
|
||||
name: 'skip',
|
||||
guildOnly: true
|
||||
guildOnly: true,
|
||||
restricted: true
|
||||
});
|
||||
}
|
||||
|
||||
async execute ()
|
||||
async execute (message: Message<true>)
|
||||
{
|
||||
const { member, author, guild } = message;
|
||||
const { me } = guild.members;
|
||||
if (!member?.voice || member.voice.channelId !== me?.voice.channelId)
|
||||
return 'Only vc participants can adjust volume';
|
||||
|
||||
this.logger.info(`${author.username} (${author.id}) skipped a song`);
|
||||
this.client.musicPlayer.playNext();
|
||||
return 'Song skipped';
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ class VolumeCommand extends Command
|
||||
{
|
||||
super(client, {
|
||||
name: 'volume',
|
||||
aliases: [ 'v' ],
|
||||
options: [
|
||||
{
|
||||
name: 'volume',
|
||||
@ -19,7 +18,8 @@ class VolumeCommand extends Command
|
||||
maximum: 100
|
||||
}
|
||||
],
|
||||
guildOnly: true
|
||||
guildOnly: true,
|
||||
restricted: true
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { inspect } from 'node:util';
|
||||
|
||||
import { APIEmbed, ChannelType, DiscordAPIError, EmbedBuilder, Events, Message, MessagePayload } from 'discord.js';
|
||||
import { CommandOpts, ICommand, Parser, ParserError } from '@navy.gif/commandparser';
|
||||
import { CommandOpts, ICommand, OptionType, Parser, ParserError } from '@navy.gif/commandparser';
|
||||
|
||||
import Observer from '../../../interfaces/Observer.js';
|
||||
import DiscordClient from '../../DiscordClient.js';
|
||||
@ -9,6 +9,7 @@ import { InhibitorResponse } from '../../../../@types/DiscordClient.js';
|
||||
import Command from '../../../interfaces/Command.js';
|
||||
import CommandError from '../../../errors/CommandError.js';
|
||||
import Util from '../../../utilities/Util.js';
|
||||
import { stripIndents } from 'common-tags';
|
||||
|
||||
|
||||
class CommandHandler extends Observer
|
||||
@ -119,6 +120,9 @@ class CommandHandler extends Observer
|
||||
});
|
||||
}
|
||||
|
||||
if ((command as Command).showUsage && !Object.keys(rest.args).length && !rest.subcommand && !rest.subcommandGroup)
|
||||
return this.#showUsage(message, command as Command);
|
||||
|
||||
this.#executeCommand(message, command, rest);
|
||||
}
|
||||
|
||||
@ -220,6 +224,34 @@ class CommandHandler extends Observer
|
||||
}
|
||||
}
|
||||
|
||||
#showUsage (message: Message<boolean>, command: Command): void | PromiseLike<void>
|
||||
{
|
||||
const { options } = command;
|
||||
const flags = options.filter(option => option.flag);
|
||||
const nonFlags = options.filter(option => !option.flag);
|
||||
let output = stripIndents`
|
||||
USAGE: \`${this.client.prefix}${command.name} [OPTIONS] [FLAGS]\`
|
||||
`;
|
||||
|
||||
if (nonFlags.length)
|
||||
{
|
||||
output += '\n\n' + stripIndents`
|
||||
OPTIONS:
|
||||
${nonFlags.map(opt => `\t \\- ${opt.name} (${OptionType[opt.type]})`).join('\n')}
|
||||
`;
|
||||
}
|
||||
|
||||
if (flags.length)
|
||||
{
|
||||
output += '\n\n' + stripIndents`
|
||||
FLAGS:
|
||||
${flags.map(flag => `\t \\- ${flag.name} (${OptionType[flag.type]})`).join('\n')}
|
||||
`;
|
||||
}
|
||||
|
||||
message.reply(output);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CommandHandler;
|
@ -18,6 +18,7 @@ abstract class Command extends Component implements ICommand
|
||||
#guildOnly: boolean;
|
||||
#dmOnly: boolean;
|
||||
#limited: Snowflake[] | null; // Limited to specific roles
|
||||
#showUsage: boolean;
|
||||
|
||||
constructor (client: DiscordClient, def: CommandDefinition)
|
||||
{
|
||||
@ -33,6 +34,7 @@ abstract class Command extends Component implements ICommand
|
||||
this.#guildOnly = def.guildOnly ?? false;
|
||||
this.#dmOnly = def.dmOnly ?? false;
|
||||
this.#limited = def.limited ?? null;
|
||||
this.#showUsage = def.showUsage ?? false;
|
||||
|
||||
this.#options = [];
|
||||
|
||||
@ -67,6 +69,11 @@ abstract class Command extends Component implements ICommand
|
||||
throw new CommandError(this, { reason: 'Command timed out', user });
|
||||
}
|
||||
|
||||
get showUsage ()
|
||||
{
|
||||
return this.#showUsage;
|
||||
}
|
||||
|
||||
get restricted ()
|
||||
{
|
||||
return this.#restricted;
|
||||
|
45
yarn.lock
45
yarn.lock
@ -1804,6 +1804,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/common-tags@npm:^1":
|
||||
version: 1.8.4
|
||||
resolution: "@types/common-tags@npm:1.8.4"
|
||||
checksum: 10/40c95a2f6388beb1cdeed3c9986ac0d6a3a551fce706e3e364a00ded48ab624b06b1ac8b94679bb2da9653e5eb3e450bad26873f5189993a5d8e8bdace74cbb2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/eslint@npm:^8":
|
||||
version: 8.56.6
|
||||
resolution: "@types/eslint@npm:8.56.6"
|
||||
@ -1851,6 +1858,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/similarity@npm:^1":
|
||||
version: 1.2.3
|
||||
resolution: "@types/similarity@npm:1.2.3"
|
||||
checksum: 10/1f3c9ad6e803e3c1d161c6701da09686a86a8772703765b259d3614ab1b0a84294b0b9f9044d596f7310cf583cc8614e1b1699f3e22175ba4af01fd3b295804e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:8.5.9":
|
||||
version: 8.5.9
|
||||
resolution: "@types/ws@npm:8.5.9"
|
||||
@ -2353,6 +2367,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"common-tags@npm:^1.8.2":
|
||||
version: 1.8.2
|
||||
resolution: "common-tags@npm:1.8.2"
|
||||
checksum: 10/c665d0f463ee79dda801471ad8da6cb33ff7332ba45609916a508ad3d77ba07ca9deeb452e83f81f24c2b081e2c1315347f23d239210e63d1c5e1a0c7c019fe2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concat-map@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "concat-map@npm:0.0.1"
|
||||
@ -3251,6 +3272,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"levenshtein-edit-distance@npm:^2.0.0":
|
||||
version: 2.0.5
|
||||
resolution: "levenshtein-edit-distance@npm:2.0.5"
|
||||
bin:
|
||||
levenshtein-edit-distance: cli.js
|
||||
checksum: 10/50618c01cd0c9bae6d4371d75af62c17c25a8f91bfd8d06400315b8b15976900cff951b48e102e074e9c5c6758260fff1675cfad186732afe124a5708e1032fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"levn@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "levn@npm:0.4.1"
|
||||
@ -3552,17 +3582,21 @@ __metadata:
|
||||
"@navy.gif/timestring": "npm:^6.0.6"
|
||||
"@types/babel__core": "npm:^7"
|
||||
"@types/babel__preset-env": "npm:^7"
|
||||
"@types/common-tags": "npm:^1"
|
||||
"@types/eslint": "npm:^8"
|
||||
"@types/humanize-duration": "npm:^3"
|
||||
"@types/similarity": "npm:^1"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.3.1"
|
||||
"@typescript-eslint/parser": "npm:^7.3.1"
|
||||
bufferutil: "npm:^4.0.8"
|
||||
common-tags: "npm:^1.8.2"
|
||||
discord.js: "npm:^14.14.1"
|
||||
dotenv: "npm:^16.4.5"
|
||||
eslint: "npm:^8.57.0"
|
||||
ffmpeg: "npm:^0.0.4"
|
||||
humanize-duration: "npm:^3.31.0"
|
||||
music-metadata: "npm:^7.14.0"
|
||||
similarity: "npm:^1.2.1"
|
||||
sodium-native: "npm:^4.1.1"
|
||||
typescript: "npm:^5.4.3"
|
||||
utf-8-validate: "npm:^6.0.3"
|
||||
@ -4078,6 +4112,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"similarity@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "similarity@npm:1.2.1"
|
||||
dependencies:
|
||||
levenshtein-edit-distance: "npm:^2.0.0"
|
||||
bin:
|
||||
similarity: cli.js
|
||||
checksum: 10/7010abfb53ea72fcecb3b9f59e0753f589256b8a134886a5282cd1ee21226fdc7f11bfe45f673631928d8ae88283f23e6b6f5c4e84ab3f15d175b2d520e1bffe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slash@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "slash@npm:3.0.0"
|
||||
|
Loading…
Reference in New Issue
Block a user