mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-23 09:15:29 +01:00
Add /favorites
This commit is contained in:
parent
1f59994dc4
commit
8e00726dc2
|
@ -0,0 +1,10 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FavoriteQuery" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"guildId" TEXT NOT NULL,
|
||||||
|
"authorId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"query" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
|
@ -0,0 +1,10 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `Shortcut` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropTable
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
DROP TABLE "Shortcut";
|
||||||
|
PRAGMA foreign_keys=on;
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[name]` on the table `FavoriteQuery` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FavoriteQuery_name_key" ON "FavoriteQuery"("name");
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[guildId,name]` on the table `FavoriteQuery` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "FavoriteQuery_name_key";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "FavoriteQuery_guildId_name_key" ON "FavoriteQuery"("guildId", "name");
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `finishedSetup` on the `Setting` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `prefix` on the `Setting` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Setting" (
|
||||||
|
"guildId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"channel" TEXT,
|
||||||
|
"playlistLimit" INTEGER NOT NULL DEFAULT 50,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Setting" ("channel", "createdAt", "guildId", "playlistLimit", "updatedAt") SELECT "channel", "createdAt", "guildId", "playlistLimit", "updatedAt" FROM "Setting";
|
||||||
|
DROP TABLE "Setting";
|
||||||
|
ALTER TABLE "new_Setting" RENAME TO "Setting";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -52,7 +52,7 @@
|
||||||
"eslint-config-xo-typescript": "^0.44.0",
|
"eslint-config-xo-typescript": "^0.44.0",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"nodemon": "^2.0.7",
|
"nodemon": "^2.0.7",
|
||||||
"prisma": "^3.7.0",
|
"prisma": "^3.8.1",
|
||||||
"release-it": "^14.11.8",
|
"release-it": "^14.11.8",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"type-fest": "^2.8.0",
|
"type-fest": "^2.8.0",
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
"@discordjs/opus": "^0.7.0",
|
"@discordjs/opus": "^0.7.0",
|
||||||
"@discordjs/rest": "^0.1.0-canary.0",
|
"@discordjs/rest": "^0.1.0-canary.0",
|
||||||
"@discordjs/voice": "^0.7.5",
|
"@discordjs/voice": "^0.7.5",
|
||||||
"@prisma/client": "^3.7.0",
|
"@prisma/client": "^3.8.1",
|
||||||
"@types/libsodium-wrappers": "^0.7.9",
|
"@types/libsodium-wrappers": "^0.7.9",
|
||||||
"array-shuffle": "^3.0.0",
|
"array-shuffle": "^3.0.0",
|
||||||
"debug": "^4.3.3",
|
"debug": "^4.3.3",
|
||||||
|
|
|
@ -25,25 +25,20 @@ model KeyValueCache {
|
||||||
|
|
||||||
model Setting {
|
model Setting {
|
||||||
guildId String @id
|
guildId String @id
|
||||||
prefix String
|
|
||||||
channel String?
|
channel String?
|
||||||
finishedSetup Boolean @default(false)
|
|
||||||
playlistLimit Int @default(50)
|
playlistLimit Int @default(50)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Shortcut {
|
model FavoriteQuery {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
guildId String
|
guildId String
|
||||||
authorId String
|
authorId String
|
||||||
shortcut String
|
name String
|
||||||
command String
|
query String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([guildId, name])
|
||||||
@@index([shortcut], map: "shortcuts_shortcut")
|
|
||||||
@@index([guildId], map: "shortcuts_guild_id")
|
|
||||||
@@index([guildId, shortcut])
|
|
||||||
}
|
}
|
||||||
|
|
30
src/bot.ts
30
src/bot.ts
|
@ -35,16 +35,25 @@ export default class {
|
||||||
|
|
||||||
public async register(): Promise<void> {
|
public async register(): Promise<void> {
|
||||||
// Load in commands
|
// Load in commands
|
||||||
container.getAll<Command>(TYPES.Command).forEach(command => {
|
for (const command of container.getAll<Command>(TYPES.Command)) {
|
||||||
// TODO: remove !
|
// Make sure we can serialize to JSON without errors
|
||||||
if (command.slashCommand?.name) {
|
try {
|
||||||
|
command.slashCommand.toJSON();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw new Error(`Could not serialize /${command.slashCommand.name ?? ''} to JSON`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.slashCommand.name) {
|
||||||
this.commandsByName.set(command.slashCommand.name, command);
|
this.commandsByName.set(command.slashCommand.name, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.handledButtonIds) {
|
if (command.handledButtonIds) {
|
||||||
command.handledButtonIds.forEach(id => this.commandsByButtonId.set(id, command));
|
for (const buttonId of command.handledButtonIds) {
|
||||||
|
this.commandsByButtonId.set(buttonId, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Register event handlers
|
// Register event handlers
|
||||||
this.client.on('interactionCreate', async interaction => {
|
this.client.on('interactionCreate', async interaction => {
|
||||||
|
@ -61,7 +70,9 @@ export default class {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
|
const requiresVC = command.requiresVC instanceof Function ? command.requiresVC(interaction) : command.requiresVC;
|
||||||
|
|
||||||
|
if (requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
|
||||||
await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
|
await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -122,13 +133,16 @@ export default class {
|
||||||
} else {
|
} else {
|
||||||
spinner.text = '📡 updating commands in all guilds...';
|
spinner.text = '📡 updating commands in all guilds...';
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all([
|
||||||
this.client.guilds.cache.map(async guild => {
|
...this.client.guilds.cache.map(async guild => {
|
||||||
await rest.put(
|
await rest.put(
|
||||||
Routes.applicationGuildCommands(this.client.user!.id, guild.id),
|
Routes.applicationGuildCommands(this.client.user!.id, guild.id),
|
||||||
{body: this.commandsByName.map(command => command.slashCommand.toJSON())},
|
{body: this.commandsByName.map(command => command.slashCommand.toJSON())},
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
// Remove commands registered on bot (if they exist)
|
||||||
|
rest.put(Routes.applicationCommands(this.client.user!.id), {body: []}),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
191
src/commands/favorites.ts
Normal file
191
src/commands/favorites.ts
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||||
|
import {AutocompleteInteraction, CommandInteraction, MessageEmbed} from 'discord.js';
|
||||||
|
import {inject, injectable} from 'inversify';
|
||||||
|
import Command from '.';
|
||||||
|
import AddQueryToQueue from '../services/add-query-to-queue.js';
|
||||||
|
import {TYPES} from '../types.js';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export default class implements Command {
|
||||||
|
public readonly slashCommand = new SlashCommandBuilder()
|
||||||
|
.setName('favorites')
|
||||||
|
.setDescription('adds a song to your favorites')
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName('use')
|
||||||
|
.setDescription('use a favorite')
|
||||||
|
.addStringOption(option => option
|
||||||
|
.setName('name')
|
||||||
|
.setDescription('name of favorite')
|
||||||
|
.setRequired(true)
|
||||||
|
.setAutocomplete(true))
|
||||||
|
.addBooleanOption(option => option
|
||||||
|
.setName('immediate')
|
||||||
|
.setDescription('add track to the front of the queue'))
|
||||||
|
.addBooleanOption(option => option
|
||||||
|
.setName('shuffle')
|
||||||
|
.setDescription('shuffle the input if you\'re adding multiple tracks')))
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName('list')
|
||||||
|
.setDescription('list all favorites'))
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName('create')
|
||||||
|
.setDescription('create a new favorite')
|
||||||
|
.addStringOption(option => option
|
||||||
|
.setName('name')
|
||||||
|
.setDescription('you\'ll type this when using this favorite')
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(option => option
|
||||||
|
.setName('query')
|
||||||
|
.setDescription('any input you\'d normally give to the play command')
|
||||||
|
.setRequired(true),
|
||||||
|
))
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName('remove')
|
||||||
|
.setDescription('remove a favorite')
|
||||||
|
.addStringOption(option => option
|
||||||
|
.setName('name')
|
||||||
|
.setDescription('name of favorite')
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.setRequired(true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(@inject(TYPES.Services.AddQueryToQueue) private readonly addQueryToQueue: AddQueryToQueue) {}
|
||||||
|
|
||||||
|
requiresVC = (interaction: CommandInteraction) => interaction.options.getSubcommand() === 'use';
|
||||||
|
|
||||||
|
async execute(interaction: CommandInteraction) {
|
||||||
|
switch (interaction.options.getSubcommand()) {
|
||||||
|
case 'use':
|
||||||
|
await this.use(interaction);
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
await this.list(interaction);
|
||||||
|
break;
|
||||||
|
case 'create':
|
||||||
|
await this.create(interaction);
|
||||||
|
break;
|
||||||
|
case 'remove':
|
||||||
|
await this.remove(interaction);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('unknown subcommand');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAutocompleteInteraction(interaction: AutocompleteInteraction) {
|
||||||
|
const query = interaction.options.getString('name')!.trim();
|
||||||
|
|
||||||
|
const favorites = await prisma.favoriteQuery.findMany({
|
||||||
|
where: {
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const names = favorites.map(favorite => favorite.name);
|
||||||
|
|
||||||
|
const results = query === '' ? names : names.filter(name => name.startsWith(query));
|
||||||
|
|
||||||
|
await interaction.respond(results.map(r => ({
|
||||||
|
name: r,
|
||||||
|
value: r,
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async use(interaction: CommandInteraction) {
|
||||||
|
const name = interaction.options.getString('name')!.trim();
|
||||||
|
|
||||||
|
const favorite = await prisma.favoriteQuery.findFirst({
|
||||||
|
where: {
|
||||||
|
name,
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!favorite) {
|
||||||
|
throw new Error('no favorite with that name exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.addQueryToQueue.addToQueue({
|
||||||
|
interaction,
|
||||||
|
query: favorite.query,
|
||||||
|
shuffleAdditions: interaction.options.getBoolean('shuffle') ?? false,
|
||||||
|
addToFrontOfQueue: interaction.options.getBoolean('immediate') ?? false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async list(interaction: CommandInteraction) {
|
||||||
|
const favorites = await prisma.favoriteQuery.findMany({
|
||||||
|
where: {
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (favorites.length === 0) {
|
||||||
|
await interaction.reply('there aren\'t any favorites yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new MessageEmbed().setTitle('Favorites');
|
||||||
|
|
||||||
|
let description = '';
|
||||||
|
for (const favorite of favorites) {
|
||||||
|
description += `**${favorite.name}**: ${favorite.query} (<@${favorite.authorId}>)\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.setDescription(description);
|
||||||
|
|
||||||
|
await interaction.reply({
|
||||||
|
embeds: [embed],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async create(interaction: CommandInteraction) {
|
||||||
|
const name = interaction.options.getString('name')!.trim();
|
||||||
|
const query = interaction.options.getString('query')!.trim();
|
||||||
|
|
||||||
|
const existingFavorite = await prisma.favoriteQuery.findFirst({where: {
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
name,
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (existingFavorite) {
|
||||||
|
throw new Error('a favorite with that name already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.favoriteQuery.create({
|
||||||
|
data: {
|
||||||
|
authorId: interaction.member!.user.id,
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
name,
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply('👍 favorite created');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async remove(interaction: CommandInteraction) {
|
||||||
|
const name = interaction.options.getString('name')!.trim();
|
||||||
|
|
||||||
|
const favorite = await prisma.favoriteQuery.findFirst({where: {
|
||||||
|
name,
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
}});
|
||||||
|
|
||||||
|
if (!favorite) {
|
||||||
|
throw new Error('no favorite with that name exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserGuildOwner = interaction.member!.user.id === interaction.guild!.ownerId;
|
||||||
|
|
||||||
|
if (favorite.authorId !== interaction.member!.user.id && !isUserGuildOwner) {
|
||||||
|
throw new Error('you can only remove your own favorites');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.favoriteQuery.delete({where: {id: favorite.id}});
|
||||||
|
|
||||||
|
await interaction.reply('👍 favorite removed');
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
import {SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder} from '@discordjs/builders';
|
||||||
import {AutocompleteInteraction, ButtonInteraction, CommandInteraction} from 'discord.js';
|
import {AutocompleteInteraction, ButtonInteraction, CommandInteraction} from 'discord.js';
|
||||||
|
|
||||||
export default interface Command {
|
export default interface Command {
|
||||||
readonly slashCommand: Partial<SlashCommandBuilder> & Pick<SlashCommandBuilder, 'toJSON'>;
|
readonly slashCommand: Partial<SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder> & Pick<SlashCommandBuilder, 'toJSON'>;
|
||||||
readonly handledButtonIds?: readonly string[];
|
readonly handledButtonIds?: readonly string[];
|
||||||
readonly requiresVC?: boolean;
|
readonly requiresVC?: boolean | ((interaction: CommandInteraction) => boolean);
|
||||||
execute: (interaction: CommandInteraction) => Promise<void>;
|
execute: (interaction: CommandInteraction) => Promise<void>;
|
||||||
handleButtonInteraction?: (interaction: ButtonInteraction) => Promise<void>;
|
handleButtonInteraction?: (interaction: ButtonInteraction) => Promise<void>;
|
||||||
handleAutocompleteInteraction?: (interaction: AutocompleteInteraction) => Promise<void>;
|
handleAutocompleteInteraction?: (interaction: AutocompleteInteraction) => Promise<void>;
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import {AutocompleteInteraction, CommandInteraction, GuildMember} from 'discord.js';
|
import {AutocompleteInteraction, CommandInteraction} from 'discord.js';
|
||||||
import {URL} from 'url';
|
import {URL} from 'url';
|
||||||
import {Except} from 'type-fest';
|
|
||||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||||
import shuffle from 'array-shuffle';
|
|
||||||
import {inject, injectable} from 'inversify';
|
import {inject, injectable} from 'inversify';
|
||||||
import Spotify from 'spotify-web-api-node';
|
import Spotify from 'spotify-web-api-node';
|
||||||
import Command from '.';
|
import Command from '.';
|
||||||
import {TYPES} from '../types.js';
|
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 GetSongs from '../services/get-songs.js';
|
|
||||||
import {prisma} from '../utils/db.js';
|
|
||||||
import ThirdParty from '../services/third-party.js';
|
import ThirdParty from '../services/third-party.js';
|
||||||
import getYouTubeAndSpotifySuggestionsFor from '../utils/get-youtube-and-spotify-suggestions-for.js';
|
import getYouTubeAndSpotifySuggestionsFor from '../utils/get-youtube-and-spotify-suggestions-for.js';
|
||||||
import KeyValueCacheProvider from '../services/key-value-cache.js';
|
import KeyValueCacheProvider from '../services/key-value-cache.js';
|
||||||
import {ONE_HOUR_IN_SECONDS} from '../utils/constants.js';
|
import {ONE_HOUR_IN_SECONDS} from '../utils/constants.js';
|
||||||
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
import AddQueryToQueue from '../services/add-query-to-queue.js';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export default class implements Command {
|
export default class implements Command {
|
||||||
|
@ -30,178 +23,31 @@ export default class implements Command {
|
||||||
.setAutocomplete(true))
|
.setAutocomplete(true))
|
||||||
.addBooleanOption(option => option
|
.addBooleanOption(option => option
|
||||||
.setName('immediate')
|
.setName('immediate')
|
||||||
.setDescription('adds track to the front of the queue'))
|
.setDescription('add track to the front of the queue'))
|
||||||
.addBooleanOption(option => option
|
.addBooleanOption(option => option
|
||||||
.setName('shuffle')
|
.setName('shuffle')
|
||||||
.setDescription('shuffles the input if it\'s a playlist'));
|
.setDescription('shuffle the input if you\'re adding multiple tracks'));
|
||||||
|
|
||||||
public requiresVC = true;
|
public requiresVC = true;
|
||||||
|
|
||||||
private readonly playerManager: PlayerManager;
|
|
||||||
private readonly getSongs: GetSongs;
|
|
||||||
private readonly spotify: Spotify;
|
private readonly spotify: Spotify;
|
||||||
private readonly cache: KeyValueCacheProvider;
|
private readonly cache: KeyValueCacheProvider;
|
||||||
|
private readonly addQueryToQueue: AddQueryToQueue;
|
||||||
|
|
||||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Services.GetSongs) getSongs: GetSongs, @inject(TYPES.ThirdParty) thirdParty: ThirdParty, @inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider) {
|
constructor(@inject(TYPES.ThirdParty) thirdParty: ThirdParty, @inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider, @inject(TYPES.Services.AddQueryToQueue) addQueryToQueue: AddQueryToQueue) {
|
||||||
this.playerManager = playerManager;
|
|
||||||
this.getSongs = getSongs;
|
|
||||||
this.spotify = thirdParty.spotify;
|
this.spotify = thirdParty.spotify;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
|
this.addQueryToQueue = addQueryToQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
public async execute(interaction: CommandInteraction): Promise<void> {
|
public async execute(interaction: CommandInteraction): Promise<void> {
|
||||||
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
|
await this.addQueryToQueue.addToQueue({
|
||||||
|
interaction,
|
||||||
const settings = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}});
|
query: interaction.options.getString('query')!.trim(),
|
||||||
|
addToFrontOfQueue: interaction.options.getBoolean('immediate') ?? false,
|
||||||
if (!settings) {
|
shuffleAdditions: interaction.options.getBoolean('shuffle') ?? false,
|
||||||
throw new Error('Could not find settings for guild');
|
|
||||||
}
|
|
||||||
|
|
||||||
const {playlistLimit} = settings;
|
|
||||||
|
|
||||||
const player = this.playerManager.get(interaction.guild!.id);
|
|
||||||
const wasPlayingSong = player.getCurrent() !== null;
|
|
||||||
|
|
||||||
const query = interaction.options.getString('query');
|
|
||||||
|
|
||||||
if (!query) {
|
|
||||||
if (player.status === STATUS.PLAYING) {
|
|
||||||
throw new Error('already playing, give me a song name');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be resuming play
|
|
||||||
if (!wasPlayingSong) {
|
|
||||||
throw new Error('nothing to play');
|
|
||||||
}
|
|
||||||
|
|
||||||
await player.connect(targetVoiceChannel);
|
|
||||||
await player.play();
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const YOUTUBE_HOSTS = [
|
|
||||||
'www.youtube.com',
|
|
||||||
'youtu.be',
|
|
||||||
'youtube.com',
|
|
||||||
'music.youtube.com',
|
|
||||||
'www.music.youtube.com',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (YOUTUBE_HOSTS.includes(url.host)) {
|
|
||||||
// YouTube source
|
|
||||||
if (url.searchParams.get('list')) {
|
|
||||||
// YouTube playlist
|
|
||||||
newSongs.push(...await this.getSongs.youtubePlaylist(url.searchParams.get('list')!));
|
|
||||||
} else {
|
|
||||||
const song = await this.getSongs.youtubeVideo(url.href);
|
|
||||||
|
|
||||||
if (song) {
|
|
||||||
newSongs.push(song);
|
|
||||||
} else {
|
|
||||||
throw new Error('that doesn\'t exist');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
|
|
||||||
const [convertedSongs, nSongsNotFound, totalSongs] = await this.getSongs.spotifySource(query, playlistLimit);
|
|
||||||
|
|
||||||
if (totalSongs > playlistLimit) {
|
|
||||||
extraMsg = `a random sample of ${playlistLimit} songs was taken`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalSongs > playlistLimit && nSongsNotFound !== 0) {
|
|
||||||
extraMsg += ' and ';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nSongsNotFound !== 0) {
|
|
||||||
if (nSongsNotFound === 1) {
|
|
||||||
extraMsg += '1 song was not found';
|
|
||||||
} else {
|
|
||||||
extraMsg += `${nSongsNotFound.toString()} songs were not found`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newSongs.push(...convertedSongs);
|
|
||||||
}
|
|
||||||
} catch (_: unknown) {
|
|
||||||
// Not a URL, must search YouTube
|
|
||||||
const song = await this.getSongs.youtubeVideoSearch(query);
|
|
||||||
|
|
||||||
if (song) {
|
|
||||||
newSongs.push(song);
|
|
||||||
} else {
|
|
||||||
throw new Error('that doesn\'t exist');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSongs.length === 0) {
|
|
||||||
throw new Error('no songs found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shuffleAdditions) {
|
|
||||||
newSongs = shuffle(newSongs);
|
|
||||||
}
|
|
||||||
|
|
||||||
newSongs.forEach(song => {
|
|
||||||
player.add({...song, addedInChannelId: interaction.channel!.id, requestedBy: interaction.member!.user.id}, {immediate: addToFrontOfQueue ?? false});
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstSong = newSongs[0];
|
|
||||||
|
|
||||||
let statusMsg = '';
|
|
||||||
|
|
||||||
if (player.voiceConnection === null) {
|
|
||||||
await player.connect(targetVoiceChannel);
|
|
||||||
|
|
||||||
// Resume / start playback
|
|
||||||
await player.play();
|
|
||||||
|
|
||||||
if (wasPlayingSong) {
|
|
||||||
statusMsg = 'resuming playback';
|
|
||||||
}
|
|
||||||
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [buildPlayingMessageEmbed(player)],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build response message
|
|
||||||
if (statusMsg !== '') {
|
|
||||||
if (extraMsg === '') {
|
|
||||||
extraMsg = statusMsg;
|
|
||||||
} else {
|
|
||||||
extraMsg = `${statusMsg}, ${extraMsg}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraMsg !== '') {
|
|
||||||
extraMsg = ` (${extraMsg})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSongs.length === 1) {
|
|
||||||
await interaction.editReply(`u betcha, **${firstSong.title}** added to the${addToFrontOfQueue ? ' front of the' : ''} queue${extraMsg}`);
|
|
||||||
} else {
|
|
||||||
await interaction.editReply(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleAutocompleteInteraction(interaction: AutocompleteInteraction): Promise<void> {
|
public async handleAutocompleteInteraction(interaction: AutocompleteInteraction): Promise<void> {
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
import {Guild, TextChannel, Message, MessageReaction, User, ApplicationCommandData,
|
import {Guild, Client} from 'discord.js';
|
||||||
} from 'discord.js';
|
|
||||||
import emoji from 'node-emoji';
|
|
||||||
import pEvent from 'p-event';
|
|
||||||
import {chunk} from '../utils/arrays.js';
|
|
||||||
import container from '../inversify.config.js';
|
import container from '../inversify.config.js';
|
||||||
import Command from '../commands';
|
import Command from '../commands';
|
||||||
import {TYPES} from '../types.js';
|
import {TYPES} from '../types.js';
|
||||||
import Config from '../services/config.js';
|
import Config from '../services/config.js';
|
||||||
import {prisma} from '../utils/db.js';
|
import {prisma} from '../utils/db.js';
|
||||||
|
import {REST} from '@discordjs/rest';
|
||||||
const DEFAULT_PREFIX = '!';
|
import {Routes} from 'discord-api-types/v9';
|
||||||
|
|
||||||
export default async (guild: Guild): Promise<void> => {
|
export default async (guild: Guild): Promise<void> => {
|
||||||
await prisma.setting.upsert({
|
await prisma.setting.upsert({
|
||||||
|
@ -18,99 +14,22 @@ export default async (guild: Guild): Promise<void> => {
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
guildId: guild.id,
|
guildId: guild.id,
|
||||||
prefix: DEFAULT_PREFIX,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
prefix: DEFAULT_PREFIX,
|
|
||||||
},
|
},
|
||||||
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = container.get<Config>(TYPES.Config);
|
const config = container.get<Config>(TYPES.Config);
|
||||||
|
|
||||||
// Setup slash commands
|
// Setup slash commands
|
||||||
if (!config.REGISTER_COMMANDS_ON_BOT) {
|
if (!config.REGISTER_COMMANDS_ON_BOT) {
|
||||||
const commands: ApplicationCommandData[] = container.getAll<Command>(TYPES.Command)
|
const token = container.get<Config>(TYPES.Config).DISCORD_TOKEN;
|
||||||
.filter(command => command.slashCommand?.name)
|
const client = container.get<Client>(TYPES.Client);
|
||||||
.map(command => command.slashCommand as ApplicationCommandData);
|
|
||||||
|
|
||||||
await guild.commands.set(commands);
|
const rest = new REST({version: '9'}).setToken(token);
|
||||||
|
|
||||||
|
await rest.put(
|
||||||
|
Routes.applicationGuildCommands(client.user!.id, guild.id),
|
||||||
|
{body: container.getAll<Command>(TYPES.Command).map(command => command.slashCommand.toJSON())},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const owner = await guild.client.users.fetch(guild.ownerId);
|
|
||||||
|
|
||||||
let firstStep = '👋 Hi!\n';
|
|
||||||
firstStep += 'I just need to ask a few questions before you start listening to music.\n\n';
|
|
||||||
firstStep += 'First, what channel should I listen to for music commands?\n\n';
|
|
||||||
|
|
||||||
const firstStepMsg = await owner.send(firstStep);
|
|
||||||
|
|
||||||
// Show emoji selector
|
|
||||||
interface EmojiChannel {
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
emoji: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiChannels: EmojiChannel[] = [];
|
|
||||||
|
|
||||||
for (const [channelId, channel] of guild.channels.cache) {
|
|
||||||
if (channel.type === 'GUILD_TEXT') {
|
|
||||||
emojiChannels.push({
|
|
||||||
name: channel.name,
|
|
||||||
id: channelId,
|
|
||||||
emoji: emoji.random().emoji,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentMessageIds: string[] = [];
|
|
||||||
|
|
||||||
chunk(emojiChannels, 10).map(async chunk => {
|
|
||||||
let str = '';
|
|
||||||
for (const channel of chunk) {
|
|
||||||
str += `${channel.emoji}: #${channel.name}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const msg = await owner.send(str);
|
|
||||||
|
|
||||||
sentMessageIds.push(msg.id);
|
|
||||||
|
|
||||||
await Promise.all(chunk.map(async channel => msg.react(channel.emoji)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for response from user
|
|
||||||
const [choice] = await pEvent(guild.client, 'messageReactionAdd', {
|
|
||||||
multiArgs: true,
|
|
||||||
filter: ([reaction, user]: [MessageReaction, User]) => sentMessageIds.includes(reaction.message.id) && user.id === owner.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const chosenChannel = emojiChannels.find(e => e.emoji === (choice as unknown as MessageReaction).emoji.name)!;
|
|
||||||
|
|
||||||
// Second setup step (get prefix)
|
|
||||||
let secondStep = `👍 Cool, I'll listen to **#${chosenChannel.name}** \n\n`;
|
|
||||||
secondStep += 'Last question: what character should I use for a prefix? Type a single character and hit enter.';
|
|
||||||
|
|
||||||
await owner.send(secondStep);
|
|
||||||
|
|
||||||
const prefixResponses = await firstStepMsg.channel.awaitMessages({filter: (r: Message) => r.content.length === 1, max: 1});
|
|
||||||
|
|
||||||
const prefixCharacter = prefixResponses.first()!.content;
|
|
||||||
|
|
||||||
// Save settings
|
|
||||||
await prisma.setting.update({
|
|
||||||
where: {
|
|
||||||
guildId: guild.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
channel: chosenChannel.id,
|
|
||||||
prefix: prefixCharacter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send welcome
|
|
||||||
const boundChannel = guild.client.channels.cache.get(chosenChannel.id) as TextChannel;
|
|
||||||
|
|
||||||
await boundChannel.send(`hey <@${owner.id}> try \`\\play https://www.youtube.com/watch?v=dQw4w9WgXcQ\``);
|
|
||||||
|
|
||||||
await firstStepMsg.channel.send(`Sounds good. Check out **#${chosenChannel.name}** to get started.`);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,13 +8,15 @@ import ConfigProvider from './services/config.js';
|
||||||
// Managers
|
// Managers
|
||||||
import PlayerManager from './managers/player.js';
|
import PlayerManager from './managers/player.js';
|
||||||
|
|
||||||
// Helpers
|
// Services
|
||||||
|
import AddQueryToQueue from './services/add-query-to-queue.js';
|
||||||
import GetSongs from './services/get-songs.js';
|
import GetSongs from './services/get-songs.js';
|
||||||
|
|
||||||
// Comands
|
// Comands
|
||||||
import Command from './commands';
|
import Command from './commands';
|
||||||
import Clear from './commands/clear.js';
|
import Clear from './commands/clear.js';
|
||||||
import Disconnect from './commands/disconnect.js';
|
import Disconnect from './commands/disconnect.js';
|
||||||
|
import Favorites from './commands/favorites.js';
|
||||||
import ForwardSeek from './commands/fseek.js';
|
import ForwardSeek from './commands/fseek.js';
|
||||||
import Pause from './commands/pause.js';
|
import Pause from './commands/pause.js';
|
||||||
import Play from './commands/play.js';
|
import Play from './commands/play.js';
|
||||||
|
@ -45,13 +47,15 @@ container.bind<Client>(TYPES.Client).toConstantValue(new Client({intents}));
|
||||||
// Managers
|
// Managers
|
||||||
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
|
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
|
||||||
|
|
||||||
// Helpers
|
// Services
|
||||||
container.bind<GetSongs>(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope();
|
container.bind<GetSongs>(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope();
|
||||||
|
container.bind<AddQueryToQueue>(TYPES.Services.AddQueryToQueue).to(AddQueryToQueue).inSingletonScope();
|
||||||
|
|
||||||
// Commands
|
// Commands
|
||||||
[
|
[
|
||||||
Clear,
|
Clear,
|
||||||
Disconnect,
|
Disconnect,
|
||||||
|
Favorites,
|
||||||
ForwardSeek,
|
ForwardSeek,
|
||||||
Pause,
|
Pause,
|
||||||
Play,
|
Play,
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default class {
|
||||||
let player = this.guildPlayers.get(guildId);
|
let player = this.guildPlayers.get(guildId);
|
||||||
|
|
||||||
if (!player) {
|
if (!player) {
|
||||||
player = new Player(this.discordClient, this.fileCache);
|
player = new Player(this.discordClient, this.fileCache, guildId);
|
||||||
|
|
||||||
this.guildPlayers.set(guildId, player);
|
this.guildPlayers.set(guildId, player);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,14 @@ import {DATA_DIR} from '../services/config.js';
|
||||||
import createDatabaseUrl from '../utils/create-database-url.js';
|
import createDatabaseUrl from '../utils/create-database-url.js';
|
||||||
import {execa} from 'execa';
|
import {execa} from 'execa';
|
||||||
|
|
||||||
process.env.DATABASE_URL = createDatabaseUrl(DATA_DIR);
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await execa(process.argv[2], process.argv.slice(3), {
|
await execa(process.argv[2], process.argv.slice(3), {
|
||||||
preferLocal: true,
|
preferLocal: true,
|
||||||
stderr: process.stderr,
|
stderr: process.stderr,
|
||||||
stdout: process.stdout,
|
stdout: process.stdout,
|
||||||
|
stdin: process.stdin,
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: createDatabaseUrl(DATA_DIR),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
155
src/services/add-query-to-queue.ts
Normal file
155
src/services/add-query-to-queue.ts
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
import {CommandInteraction, GuildMember} from 'discord.js';
|
||||||
|
import {inject, injectable} from 'inversify';
|
||||||
|
import {Except} from 'type-fest';
|
||||||
|
import shuffle from 'array-shuffle';
|
||||||
|
import {TYPES} from '../types.js';
|
||||||
|
import GetSongs from '../services/get-songs.js';
|
||||||
|
import {QueuedSong} from './player.js';
|
||||||
|
import PlayerManager from '../managers/player.js';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
||||||
|
import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js';
|
||||||
|
|
||||||
|
@injectable()
|
||||||
|
export default class AddQueryToQueue {
|
||||||
|
constructor(@inject(TYPES.Services.GetSongs) private readonly getSongs: GetSongs, @inject(TYPES.Managers.Player) private readonly playerManager: PlayerManager) {}
|
||||||
|
|
||||||
|
public async addToQueue({
|
||||||
|
query,
|
||||||
|
addToFrontOfQueue,
|
||||||
|
shuffleAdditions,
|
||||||
|
interaction,
|
||||||
|
}: {
|
||||||
|
query: string;
|
||||||
|
addToFrontOfQueue: boolean;
|
||||||
|
shuffleAdditions: boolean;
|
||||||
|
interaction: CommandInteraction;
|
||||||
|
}): Promise<void> {
|
||||||
|
const guildId = interaction.guild!.id;
|
||||||
|
const player = this.playerManager.get(guildId);
|
||||||
|
const wasPlayingSong = player.getCurrent() !== null;
|
||||||
|
|
||||||
|
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
|
||||||
|
|
||||||
|
const settings = await prisma.setting.findUnique({where: {guildId}});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error('Could not find settings for guild');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {playlistLimit} = settings;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const YOUTUBE_HOSTS = [
|
||||||
|
'www.youtube.com',
|
||||||
|
'youtu.be',
|
||||||
|
'youtube.com',
|
||||||
|
'music.youtube.com',
|
||||||
|
'www.music.youtube.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (YOUTUBE_HOSTS.includes(url.host)) {
|
||||||
|
// YouTube source
|
||||||
|
if (url.searchParams.get('list')) {
|
||||||
|
// YouTube playlist
|
||||||
|
newSongs.push(...await this.getSongs.youtubePlaylist(url.searchParams.get('list')!));
|
||||||
|
} else {
|
||||||
|
const song = await this.getSongs.youtubeVideo(url.href);
|
||||||
|
|
||||||
|
if (song) {
|
||||||
|
newSongs.push(song);
|
||||||
|
} else {
|
||||||
|
throw new Error('that doesn\'t exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') {
|
||||||
|
const [convertedSongs, nSongsNotFound, totalSongs] = await this.getSongs.spotifySource(query, playlistLimit);
|
||||||
|
|
||||||
|
if (totalSongs > playlistLimit) {
|
||||||
|
extraMsg = `a random sample of ${playlistLimit} songs was taken`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSongs > playlistLimit && nSongsNotFound !== 0) {
|
||||||
|
extraMsg += ' and ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nSongsNotFound !== 0) {
|
||||||
|
if (nSongsNotFound === 1) {
|
||||||
|
extraMsg += '1 song was not found';
|
||||||
|
} else {
|
||||||
|
extraMsg += `${nSongsNotFound.toString()} songs were not found`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newSongs.push(...convertedSongs);
|
||||||
|
}
|
||||||
|
} catch (_: unknown) {
|
||||||
|
// Not a URL, must search YouTube
|
||||||
|
const song = await this.getSongs.youtubeVideoSearch(query);
|
||||||
|
|
||||||
|
if (song) {
|
||||||
|
newSongs.push(song);
|
||||||
|
} else {
|
||||||
|
throw new Error('that doesn\'t exist');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSongs.length === 0) {
|
||||||
|
throw new Error('no songs found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shuffleAdditions) {
|
||||||
|
newSongs = shuffle(newSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
newSongs.forEach(song => {
|
||||||
|
player.add({...song, addedInChannelId: interaction.channel!.id, requestedBy: interaction.member!.user.id}, {immediate: addToFrontOfQueue ?? false});
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstSong = newSongs[0];
|
||||||
|
|
||||||
|
let statusMsg = '';
|
||||||
|
|
||||||
|
if (player.voiceConnection === null) {
|
||||||
|
await player.connect(targetVoiceChannel);
|
||||||
|
|
||||||
|
// Resume / start playback
|
||||||
|
await player.play();
|
||||||
|
|
||||||
|
if (wasPlayingSong) {
|
||||||
|
statusMsg = 'resuming playback';
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [buildPlayingMessageEmbed(player)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build response message
|
||||||
|
if (statusMsg !== '') {
|
||||||
|
if (extraMsg === '') {
|
||||||
|
extraMsg = statusMsg;
|
||||||
|
} else {
|
||||||
|
extraMsg = `${statusMsg}, ${extraMsg}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraMsg !== '') {
|
||||||
|
extraMsg = ` (${extraMsg})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSongs.length === 1) {
|
||||||
|
await interaction.editReply(`u betcha, **${firstSong.title}** added to the${addToFrontOfQueue ? ' front of the' : ''} queue${extraMsg}`);
|
||||||
|
} else {
|
||||||
|
await interaction.editReply(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ export interface PlayerEvents {
|
||||||
export default class {
|
export default class {
|
||||||
public voiceConnection: VoiceConnection | null = null;
|
public voiceConnection: VoiceConnection | null = null;
|
||||||
public status = STATUS.PAUSED;
|
public status = STATUS.PAUSED;
|
||||||
|
public guildId: string;
|
||||||
|
|
||||||
private queue: QueuedSong[] = [];
|
private queue: QueuedSong[] = [];
|
||||||
private queuePosition = 0;
|
private queuePosition = 0;
|
||||||
|
@ -51,9 +52,10 @@ export default class {
|
||||||
private readonly discordClient: Client;
|
private readonly discordClient: Client;
|
||||||
private readonly fileCache: FileCacheProvider;
|
private readonly fileCache: FileCacheProvider;
|
||||||
|
|
||||||
constructor(client: Client, fileCache: FileCacheProvider) {
|
constructor(client: Client, fileCache: FileCacheProvider, guildId: string) {
|
||||||
this.discordClient = client;
|
this.discordClient = client;
|
||||||
this.fileCache = fileCache;
|
this.fileCache = fileCache;
|
||||||
|
this.guildId = guildId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(channel: VoiceChannel): Promise<void> {
|
async connect(channel: VoiceChannel): Promise<void> {
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const TYPES = {
|
||||||
UpdatingQueueEmbed: Symbol('UpdatingQueueEmbed'),
|
UpdatingQueueEmbed: Symbol('UpdatingQueueEmbed'),
|
||||||
},
|
},
|
||||||
Services: {
|
Services: {
|
||||||
|
AddQueryToQueue: Symbol('AddQueryToQueue'),
|
||||||
GetSongs: Symbol('GetSongs'),
|
GetSongs: Symbol('GetSongs'),
|
||||||
NaturalLanguage: Symbol('NaturalLanguage'),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -279,7 +279,7 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@octokit/openapi-types" "^11.2.0"
|
"@octokit/openapi-types" "^11.2.0"
|
||||||
|
|
||||||
"@prisma/client@^3.7.0":
|
"@prisma/client@^3.8.1":
|
||||||
version "3.8.1"
|
version "3.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0"
|
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0"
|
||||||
integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ==
|
integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ==
|
||||||
|
@ -2899,7 +2899,7 @@ prism-media@^1.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.2.tgz#a1f04423ec15d22f3d62b1987b6a25dc49aad13b"
|
resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.2.tgz#a1f04423ec15d22f3d62b1987b6a25dc49aad13b"
|
||||||
integrity sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==
|
integrity sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==
|
||||||
|
|
||||||
prisma@^3.7.0:
|
prisma@^3.8.1:
|
||||||
version "3.8.1"
|
version "3.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873"
|
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873"
|
||||||
integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA==
|
integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA==
|
||||||
|
|
Loading…
Reference in a new issue