mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-12 21:05:29 +01:00
Merge branch 'master' into feature/slash-commands
This commit is contained in:
commit
aacb107f43
7
.github/workflows/pr.yml
vendored
7
.github/workflows/pr.yml
vendored
|
@ -26,7 +26,7 @@ jobs:
|
|||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}-${{ github.sha }}
|
||||
key: ${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}-${{ github.event.pull_request.head.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-prs-${{ matrix.build-arch }}
|
||||
|
||||
|
@ -36,10 +36,15 @@ jobs:
|
|||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: codetheweb/muse:${{ github.event.pull_request.head.sha }}-${{ matrix.tagged-platform }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Migrated to [Slash Commands](https://support.discord.com/hc/en-us/articles/1500000368501-Slash-Commands-FAQ)
|
||||
- The queue embed now automatically updates every 5 seconds (and has buttons for quick interactions)
|
||||
|
||||
## [0.5.1] - 2022-01-25
|
||||
### Fixed
|
||||
- Queueing Spotify playlists could sometimes fail when a song wasn't found on YouTube
|
||||
|
||||
## [0.5.0] - 2022-01-21
|
||||
### Changed
|
||||
- Queue embeds are now more detailed and appear when resuming playback. Thanks @bokherus!
|
||||
|
||||
## [0.4.0] - 2022-01-17
|
||||
### Added
|
||||
- Playlists can now be shuffled as they are added to the queue, using the `shuffle` option to `play`.
|
||||
|
@ -45,7 +53,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
### Added
|
||||
- Initial release
|
||||
|
||||
[Unreleased]: https://github.com/codetheweb/muse/compare/v0.4.0...HEAD
|
||||
[Unreleased]: https://github.com/codetheweb/muse/compare/v0.5.1...HEAD
|
||||
[0.5.1]: https://github.com/codetheweb/muse/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/codetheweb/muse/compare/v0.4.0...v0.5.0
|
||||
[0.4.0]: https://github.com/codetheweb/muse/compare/v0.3.2...v0.4.0
|
||||
[0.3.2]: https://github.com/codetheweb/muse/compare/v0.3.1...v0.3.2
|
||||
[0.3.1]: https://github.com/codetheweb/muse/compare/v0.3.0...v0.3.1
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "muse",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.1",
|
||||
"description": "🎧 a self-hosted Discord music bot that doesn't suck ",
|
||||
"exports": "./dist/src/index.js",
|
||||
"repository": "git@github.com:codetheweb/muse.git",
|
||||
|
@ -115,7 +115,7 @@
|
|||
"spotify-web-api-node": "^5.0.2",
|
||||
"xbytes": "^1.7.0",
|
||||
"youtube.ts": "^0.2.5",
|
||||
"ytdl-core": "^4.9.2",
|
||||
"ytsr": "^3.5.3"
|
||||
"ytdl-core": "^4.10.0",
|
||||
"ytsr": "^3.6.0"
|
||||
}
|
||||
}
|
||||
|
|
77
src/bot.ts
77
src/bot.ts
|
@ -33,7 +33,7 @@ export default class {
|
|||
this.commandsByButtonId = new Collection();
|
||||
}
|
||||
|
||||
public async listen(): Promise<void> {
|
||||
public async register(): Promise<void> {
|
||||
// Load in commands
|
||||
container.getAll<Command>(TYPES.Command).forEach(command => {
|
||||
// TODO: remove !
|
||||
|
@ -48,47 +48,28 @@ export default class {
|
|||
|
||||
// Register event handlers
|
||||
this.client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isCommand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = this.commandsByName.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!interaction.guild) {
|
||||
await interaction.reply(errorMsg('you can\'t use this bot in a DM'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (command.requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
|
||||
await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
|
||||
return;
|
||||
}
|
||||
if (interaction.isCommand()) {
|
||||
const command = this.commandsByName.get(interaction.commandName);
|
||||
|
||||
if (command.execute) {
|
||||
await command.execute(interaction);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
debug(error);
|
||||
|
||||
// This can fail if the message was deleted, and we don't want to crash the whole bot
|
||||
try {
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.editReply(errorMsg('something went wrong'));
|
||||
} else {
|
||||
await interaction.reply({content: errorMsg(error as Error), ephemeral: true});
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on('interactionCreate', async interaction => {
|
||||
try {
|
||||
if (interaction.isButton()) {
|
||||
if (!interaction.guild) {
|
||||
await interaction.reply(errorMsg('you can\'t use this bot in a DM'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
|
||||
await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.execute) {
|
||||
await command.execute(interaction);
|
||||
}
|
||||
} else if (interaction.isButton()) {
|
||||
const command = this.commandsByButtonId.get(interaction.customId);
|
||||
|
||||
if (!command) {
|
||||
|
@ -98,9 +79,7 @@ export default class {
|
|||
if (command.handleButtonInteraction) {
|
||||
await command.handleButtonInteraction(interaction);
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isAutocomplete()) {
|
||||
} else if (interaction.isAutocomplete()) {
|
||||
const command = this.commandsByName.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
|
@ -114,14 +93,14 @@ export default class {
|
|||
} catch (error: unknown) {
|
||||
debug(error);
|
||||
|
||||
// Can't reply with errors for autocomplete queries
|
||||
if (interaction.isButton()) {
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.editReply(errorMsg('something went wrong'));
|
||||
} else {
|
||||
// This can fail if the message was deleted, and we don't want to crash the whole bot
|
||||
try {
|
||||
if ((interaction.isApplicationCommand() || interaction.isButton()) && (interaction.replied || interaction.deferred)) {
|
||||
await interaction.editReply(errorMsg(error as Error));
|
||||
} else if (interaction.isApplicationCommand() || interaction.isButton()) {
|
||||
await interaction.reply({content: errorMsg(error as Error), ephemeral: true});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -138,7 +117,7 @@ export default class {
|
|||
|
||||
await rest.put(
|
||||
Routes.applicationCommands(this.client.user!.id),
|
||||
{body: this.commandsByName.map(command => command.slashCommand ? command.slashCommand.toJSON() : null)},
|
||||
{body: this.commandsByName.map(command => command.slashCommand.toJSON())},
|
||||
);
|
||||
} else {
|
||||
spinner.text = '📡 updating commands in all guilds...';
|
||||
|
@ -147,7 +126,7 @@ export default class {
|
|||
this.client.guilds.cache.map(async guild => {
|
||||
await rest.put(
|
||||
Routes.applicationGuildCommands(this.client.user!.id, guild.id),
|
||||
{body: this.commandsByName.map(command => command.slashCommand ? command.slashCommand.toJSON() : null)},
|
||||
{body: this.commandsByName.map(command => command.slashCommand.toJSON())},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -10,13 +10,13 @@ import {TYPES} from '../types.js';
|
|||
import {QueuedSong, STATUS} from '../services/player.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {getMostPopularVoiceChannel, getMemberVoiceChannel} from '../utils/channels.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import GetSongs from '../services/get-songs.js';
|
||||
import {prisma} from '../utils/db.js';
|
||||
import ThirdParty from '../services/third-party.js';
|
||||
import getYouTubeAndSpotifySuggestionsFor from '../utils/get-youtube-and-spotify-suggestions-for.js';
|
||||
import KeyValueCacheProvider from '../services/key-value-cache.js';
|
||||
import {ONE_HOUR_IN_SECONDS} from '../utils/constants.js';
|
||||
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
@ -68,31 +68,33 @@ export default class implements Command {
|
|||
|
||||
if (!query) {
|
||||
if (player.status === STATUS.PLAYING) {
|
||||
await interaction.reply({content: errorMsg('already playing, give me a song name'), ephemeral: true});
|
||||
return;
|
||||
throw new Error('already playing, give me a song name');
|
||||
}
|
||||
|
||||
// Must be resuming play
|
||||
if (!wasPlayingSong) {
|
||||
await interaction.reply({content: errorMsg('nothing to play'), ephemeral: true});
|
||||
return;
|
||||
throw new Error('nothing to play');
|
||||
}
|
||||
|
||||
await player.connect(targetVoiceChannel);
|
||||
await player.play();
|
||||
|
||||
await interaction.reply('the stop-and-go light is now green');
|
||||
await interaction.reply({
|
||||
content: 'the stop-and-go light is now green',
|
||||
embeds: [buildPlayingMessageEmbed(player)],
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const addToFrontOfQueue = interaction.options.getBoolean('immediate');
|
||||
const shuffleAdditions = interaction.options.getBoolean('shuffle');
|
||||
|
||||
let newSongs: Array<Except<QueuedSong, 'addedInChannelId'>> = [];
|
||||
let extraMsg = '';
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
let newSongs: Array<Except<QueuedSong, 'addedInChannelId' | 'requestedBy'>> = [];
|
||||
let extraMsg = '';
|
||||
|
||||
// Test if it's a complete URL
|
||||
try {
|
||||
const url = new URL(query);
|
||||
|
@ -111,14 +113,12 @@ export default class implements Command {
|
|||
// YouTube playlist
|
||||
newSongs.push(...await this.getSongs.youtubePlaylist(url.searchParams.get('list')!));
|
||||
} else {
|
||||
// Single video
|
||||
const song = await this.getSongs.youtubeVideo(url.href);
|
||||
|
||||
if (song) {
|
||||
newSongs.push(song);
|
||||
} else {
|
||||
await interaction.editReply(errorMsg('that doesn\'t exist'));
|
||||
return;
|
||||
throw new Error('that doesn\'t exist');
|
||||
}
|
||||
}
|
||||
} else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
|
||||
|
@ -149,14 +149,12 @@ export default class implements Command {
|
|||
if (song) {
|
||||
newSongs.push(song);
|
||||
} else {
|
||||
await interaction.editReply(errorMsg('that doesn\'t exist'));
|
||||
return;
|
||||
throw new Error('that doesn\'t exist');
|
||||
}
|
||||
}
|
||||
|
||||
if (newSongs.length === 0) {
|
||||
await interaction.editReply(errorMsg('no songs found'));
|
||||
return;
|
||||
throw new Error('no songs found');
|
||||
}
|
||||
|
||||
if (shuffleAdditions) {
|
||||
|
@ -164,7 +162,7 @@ export default class implements Command {
|
|||
}
|
||||
|
||||
newSongs.forEach(song => {
|
||||
player.add({...song, addedInChannelId: interaction.channel?.id}, {immediate: addToFrontOfQueue ?? false});
|
||||
player.add({...song, addedInChannelId: interaction.channel!.id, requestedBy: interaction.member!.user.id}, {immediate: addToFrontOfQueue ?? false});
|
||||
});
|
||||
|
||||
const firstSong = newSongs[0];
|
||||
|
@ -180,6 +178,10 @@ export default class implements Command {
|
|||
if (wasPlayingSong) {
|
||||
statusMsg = 'resuming playback';
|
||||
}
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [buildPlayingMessageEmbed(player)],
|
||||
});
|
||||
}
|
||||
|
||||
// Build response message
|
||||
|
@ -206,9 +208,18 @@ export default class implements Command {
|
|||
const query = interaction.options.getString('query')?.trim();
|
||||
|
||||
if (!query || query.length === 0) {
|
||||
return interaction.respond([]);
|
||||
await interaction.respond([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Don't return suggestions for URLs
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(query);
|
||||
await interaction.respond([]);
|
||||
return;
|
||||
} catch {}
|
||||
|
||||
const suggestions = await this.cache.wrap(
|
||||
getYouTubeAndSpotifySuggestionsFor,
|
||||
query,
|
||||
|
|
|
@ -1,80 +1,32 @@
|
|||
import {ButtonInteraction, CommandInteraction} from 'discord.js';
|
||||
import {CommandInteraction} from 'discord.js';
|
||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import UpdatingQueueEmbedManager from '../managers/updating-queue-embed.js';
|
||||
import {BUTTON_IDS} from '../services/updating-queue-embed.js';
|
||||
import Command from '.';
|
||||
import {buildQueueEmbed} from '../utils/build-embed.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
public readonly slashCommand = new SlashCommandBuilder()
|
||||
.setName('queue')
|
||||
.setDescription('show the current queue');
|
||||
|
||||
public readonly handledButtonIds = Object.values(BUTTON_IDS);
|
||||
.setDescription('show the current queue')
|
||||
.addIntegerOption(option => option
|
||||
.setName('page')
|
||||
.setDescription('page of queue to show [default: 1]')
|
||||
.setRequired(false));
|
||||
|
||||
private readonly playerManager: PlayerManager;
|
||||
private readonly updatingQueueEmbedManager: UpdatingQueueEmbedManager;
|
||||
|
||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Managers.UpdatingQueueEmbed) updatingQueueEmbedManager: UpdatingQueueEmbedManager) {
|
||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
|
||||
this.playerManager = playerManager;
|
||||
this.updatingQueueEmbedManager = updatingQueueEmbedManager;
|
||||
}
|
||||
|
||||
public async execute(interaction: CommandInteraction) {
|
||||
const embed = this.updatingQueueEmbedManager.get(interaction.guild!.id);
|
||||
|
||||
await embed.createFromInteraction(interaction);
|
||||
}
|
||||
|
||||
public async handleButtonInteraction(interaction: ButtonInteraction) {
|
||||
const player = this.playerManager.get(interaction.guild!.id);
|
||||
const embed = this.updatingQueueEmbedManager.get(interaction.guild!.id);
|
||||
|
||||
const buttonId = interaction.customId as keyof typeof this.handledButtonIds;
|
||||
const embed = buildQueueEmbed(player, interaction.options.getInteger('page') ?? 1);
|
||||
|
||||
// Not entirely sure why this is necessary.
|
||||
// We don't wait for the Promise to resolve here to avoid blocking the
|
||||
// main logic. However, we need to wait for the Promise to be resolved before
|
||||
// throwing as otherwise a race condition pops up when bot.ts tries updating
|
||||
// the interaction.
|
||||
const deferedUpdatePromise = interaction.deferUpdate();
|
||||
|
||||
try {
|
||||
switch (buttonId) {
|
||||
case BUTTON_IDS.TRACK_BACK:
|
||||
await player.back();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.TRACK_FORWARD:
|
||||
await player.forward(1);
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PAUSE:
|
||||
player.pause();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PLAY:
|
||||
await player.play();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PAGE_BACK:
|
||||
await embed.pageBack();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PAGE_FORWARD:
|
||||
await embed.pageForward();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('unknown customId');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
await deferedUpdatePromise;
|
||||
|
||||
throw error;
|
||||
}
|
||||
await interaction.reply({embeds: [embed]});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import PlayerManager from '../managers/player.js';
|
|||
import Command from '.';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
@ -35,9 +36,12 @@ export default class implements Command {
|
|||
|
||||
try {
|
||||
await player.forward(numToSkip);
|
||||
await interaction.reply('keep \'er movin\'');
|
||||
await interaction.reply({
|
||||
content: 'keep \'er movin\'',
|
||||
embeds: player.getCurrent() ? [buildPlayingMessageEmbed(player)] : [],
|
||||
});
|
||||
} catch (_: unknown) {
|
||||
await interaction.reply({content: errorMsg('invalid number of songs to skip'), ephemeral: true});
|
||||
await interaction.reply({content: errorMsg('no song to skip to'), ephemeral: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import PlayerManager from '../managers/player.js';
|
|||
import errorMsg from '../utils/error-msg.js';
|
||||
import Command from '.';
|
||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
|
@ -25,8 +26,10 @@ export default class implements Command {
|
|||
|
||||
try {
|
||||
await player.back();
|
||||
|
||||
await interaction.reply('back \'er up\'');
|
||||
await interaction.reply({
|
||||
content: 'back \'er up\'',
|
||||
embeds: player.getCurrent() ? [buildPlayingMessageEmbed(player)] : [],
|
||||
});
|
||||
} catch (_: unknown) {
|
||||
await interaction.reply({
|
||||
content: errorMsg('no song to go back to'),
|
||||
|
|
|
@ -18,7 +18,7 @@ const startBot = async () => {
|
|||
|
||||
await container.get<FileCacheProvider>(TYPES.FileCache).cleanup();
|
||||
|
||||
await bot.listen();
|
||||
await bot.register();
|
||||
};
|
||||
|
||||
export {startBot};
|
||||
|
|
|
@ -7,7 +7,6 @@ import ConfigProvider from './services/config.js';
|
|||
|
||||
// Managers
|
||||
import PlayerManager from './managers/player.js';
|
||||
import UpdatingQueueEmbed from './managers/updating-queue-embed.js';
|
||||
|
||||
// Helpers
|
||||
import GetSongs from './services/get-songs.js';
|
||||
|
@ -46,7 +45,6 @@ container.bind<Client>(TYPES.Client).toConstantValue(new Client({intents}));
|
|||
|
||||
// Managers
|
||||
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
|
||||
container.bind<UpdatingQueueEmbed>(TYPES.Managers.UpdatingQueueEmbed).to(UpdatingQueueEmbed).inSingletonScope();
|
||||
|
||||
// Helpers
|
||||
container.bind<GetSongs>(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope();
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import UpdatingQueueEmbed from '../services/updating-queue-embed.js';
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
private readonly embedsByGuild: Map<string, UpdatingQueueEmbed>;
|
||||
private readonly playerManager: PlayerManager;
|
||||
|
||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
|
||||
this.embedsByGuild = new Map();
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
get(guildId: string): UpdatingQueueEmbed {
|
||||
let embed = this.embedsByGuild.get(guildId);
|
||||
|
||||
if (!embed) {
|
||||
const player = this.playerManager.get(guildId);
|
||||
|
||||
if (!player) {
|
||||
throw new Error('Player does not exist for guild.');
|
||||
}
|
||||
|
||||
embed = new UpdatingQueueEmbed(player);
|
||||
|
||||
this.embedsByGuild.set(guildId, embed);
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ import Config from './config.js';
|
|||
import KeyValueCacheProvider from './key-value-cache.js';
|
||||
import {ONE_HOUR_IN_SECONDS, ONE_MINUTE_IN_SECONDS} from '../utils/constants.js';
|
||||
|
||||
type QueuedSongWithoutChannel = Except<QueuedSong, 'addedInChannelId'>;
|
||||
type SongMetadata = Except<QueuedSong, 'addedInChannelId' | 'requestedBy'>;
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
|
@ -40,7 +40,7 @@ export default class {
|
|||
this.ytsrQueue = new PQueue({concurrency: 4});
|
||||
}
|
||||
|
||||
async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel> {
|
||||
async youtubeVideoSearch(query: string): Promise<SongMetadata> {
|
||||
const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
|
||||
ytsr,
|
||||
query,
|
||||
|
@ -68,7 +68,7 @@ export default class {
|
|||
return this.youtubeVideo(firstVideo.id);
|
||||
}
|
||||
|
||||
async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel> {
|
||||
async youtubeVideo(url: string): Promise<SongMetadata> {
|
||||
const videoDetails = await this.cache.wrap(
|
||||
this.youtube.videos.get,
|
||||
cleanUrl(url),
|
||||
|
@ -84,10 +84,11 @@ export default class {
|
|||
url: videoDetails.id,
|
||||
playlist: null,
|
||||
isLive: videoDetails.snippet.liveBroadcastContent === 'live',
|
||||
thumbnailUrl: videoDetails.snippet.thumbnails.medium.url,
|
||||
};
|
||||
}
|
||||
|
||||
async youtubePlaylist(listId: string): Promise<QueuedSongWithoutChannel[]> {
|
||||
async youtubePlaylist(listId: string): Promise<SongMetadata[]> {
|
||||
// YouTube playlist
|
||||
const playlist = await this.cache.wrap(
|
||||
this.youtube.playlists.get,
|
||||
|
@ -156,7 +157,7 @@ export default class {
|
|||
|
||||
const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id};
|
||||
|
||||
const songsToReturn: QueuedSongWithoutChannel[] = [];
|
||||
const songsToReturn: SongMetadata[] = [];
|
||||
|
||||
for (const video of playlistVideos) {
|
||||
try {
|
||||
|
@ -169,6 +170,7 @@ export default class {
|
|||
url: video.contentDetails.videoId,
|
||||
playlist: queuedPlaylist,
|
||||
isLive: false,
|
||||
thumbnailUrl: video.snippet.thumbnails.medium.url,
|
||||
});
|
||||
} catch (_: unknown) {
|
||||
// Private and deleted videos are sometimes in playlists, duration of these is not returned and they should not be added to the queue.
|
||||
|
@ -178,7 +180,7 @@ export default class {
|
|||
return songsToReturn;
|
||||
}
|
||||
|
||||
async spotifySource(url: string, playlistLimit: number): Promise<[QueuedSongWithoutChannel[], number, number]> {
|
||||
async spotifySource(url: string, playlistLimit: number): Promise<[SongMetadata[], number, number]> {
|
||||
const parsed = spotifyURI.parse(url);
|
||||
|
||||
let tracks: SpotifyApi.TrackObjectSimplified[] = [];
|
||||
|
@ -251,14 +253,17 @@ export default class {
|
|||
tracks = shuffled.slice(0, playlistLimit);
|
||||
}
|
||||
|
||||
let songs = await Promise.all(tracks.map(async track => this.spotifyToYouTube(track, playlist)));
|
||||
const searchResults = await Promise.allSettled(tracks.map(async track => this.spotifyToYouTube(track)));
|
||||
|
||||
let nSongsNotFound = 0;
|
||||
|
||||
// Get rid of null values
|
||||
songs = songs.reduce((accum: QueuedSongWithoutChannel[], song) => {
|
||||
if (song) {
|
||||
accum.push(song);
|
||||
// Count songs that couldn't be found
|
||||
const songs: SongMetadata[] = searchResults.reduce((accum: SongMetadata[], result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
accum.push({
|
||||
...result.value,
|
||||
...(playlist ? {playlist} : {}),
|
||||
});
|
||||
} else {
|
||||
nSongsNotFound++;
|
||||
}
|
||||
|
@ -266,10 +271,10 @@ export default class {
|
|||
return accum;
|
||||
}, []);
|
||||
|
||||
return [songs as QueuedSongWithoutChannel[], nSongsNotFound, originalNSongs];
|
||||
return [songs, nSongsNotFound, originalNSongs];
|
||||
}
|
||||
|
||||
private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel> {
|
||||
private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified): Promise<SongMetadata> {
|
||||
return this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ export default class {
|
|||
playlist: null,
|
||||
isLive: false,
|
||||
addedInChannelId: msg.channel.id,
|
||||
thumbnailUrl: null,
|
||||
requestedBy: msg.author.id,
|
||||
}, 8, 10),
|
||||
]);
|
||||
|
||||
|
@ -49,6 +51,8 @@ export default class {
|
|||
playlist: null,
|
||||
isLive: false,
|
||||
addedInChannelId: msg.channel.id,
|
||||
thumbnailUrl: null,
|
||||
requestedBy: msg.author.id,
|
||||
}, 358, 5.5),
|
||||
]);
|
||||
|
||||
|
@ -66,6 +70,8 @@ export default class {
|
|||
playlist: null,
|
||||
isLive: false,
|
||||
addedInChannelId: msg.channel.id,
|
||||
thumbnailUrl: null,
|
||||
requestedBy: msg.author.id,
|
||||
}, 50, 13),
|
||||
]);
|
||||
|
||||
|
|
|
@ -23,7 +23,9 @@ export interface QueuedSong {
|
|||
length: number;
|
||||
playlist: QueuedPlaylist | null;
|
||||
isLive: boolean;
|
||||
addedInChannelId?: Snowflake;
|
||||
addedInChannelId: Snowflake;
|
||||
thumbnailUrl: string | null;
|
||||
requestedBy: string;
|
||||
}
|
||||
|
||||
export enum STATUS {
|
||||
|
|
|
@ -1,229 +0,0 @@
|
|||
import {CommandInteraction, MessageActionRow, MessageButton, MessageEmbed, DiscordAPIError} from 'discord.js';
|
||||
import getYouTubeID from 'get-youtube-id';
|
||||
import getProgressBar from '../utils/get-progress-bar.js';
|
||||
import {prettyTime} from '../utils/time.js';
|
||||
import Player, {STATUS} from './player.js';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const REFRESH_INTERVAL_MS = 5 * 1000;
|
||||
|
||||
export enum BUTTON_IDS {
|
||||
PAGE_BACK = 'page-back',
|
||||
PAGE_FORWARD = 'page-forward',
|
||||
TRACK_BACK = 'track-back',
|
||||
TRACK_FORWARD = 'track-forward',
|
||||
PAUSE = 'pause',
|
||||
PLAY = 'play',
|
||||
}
|
||||
|
||||
export default class {
|
||||
private readonly player: Player;
|
||||
private interaction?: CommandInteraction;
|
||||
|
||||
// 1-indexed
|
||||
private currentPage = 1;
|
||||
|
||||
private refreshTimeout?: NodeJS.Timeout;
|
||||
|
||||
constructor(player: Player) {
|
||||
this.player = player;
|
||||
|
||||
this.addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates & replies with a new embed from the given interaction.
|
||||
* Starts updating the embed at a regular interval.
|
||||
* Can be called multiple times within the lifecycle of this class.
|
||||
* Calling this method will make it forgot the previous interaction & reply.
|
||||
* @param interaction
|
||||
*/
|
||||
async createFromInteraction(interaction: CommandInteraction) {
|
||||
const oldInteraction = this.interaction;
|
||||
|
||||
this.resetState();
|
||||
|
||||
this.interaction = interaction;
|
||||
|
||||
await Promise.all([
|
||||
interaction.reply({
|
||||
embeds: [this.buildEmbed()],
|
||||
components: this.buildButtons(this.player),
|
||||
}),
|
||||
(async () => {
|
||||
if (oldInteraction) {
|
||||
await oldInteraction.deleteReply();
|
||||
}
|
||||
})(),
|
||||
]);
|
||||
|
||||
if (!this.refreshTimeout) {
|
||||
this.refreshTimeout = setInterval(async () => this.update(), REFRESH_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
async update(shouldResetPage = false) {
|
||||
if (shouldResetPage) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.interaction?.editReply({
|
||||
embeds: [this.buildEmbed()],
|
||||
components: this.buildButtons(this.player),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof DiscordAPIError) {
|
||||
// Interaction / message was deleted
|
||||
if (error.code === 10008) {
|
||||
this.resetState();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async pageBack() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
}
|
||||
|
||||
await this.update();
|
||||
}
|
||||
|
||||
async pageForward() {
|
||||
if (this.currentPage < this.getMaxPage()) {
|
||||
this.currentPage++;
|
||||
}
|
||||
|
||||
await this.update();
|
||||
}
|
||||
|
||||
private resetState() {
|
||||
if (this.refreshTimeout) {
|
||||
clearInterval(this.refreshTimeout);
|
||||
this.refreshTimeout = undefined;
|
||||
}
|
||||
|
||||
this.currentPage = 1;
|
||||
this.interaction = undefined;
|
||||
}
|
||||
|
||||
private buildButtons(player: Player): MessageActionRow[] {
|
||||
const queuePageControls = new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PAGE_BACK)
|
||||
.setStyle('SECONDARY')
|
||||
.setDisabled(this.currentPage === 1)
|
||||
.setEmoji('⬅️'),
|
||||
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PAGE_FORWARD)
|
||||
.setStyle('SECONDARY')
|
||||
.setDisabled(this.currentPage >= this.getMaxPage())
|
||||
.setEmoji('➡️'),
|
||||
);
|
||||
|
||||
const components = [];
|
||||
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.TRACK_BACK)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.canGoBack())
|
||||
.setEmoji('⏮'));
|
||||
|
||||
if (player.status === STATUS.PLAYING) {
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PAUSE)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.getCurrent())
|
||||
.setEmoji('⏸️'));
|
||||
} else {
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PLAY)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.getCurrent())
|
||||
.setEmoji('▶️'));
|
||||
}
|
||||
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.TRACK_FORWARD)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.canGoForward(1))
|
||||
.setEmoji('⏭'),
|
||||
);
|
||||
|
||||
const playerControls = new MessageActionRow().addComponents(components);
|
||||
|
||||
return [queuePageControls, playerControls];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an embed for the current page of the queue.
|
||||
* @returns MessageEmbed
|
||||
*/
|
||||
private buildEmbed() {
|
||||
const currentlyPlaying = this.player.getCurrent();
|
||||
|
||||
if (!currentlyPlaying) {
|
||||
throw new Error('queue is empty');
|
||||
}
|
||||
|
||||
const queueSize = this.player.queueSize();
|
||||
|
||||
if (this.currentPage > this.getMaxPage()) {
|
||||
throw new Error('the queue isn\'t that big');
|
||||
}
|
||||
|
||||
const embed = new MessageEmbed();
|
||||
|
||||
embed.setTitle(currentlyPlaying.title);
|
||||
embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url.length === 11 ? currentlyPlaying.url : getYouTubeID(currentlyPlaying.url) ?? ''}`);
|
||||
|
||||
let description = getProgressBar(20, this.player.getPosition() / currentlyPlaying.length);
|
||||
description += ' ';
|
||||
description += `\`[${prettyTime(this.player.getPosition())}/${currentlyPlaying.isLive ? 'live' : prettyTime(currentlyPlaying.length)}]\``;
|
||||
description += ' 🔉';
|
||||
description += this.player.isQueueEmpty() ? '' : '\n\n**Next up:**';
|
||||
|
||||
embed.setDescription(description);
|
||||
|
||||
let footer = `Source: ${currentlyPlaying.artist}`;
|
||||
|
||||
if (currentlyPlaying.playlist) {
|
||||
footer += ` (${currentlyPlaying.playlist.title})`;
|
||||
}
|
||||
|
||||
embed.setFooter(footer);
|
||||
|
||||
const queuePageBegin = (this.currentPage - 1) * PAGE_SIZE;
|
||||
const queuePageEnd = queuePageBegin + PAGE_SIZE;
|
||||
|
||||
this.player.getQueue().slice(queuePageBegin, queuePageEnd).forEach((song, i) => {
|
||||
embed.addField(`${(i + 1 + queuePageBegin).toString()}/${queueSize.toString()}`, song.title, false);
|
||||
});
|
||||
|
||||
embed.addField('Page', `${this.currentPage} out of ${this.getMaxPage()}`, false);
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
private getMaxPage() {
|
||||
return Math.ceil((this.player.queueSize() + 1) / PAGE_SIZE);
|
||||
}
|
||||
|
||||
private addEventHandlers() {
|
||||
this.player.on('statusChange', async () => this.update(true));
|
||||
|
||||
// TODO: also update on other player events
|
||||
}
|
||||
}
|
127
src/utils/build-embed.ts
Normal file
127
src/utils/build-embed.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import getYouTubeID from 'get-youtube-id';
|
||||
import {MessageEmbed} from 'discord.js';
|
||||
import Player, {QueuedSong, STATUS} from '../services/player.js';
|
||||
import getProgressBar from './get-progress-bar.js';
|
||||
import {prettyTime} from './time.js';
|
||||
import {truncate} from './string.js';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const getMaxSongTitleLength = (title: string) => {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const nonASCII = /[^\x00-\x7F]+/;
|
||||
return nonASCII.test(title) ? 28 : 48;
|
||||
};
|
||||
|
||||
const getSongTitle = ({title, url}: QueuedSong, shouldTruncate = false) => {
|
||||
const cleanSongTitle = title.replace(/\[.*\]/, '').trim();
|
||||
|
||||
const songTitle = shouldTruncate ? truncate(cleanSongTitle, getMaxSongTitleLength(cleanSongTitle)) : cleanSongTitle;
|
||||
const youtubeId = url.length === 11 ? url : getYouTubeID(url) ?? '';
|
||||
|
||||
return `[${songTitle}](https://www.youtube.com/watch?v=${youtubeId})`;
|
||||
};
|
||||
|
||||
const getQueueInfo = (player: Player) => {
|
||||
const queueSize = player.queueSize();
|
||||
if (queueSize === 0) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return queueSize === 1 ? '1 song' : `${queueSize} songs`;
|
||||
};
|
||||
|
||||
const getPlayerUI = (player: Player) => {
|
||||
const song = player.getCurrent();
|
||||
|
||||
if (!song) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const position = player.getPosition();
|
||||
const button = player.status === STATUS.PLAYING ? '⏹️' : '▶️';
|
||||
const progressBar = getProgressBar(15, position / song.length);
|
||||
const elapsedTime = `${prettyTime(position)}/${song.isLive ? 'live' : prettyTime(song.length)}`;
|
||||
|
||||
return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉`;
|
||||
};
|
||||
|
||||
export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => {
|
||||
const currentlyPlaying = player.getCurrent();
|
||||
|
||||
if (!currentlyPlaying) {
|
||||
throw new Error('No playing song found');
|
||||
}
|
||||
|
||||
const {artist, thumbnailUrl, requestedBy} = currentlyPlaying;
|
||||
const message = new MessageEmbed();
|
||||
|
||||
message
|
||||
.setColor('DARK_GREEN')
|
||||
.setTitle('Now Playing')
|
||||
.setDescription(`
|
||||
**${getSongTitle(currentlyPlaying)}**
|
||||
Requested by: <@${requestedBy}>\n
|
||||
${getPlayerUI(player)}
|
||||
`)
|
||||
.setFooter({text: `Source: ${artist}`});
|
||||
|
||||
if (thumbnailUrl) {
|
||||
message.setThumbnail(thumbnailUrl);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => {
|
||||
const currentlyPlaying = player.getCurrent();
|
||||
|
||||
if (!currentlyPlaying) {
|
||||
throw new Error('queue is empty');
|
||||
}
|
||||
|
||||
const queueSize = player.queueSize();
|
||||
const maxQueuePage = Math.ceil((queueSize + 1) / PAGE_SIZE);
|
||||
|
||||
if (page > maxQueuePage) {
|
||||
throw new Error('the queue isn\'t that big');
|
||||
}
|
||||
|
||||
const queuePageBegin = (page - 1) * PAGE_SIZE;
|
||||
const queuePageEnd = queuePageBegin + PAGE_SIZE;
|
||||
const queuedSongs = player
|
||||
.getQueue()
|
||||
.slice(queuePageBegin, queuePageEnd)
|
||||
.map((song, index) => `\`${index + 1 + queuePageBegin}.\` ${getSongTitle(song, true)} \`[${prettyTime(song.length)}]\``)
|
||||
.join('\n');
|
||||
|
||||
const {artist, thumbnailUrl, playlist, requestedBy} = currentlyPlaying;
|
||||
const playlistTitle = playlist ? `(${playlist.title})` : '';
|
||||
const totalLength = player.getQueue().reduce((accumulator, current) => accumulator + current.length, 0);
|
||||
|
||||
const message = new MessageEmbed();
|
||||
|
||||
let description = `**${getSongTitle(currentlyPlaying)}**\n`;
|
||||
description += `Requested by: <@${requestedBy}>\n\n`;
|
||||
description += `${getPlayerUI(player)}\n\n`;
|
||||
|
||||
if (player.getQueue().length > 0) {
|
||||
description += '**Up next:**\n';
|
||||
description += queuedSongs;
|
||||
}
|
||||
|
||||
message
|
||||
.setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Queued songs')
|
||||
.setColor(player.status === STATUS.PLAYING ? 'DARK_GREEN' : 'NOT_QUITE_BLACK')
|
||||
.setDescription(description)
|
||||
.addField('In queue', getQueueInfo(player), true)
|
||||
.addField('Total length', `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, true)
|
||||
.addField('Page', `${page} out of ${maxQueuePage}`, true)
|
||||
.setFooter({text: `Source: ${artist} ${playlistTitle}`});
|
||||
|
||||
if (thumbnailUrl) {
|
||||
message.setThumbnail(thumbnailUrl);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
|
@ -5,7 +5,7 @@ export default (error?: string | Error): string => {
|
|||
if (typeof error === 'string') {
|
||||
str = `🚫 ${error}`;
|
||||
} else if (error instanceof Error) {
|
||||
str = `🚫 ope: ${error.name}`;
|
||||
str = `🚫 ope: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import {TextChannel, Message, MessageReaction} from 'discord.js';
|
||||
import delay from 'delay';
|
||||
|
||||
const INITAL_DELAY = 500;
|
||||
const PERIOD = 500;
|
||||
|
||||
export default class {
|
||||
public isStopped = true;
|
||||
private readonly channel: TextChannel;
|
||||
private readonly text: string;
|
||||
private msg!: Message;
|
||||
|
||||
constructor(channel: TextChannel, text = 'cows! count \'em') {
|
||||
this.channel = channel;
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.msg = await this.channel.send(this.text);
|
||||
|
||||
const icons = ['🐮', '🐴', '🐄'];
|
||||
|
||||
const reactions: MessageReaction[] = [];
|
||||
|
||||
let i = 0;
|
||||
let isRemoving = false;
|
||||
|
||||
this.isStopped = false;
|
||||
|
||||
(async () => {
|
||||
await delay(INITAL_DELAY);
|
||||
|
||||
while (!this.isStopped) {
|
||||
if (reactions.length === icons.length) {
|
||||
isRemoving = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await delay(PERIOD);
|
||||
|
||||
if (isRemoving) {
|
||||
const reactionToRemove = reactions.shift();
|
||||
|
||||
if (reactionToRemove) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await reactionToRemove.users.remove(this.msg.client.user!.id);
|
||||
} else {
|
||||
isRemoving = false;
|
||||
}
|
||||
} else {
|
||||
if (!this.isStopped) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
reactions.push(await this.msg.react(icons[i % icons.length]));
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
async stop(str = 'u betcha'): Promise<Message> {
|
||||
const wasAlreadyStopped = this.isStopped;
|
||||
|
||||
this.isStopped = true;
|
||||
|
||||
const editPromise = str ? this.msg.edit(str) : null;
|
||||
const reactPromise = str && !wasAlreadyStopped ? (async () => {
|
||||
await this.msg.fetch();
|
||||
await Promise.all(this.msg.reactions.cache.map(async react => {
|
||||
if (react.me) {
|
||||
await react.users.remove(this.msg.client.user!.id);
|
||||
}
|
||||
}));
|
||||
})() : null;
|
||||
|
||||
await Promise.all([editPromise, reactPromise]);
|
||||
|
||||
return this.msg;
|
||||
}
|
||||
}
|
2
src/utils/string.ts
Normal file
2
src/utils/string.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const truncate = (text: string, maxLength = 50) =>
|
||||
text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text;
|
82
yarn.lock
82
yarn.lock
|
@ -22,9 +22,9 @@
|
|||
integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==
|
||||
|
||||
"@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.7.tgz#81a01d7d675046f0d96f82450d9d9578bdfd6b0b"
|
||||
integrity sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==
|
||||
version "7.16.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88"
|
||||
integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.16.7"
|
||||
chalk "^2.0.0"
|
||||
|
@ -251,15 +251,15 @@
|
|||
once "^1.4.0"
|
||||
|
||||
"@octokit/request@^5.6.0":
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.2.tgz#1aa74d5da7b9e04ac60ef232edd9a7438dcf32d8"
|
||||
integrity sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA==
|
||||
version "5.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0"
|
||||
integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^6.0.1"
|
||||
"@octokit/request-error" "^2.1.0"
|
||||
"@octokit/types" "^6.16.1"
|
||||
is-plain-object "^5.0.0"
|
||||
node-fetch "^2.6.1"
|
||||
node-fetch "^2.6.7"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/rest@18.12.0":
|
||||
|
@ -319,9 +319,9 @@
|
|||
integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
|
||||
|
||||
"@sindresorhus/is@^4.0.0", "@sindresorhus/is@^4.2.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.3.0.tgz#344fd9bf808a84567ba563d00cc54b2f428dbab1"
|
||||
integrity sha512-wwOvh0eO3PiTEivGJWiZ+b946SlMSb4pe+y+Ur/4S87cwo09pYi+FWHHnbrM3W9W7cBYKDqQXcrFYjYUCOJUEQ==
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.4.0.tgz#e277e5bdbdf7cb1e20d320f02f5e2ed113cd3185"
|
||||
integrity sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ==
|
||||
|
||||
"@szmarczak/http-timer@^1.1.2":
|
||||
version "1.1.2"
|
||||
|
@ -441,9 +441,9 @@
|
|||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*", "@types/node@^17.0.0":
|
||||
version "17.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.10.tgz#616f16e9d3a2a3d618136b1be244315d95bd7cab"
|
||||
integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==
|
||||
version "17.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.12.tgz#f7aa331b27f08244888c47b7df126184bc2339c5"
|
||||
integrity sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
|
@ -1178,14 +1178,6 @@ delegates@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
deprecated-obj@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/deprecated-obj/-/deprecated-obj-2.0.0.tgz#e6ba93a3989f6ed18d685e7d99fb8d469b4beffc"
|
||||
integrity sha512-CkdywZC2rJ8RGh+y3MM1fw1EJ4oO/oNExGbRFv0AQoMS+faTd3nO7slYjkj/6t8OnIMUE+wxh6G97YHhK1ytrw==
|
||||
dependencies:
|
||||
flat "^5.0.2"
|
||||
lodash "^4.17.20"
|
||||
|
||||
deprecation@^2.0.0, deprecation@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
|
||||
|
@ -1599,11 +1591,6 @@ flat-cache@^3.0.4:
|
|||
flatted "^3.1.0"
|
||||
rimraf "^3.0.2"
|
||||
|
||||
flat@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241"
|
||||
integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==
|
||||
|
||||
flatted@^3.1.0:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.4.tgz#28d9969ea90661b5134259f312ab6aa7929ac5e2"
|
||||
|
@ -2110,7 +2097,7 @@ is-ci@^2.0.0:
|
|||
dependencies:
|
||||
ci-info "^2.0.0"
|
||||
|
||||
is-core-module@^2.8.0:
|
||||
is-core-module@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
|
||||
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
|
||||
|
@ -2342,7 +2329,7 @@ lodash.truncate@^4.4.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||
|
||||
lodash@4.17.21, lodash@^4.17.20, lodash@^4.17.21:
|
||||
lodash@4.17.21, lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
@ -2540,9 +2527,9 @@ new-github-release-url@1.0.0:
|
|||
type-fest "^0.4.1"
|
||||
|
||||
node-addon-api@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87"
|
||||
integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
|
||||
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
|
||||
|
||||
node-emoji@^1.10.0:
|
||||
version "1.11.0"
|
||||
|
@ -2551,7 +2538,7 @@ node-emoji@^1.10.0:
|
|||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.5:
|
||||
node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
|
@ -3039,9 +3026,9 @@ registry-url@^5.0.0:
|
|||
rc "^1.2.8"
|
||||
|
||||
release-it@^14.11.8:
|
||||
version "14.12.3"
|
||||
resolved "https://registry.yarnpkg.com/release-it/-/release-it-14.12.3.tgz#634c91796eba38c7113c095e1dff218b0e4542e7"
|
||||
integrity sha512-qek7ml9WaxpXSjLpU4UGCF9nWpDgOODL1gZTuydafs1HdQPAeYOd2od8I8lUL4NlEKW2TirDhH4aFTVIpP3/cQ==
|
||||
version "14.12.4"
|
||||
resolved "https://registry.yarnpkg.com/release-it/-/release-it-14.12.4.tgz#0fd13de85e382323c634a0697a601437e042123a"
|
||||
integrity sha512-lqf9PMsj7ycCqFHGag8Uv7cE1hNsKa+yKUMe+Fkh9fdOfxu2F01On+YUefRCP0DuQthmr/WyLCYdrjThMEkWFQ==
|
||||
dependencies:
|
||||
"@iarna/toml" "2.2.5"
|
||||
"@octokit/rest" "18.12.0"
|
||||
|
@ -3049,7 +3036,6 @@ release-it@^14.11.8:
|
|||
chalk "4.1.2"
|
||||
cosmiconfig "7.0.1"
|
||||
debug "4.3.3"
|
||||
deprecated-obj "2.0.0"
|
||||
execa "5.1.1"
|
||||
form-data "4.0.0"
|
||||
git-url-parse "11.6.0"
|
||||
|
@ -3099,11 +3085,11 @@ resolve-from@^5.0.0:
|
|||
integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
|
||||
|
||||
resolve@^1.1.6:
|
||||
version "1.21.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f"
|
||||
integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
|
||||
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
|
||||
dependencies:
|
||||
is-core-module "^2.8.0"
|
||||
is-core-module "^2.8.1"
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
|
@ -3550,9 +3536,9 @@ type-fest@^0.8.0:
|
|||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-fest@^2.8.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.9.0.tgz#7a2d430dd966f52b6bc723da2aaa2c9867530551"
|
||||
integrity sha512-uC0hJKi7eAGXUJ/YKk53RhnKxMwzHWgzf4t92oz8Qez28EBgVTfpDTB59y9hMYLzc/Wl85cD7Tv1hLZZoEJtrg==
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.10.0.tgz#0ee9c3cd411efb3bba434065d32235592d3644cf"
|
||||
integrity sha512-u2yreDMllFI3VCpWt0rKrGs/E2LO0YHBwiiOIj+ilQh9+ALMaa4lNBSdoDvuHN3cbKcYk9L1BXP49x9RT+o/SA==
|
||||
|
||||
typed-emitter@^1.4.0:
|
||||
version "1.4.0"
|
||||
|
@ -3567,9 +3553,9 @@ typedarray-to-buffer@^3.1.5:
|
|||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@>=4.3, typescript@^4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
|
||||
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
|
||||
version "4.5.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
|
||||
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
|
||||
|
||||
undefsafe@^2.0.5:
|
||||
version "2.0.5"
|
||||
|
@ -3802,7 +3788,7 @@ youtube.ts@^0.2.5:
|
|||
axios "^0.19.0"
|
||||
ytdl-core "^4.9.1"
|
||||
|
||||
ytdl-core@^4.9.1, ytdl-core@^4.9.2:
|
||||
ytdl-core@^4.10.0, ytdl-core@^4.9.1:
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.10.0.tgz#0835cb411677684539fac2bcc10553f6f58db3e1"
|
||||
integrity sha512-RCCoSVTmMeBPH5NFR1fh3nkDU9okvWM0ZdN6plw6I5+vBBZVUEpOt8vjbSgprLRMmGUsmrQZJhvG1CHOat4mLA==
|
||||
|
@ -3811,7 +3797,7 @@ ytdl-core@^4.9.1, ytdl-core@^4.9.2:
|
|||
miniget "^4.0.0"
|
||||
sax "^1.1.3"
|
||||
|
||||
ytsr@^3.5.3:
|
||||
ytsr@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/ytsr/-/ytsr-3.6.0.tgz#bc55e8957dcc293e49e18cc3b3e6d2890d15a15e"
|
||||
integrity sha512-3fN8lxL+JHtp2xEZoAK3AeTjNm5WB4MH6n2OxHNxP06xQtuO5khbLwh6IJGiZRNi/v3de+jYYbctp2pUqNT/Qw==
|
||||
|
|
Loading…
Reference in a new issue