aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts7
-rw-r--r--src/commands/play.ts24
-rw-r--r--src/commands/shortcuts.ts43
-rw-r--r--src/events/guild-create.ts25
-rw-r--r--src/index.ts22
-rw-r--r--src/models/file-cache.ts14
-rw-r--r--src/models/index.ts11
-rw-r--r--src/models/key-value-cache.ts15
-rw-r--r--src/models/settings.ts22
-rw-r--r--src/models/shortcut.ts23
-rw-r--r--src/scripts/cache-clear-key-value.ts10
-rw-r--r--src/scripts/migrate-and-start.ts83
-rw-r--r--src/scripts/run-with-database-url.ts13
-rw-r--r--src/scripts/start.ts9
-rw-r--r--src/services/config.ts1
-rw-r--r--src/services/file-cache.ts85
-rw-r--r--src/services/get-songs.ts90
-rw-r--r--src/services/key-value-cache.ts33
-rw-r--r--src/services/player.ts1
-rw-r--r--src/utils/channels.ts4
-rw-r--r--src/utils/create-database-url.ts7
-rw-r--r--src/utils/db.ts13
-rw-r--r--src/utils/log-banner.ts16
23 files changed, 355 insertions, 216 deletions
diff --git a/src/bot.ts b/src/bot.ts
index fb92d14..8550303 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -22,7 +22,10 @@ export default class {
private readonly commandsByName!: Collection<string, Command>;
private readonly commandsByButtonId!: Collection<string, Command>;
- constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Config) config: Config) {
+ constructor(
+ @inject(TYPES.Client) client: Client,
+ @inject(TYPES.Config) config: Config,
+ ) {
this.client = client;
this.token = config.DISCORD_TOKEN;
this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT;
@@ -61,7 +64,7 @@ export default class {
}
try {
- if (command.requiresVC && !isUserInVoice(interaction.guild, interaction.member.user as User)) {
+ 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;
}
diff --git a/src/commands/play.ts b/src/commands/play.ts
index a5f451d..4ac566b 100644
--- a/src/commands/play.ts
+++ b/src/commands/play.ts
@@ -2,6 +2,7 @@ import {CommandInteraction, GuildMember} from 'discord.js';
import {URL} from 'url';
import {Except} from 'type-fest';
import {SlashCommandBuilder} from '@discordjs/builders';
+import shuffle from 'array-shuffle';
import {inject, injectable} from 'inversify';
import Command from '.';
import {TYPES} from '../types.js';
@@ -10,7 +11,7 @@ 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 Settings from '../models/settings.js';
+import {prisma} from '../utils/db.js';
@injectable()
export default class implements Command {
@@ -23,7 +24,10 @@ export default class implements Command {
.setDescription('YouTube URL, Spotify URL, or search query'))
.addBooleanOption(option => option
.setName('immediate')
- .setDescription('adds track to the front of the queue'));
+ .setDescription('adds track to the front of the queue'))
+ .addBooleanOption(option => option
+ .setName('shuffle')
+ .setDescription('shuffles the input if it\'s a playlist'));
public requiresVC = true;
@@ -39,8 +43,13 @@ export default class implements Command {
public async executeFromInteraction(interaction: CommandInteraction): Promise<void> {
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
- const settings = await Settings.findByPk(interaction.guild!.id);
- const {playlistLimit} = settings!;
+ const settings = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}});
+
+ if (!settings) {
+ 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;
@@ -67,8 +76,9 @@ export default class implements Command {
}
const addToFrontOfQueue = interaction.options.getBoolean('immediate');
+ const shuffleAdditions = interaction.options.getBoolean('shuffle');
- const newSongs: Array<Except<QueuedSong, 'addedInChannelId'>> = [];
+ let newSongs: Array<Except<QueuedSong, 'addedInChannelId'>> = [];
let extraMsg = '';
await interaction.deferReply();
@@ -139,6 +149,10 @@ export default class implements Command {
return;
}
+ if (shuffleAdditions) {
+ newSongs = shuffle(newSongs);
+ }
+
newSongs.forEach(song => {
player.add({...song, addedInChannelId: interaction.channel?.id}, {immediate: addToFrontOfQueue ?? false});
});
diff --git a/src/commands/shortcuts.ts b/src/commands/shortcuts.ts
index 929a5c1..e40d10a 100644
--- a/src/commands/shortcuts.ts
+++ b/src/commands/shortcuts.ts
@@ -1,8 +1,8 @@
import {Message} from 'discord.js';
import {injectable} from 'inversify';
-import {Shortcut, Settings} from '../models/index.js';
import errorMsg from '../utils/error-msg.js';
import Command from '.';
+import {prisma} from '../utils/db.js';
@injectable()
export default class implements Command {
@@ -18,7 +18,11 @@ export default class implements Command {
public async execute(msg: Message, args: string []): Promise<void> {
if (args.length === 0) {
// Get shortcuts for guild
- const shortcuts = await Shortcut.findAll({where: {guildId: msg.guild!.id}});
+ const shortcuts = await prisma.shortcut.findMany({
+ where: {
+ guildId: msg.guild!.id,
+ },
+ });
if (shortcuts.length === 0) {
await msg.channel.send('no shortcuts exist');
@@ -26,7 +30,11 @@ export default class implements Command {
}
// Get prefix for guild
- const settings = await Settings.findOne({where: {guildId: msg.guild!.id}});
+ const settings = await prisma.setting.findUnique({
+ where: {
+ guildId: msg.guild!.id,
+ },
+ });
if (!settings) {
return;
@@ -48,7 +56,12 @@ export default class implements Command {
switch (action) {
case 'set': {
- const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}});
+ const shortcut = await prisma.shortcut.findFirst({
+ where: {
+ guildId: msg.guild!.id,
+ shortcut: shortcutName,
+ },
+ });
const command = args.slice(2).join(' ');
@@ -60,10 +73,15 @@ export default class implements Command {
return;
}
- await shortcut.update(newShortcut);
+ await prisma.shortcut.update({
+ where: {
+ id: shortcut.id,
+ },
+ data: newShortcut,
+ });
await msg.channel.send('shortcut updated');
} else {
- await Shortcut.create(newShortcut);
+ await prisma.shortcut.create({data: newShortcut});
await msg.channel.send('shortcut created');
}
@@ -72,7 +90,12 @@ export default class implements Command {
case 'delete': {
// Check if shortcut exists
- const shortcut = await Shortcut.findOne({where: {guildId: msg.guild!.id, shortcut: shortcutName}});
+ const shortcut = await prisma.shortcut.findFirst({
+ where: {
+ guildId: msg.guild!.id,
+ shortcut: shortcutName,
+ },
+ });
if (!shortcut) {
await msg.channel.send(errorMsg('shortcut doesn\'t exist'));
@@ -85,7 +108,11 @@ export default class implements Command {
return;
}
- await shortcut.destroy();
+ await prisma.shortcut.delete({
+ where: {
+ id: shortcut.id,
+ },
+ });
await msg.channel.send('shortcut deleted');
diff --git a/src/events/guild-create.ts b/src/events/guild-create.ts
index e7a6467..3cf94ea 100644
--- a/src/events/guild-create.ts
+++ b/src/events/guild-create.ts
@@ -2,17 +2,28 @@ import {Guild, TextChannel, Message, MessageReaction, User, ApplicationCommandDa
} from 'discord.js';
import emoji from 'node-emoji';
import pEvent from 'p-event';
-import {Settings} from '../models/index.js';
import {chunk} from '../utils/arrays.js';
import container from '../inversify.config.js';
import Command from '../commands';
import {TYPES} from '../types.js';
import Config from '../services/config.js';
+import {prisma} from '../utils/db.js';
const DEFAULT_PREFIX = '!';
export default async (guild: Guild): Promise<void> => {
- await Settings.upsert({guildId: guild.id, prefix: DEFAULT_PREFIX});
+ await prisma.setting.upsert({
+ where: {
+ guildId: guild.id,
+ },
+ create: {
+ guildId: guild.id,
+ prefix: DEFAULT_PREFIX,
+ },
+ update: {
+ prefix: DEFAULT_PREFIX,
+ },
+ });
const config = container.get<Config>(TYPES.Config);
@@ -86,7 +97,15 @@ export default async (guild: Guild): Promise<void> => {
const prefixCharacter = prefixResponses.first()!.content;
// Save settings
- await Settings.update({prefix: prefixCharacter, channel: chosenChannel.id}, {where: {guildId: guild.id}});
+ 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;
diff --git a/src/index.ts b/src/index.ts
index a6b6d35..e6df09d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,28 +1,14 @@
import makeDir from 'make-dir';
import path from 'path';
-import {makeLines} from 'nodesplash';
import container from './inversify.config.js';
import {TYPES} from './types.js';
import Bot from './bot.js';
-import {sequelize} from './utils/db.js';
import Config from './services/config.js';
import FileCacheProvider from './services/file-cache.js';
-import metadata from '../package.json';
const bot = container.get<Bot>(TYPES.Bot);
-(async () => {
- // Banner
- console.log(makeLines({
- user: 'codetheweb',
- repository: 'muse',
- version: metadata.version,
- paypalUser: 'codetheweb',
- githubSponsor: 'codetheweb',
- madeByPrefix: 'Made with 🎶 by ',
- }).join('\n'));
- console.log('\n');
-
+const startBot = async () => {
// Create data directories if necessary
const config = container.get<Config>(TYPES.Config);
@@ -30,9 +16,9 @@ const bot = container.get<Bot>(TYPES.Bot);
await makeDir(config.CACHE_DIR);
await makeDir(path.join(config.CACHE_DIR, 'tmp'));
- await sequelize.sync({alter: true});
-
await container.get<FileCacheProvider>(TYPES.FileCache).cleanup();
await bot.listen();
-})();
+};
+
+export {startBot};
diff --git a/src/models/file-cache.ts b/src/models/file-cache.ts
deleted file mode 100644
index dbac6d3..0000000
--- a/src/models/file-cache.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript';
-
-@Table
-export default class FileCache extends Model {
- @PrimaryKey
- @Column
- hash!: string;
-
- @Column
- bytes!: number;
-
- @Column
- accessedAt!: Date;
-}
diff --git a/src/models/index.ts b/src/models/index.ts
deleted file mode 100644
index e3f7c0a..0000000
--- a/src/models/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import FileCache from './file-cache.js';
-import KeyValueCache from './key-value-cache.js';
-import Settings from './settings.js';
-import Shortcut from './shortcut.js';
-
-export {
- FileCache,
- KeyValueCache,
- Settings,
- Shortcut,
-};
diff --git a/src/models/key-value-cache.ts b/src/models/key-value-cache.ts
deleted file mode 100644
index 5072538..0000000
--- a/src/models/key-value-cache.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import {Table, Column, PrimaryKey, Model} from 'sequelize-typescript';
-import sequelize from 'sequelize';
-
-@Table
-export default class KeyValueCache extends Model {
- @PrimaryKey
- @Column
- key!: string;
-
- @Column(sequelize.TEXT)
- value!: string;
-
- @Column
- expiresAt!: Date;
-}
diff --git a/src/models/settings.ts b/src/models/settings.ts
deleted file mode 100644
index f9756d3..0000000
--- a/src/models/settings.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import {Table, Column, PrimaryKey, Model, Default} from 'sequelize-typescript';
-
-@Table
-export default class Settings extends Model {
- @PrimaryKey
- @Column
- guildId!: string;
-
- @Column
- prefix!: string;
-
- @Column
- channel!: string;
-
- @Default(false)
- @Column
- finishedSetup!: boolean;
-
- @Default(50)
- @Column
- playlistLimit!: number;
-}
diff --git a/src/models/shortcut.ts b/src/models/shortcut.ts
deleted file mode 100644
index 4ec88ed..0000000
--- a/src/models/shortcut.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {Table, Column, PrimaryKey, Model, AutoIncrement, Index} from 'sequelize-typescript';
-
-@Table
-export default class Shortcut extends Model {
- @PrimaryKey
- @AutoIncrement
- @Column
- id!: number;
-
- @Column
- @Index
- guildId!: string;
-
- @Column
- authorId!: string;
-
- @Column
- @Index
- shortcut!: string;
-
- @Column
- command!: string;
-}
diff --git a/src/scripts/cache-clear-key-value.ts b/src/scripts/cache-clear-key-value.ts
new file mode 100644
index 0000000..6c05ed4
--- /dev/null
+++ b/src/scripts/cache-clear-key-value.ts
@@ -0,0 +1,10 @@
+import ora from 'ora';
+import {prisma} from '../utils/db.js';
+
+(async () => {
+ const spinner = ora('Clearing key value cache...').start();
+
+ await prisma.keyValueCache.deleteMany({});
+
+ spinner.succeed('Key value cache cleared.');
+})();
diff --git a/src/scripts/migrate-and-start.ts b/src/scripts/migrate-and-start.ts
new file mode 100644
index 0000000..d971745
--- /dev/null
+++ b/src/scripts/migrate-and-start.ts
@@ -0,0 +1,83 @@
+// This script applies Prisma migrations
+// and then starts Muse.
+import dotenv from 'dotenv';
+dotenv.config();
+
+import {execa, ExecaError} from 'execa';
+import {promises as fs} from 'fs';
+import Prisma from '@prisma/client';
+import ora from 'ora';
+import {startBot} from '../index.js';
+import logBanner from '../utils/log-banner.js';
+import {createDatabasePath} from '../utils/create-database-url.js';
+import {DATA_DIR} from '../services/config.js';
+
+const client = new Prisma.PrismaClient();
+
+const migrateFromSequelizeToPrisma = async () => {
+ await execa('prisma', ['migrate', 'resolve', '--applied', '20220101155430_migrate_from_sequelize'], {preferLocal: true});
+};
+
+const doesUserHaveExistingDatabase = async () => {
+ try {
+ await fs.access(createDatabasePath(DATA_DIR));
+
+ return true;
+ } catch {
+ return false;
+ }
+};
+
+const hasDatabaseBeenMigratedToPrisma = async () => {
+ try {
+ await client.$queryRaw`SELECT COUNT(id) FROM _prisma_migrations`;
+ } catch (error: unknown) {
+ if (error instanceof Prisma.Prisma.PrismaClientKnownRequestError && error.code === 'P2010') {
+ // Table doesn't exist
+ return false;
+ }
+
+ throw error;
+ }
+
+ return true;
+};
+
+(async () => {
+ // Banner
+ logBanner();
+
+ const spinner = ora('Applying database migrations...').start();
+
+ if (await doesUserHaveExistingDatabase()) {
+ if (!(await hasDatabaseBeenMigratedToPrisma())) {
+ try {
+ await migrateFromSequelizeToPrisma();
+ } catch (error) {
+ if ((error as ExecaError).stderr) {
+ spinner.fail('Failed to apply database migrations (going from Sequelize to Prisma):');
+ console.error((error as ExecaError).stderr);
+ process.exit(1);
+ } else {
+ throw error;
+ }
+ }
+ }
+ }
+
+ try {
+ await execa('prisma', ['migrate', 'deploy'], {preferLocal: true});
+ } catch (error: unknown) {
+ if ((error as ExecaError).stderr) {
+ spinner.fail('Failed to apply database migrations:');
+ console.error((error as ExecaError).stderr);
+ process.exit(1);
+ } else {
+ throw error;
+ }
+ }
+
+ spinner.succeed('Database migrations applied.');
+
+ await startBot();
+})();
diff --git a/src/scripts/run-with-database-url.ts b/src/scripts/run-with-database-url.ts
new file mode 100644
index 0000000..b7ae3a7
--- /dev/null
+++ b/src/scripts/run-with-database-url.ts
@@ -0,0 +1,13 @@
+import {DATA_DIR} from '../services/config.js';
+import createDatabaseUrl from '../utils/create-database-url.js';
+import {execa} from 'execa';
+
+process.env.DATABASE_URL = createDatabaseUrl(DATA_DIR);
+
+(async () => {
+ await execa(process.argv[2], process.argv.slice(3), {
+ preferLocal: true,
+ stderr: process.stderr,
+ stdout: process.stdout,
+ });
+})();
diff --git a/src/scripts/start.ts b/src/scripts/start.ts
new file mode 100644
index 0000000..a9e294e
--- /dev/null
+++ b/src/scripts/start.ts
@@ -0,0 +1,9 @@
+// This script is mainly used during development.
+// Starts Muse without applying database migrations.
+import {startBot} from '../index.js';
+import logBanner from '../utils/log-banner.js';
+
+(async () => {
+ logBanner();
+ await startBot();
+})();
diff --git a/src/services/config.ts b/src/services/config.ts
index 34e612c..82cffee 100644
--- a/src/services/config.ts
+++ b/src/services/config.ts
@@ -1,4 +1,5 @@
import dotenv from 'dotenv';
+import 'reflect-metadata';
import {injectable} from 'inversify';
import path from 'path';
import xbytes from 'xbytes';
diff --git a/src/services/file-cache.ts b/src/services/file-cache.ts
index 1ffb824..98a079c 100644
--- a/src/services/file-cache.ts
+++ b/src/services/file-cache.ts
@@ -1,12 +1,12 @@
import {promises as fs, createWriteStream} from 'fs';
import path from 'path';
import {inject, injectable} from 'inversify';
-import sequelize from 'sequelize';
-import {FileCache} from '../models/index.js';
import {TYPES} from '../types.js';
import Config from './config.js';
import PQueue from 'p-queue';
import debug from '../utils/debug.js';
+import {prisma} from '../utils/db.js';
+import {FileCache} from '@prisma/client';
@injectable()
export default class FileCacheProvider {
@@ -23,7 +23,11 @@ export default class FileCacheProvider {
* @param hash lookup key
*/
async getPathFor(hash: string): Promise<string> {
- const model = await FileCache.findByPk(hash);
+ const model = await prisma.fileCache.findUnique({
+ where: {
+ hash,
+ },
+ });
if (!model) {
throw new Error('File is not cached');
@@ -34,12 +38,23 @@ export default class FileCacheProvider {
try {
await fs.access(resolvedPath);
} catch (_: unknown) {
- await FileCache.destroy({where: {hash}});
+ await prisma.fileCache.delete({
+ where: {
+ hash,
+ },
+ });
throw new Error('File is not cached');
}
- await model.update({accessedAt: new Date()});
+ await prisma.fileCache.update({
+ where: {
+ hash,
+ },
+ data: {
+ accessedAt: new Date(),
+ },
+ });
return resolvedPath;
}
@@ -64,7 +79,13 @@ export default class FileCacheProvider {
try {
await fs.rename(tmpPath, finalPath);
- await FileCache.create({hash, bytes: stats.size, accessedAt: new Date()});
+ await prisma.fileCache.create({
+ data: {
+ hash,
+ accessedAt: new Date(),
+ bytes: stats.size,
+ },
+ });
} catch (error) {
debug('Errored when moving a finished cache file:', error);
}
@@ -100,14 +121,19 @@ export default class FileCacheProvider {
// Continue to evict until we're under the limit
/* eslint-disable no-await-in-loop */
while (totalSizeBytes > this.config.CACHE_LIMIT_IN_BYTES) {
- const oldest = await FileCache.findOne({
- order: [
- ['accessedAt', 'ASC'],
- ],
+ const oldest = await prisma.fileCache.findFirst({
+ orderBy: {
+ accessedAt: 'asc',
+ },
+
});
if (oldest) {
- await oldest.destroy();
+ await prisma.fileCache.delete({
+ where: {
+ hash: oldest.hash,
+ },
+ });
await fs.unlink(path.join(this.config.CACHE_DIR, oldest.hash));
debug(`${oldest.hash} has been evicted`);
numOfEvictedFiles++;
@@ -128,7 +154,11 @@ export default class FileCacheProvider {
// Check filesystem direction (do files exist on the disk but not in the database?)
for await (const dirent of await fs.opendir(this.config.CACHE_DIR)) {
if (dirent.isFile()) {
- const model = await FileCache.findByPk(dirent.name);
+ const model = await prisma.fileCache.findUnique({
+ where: {
+ hash: dirent.name,
+ },
+ });
if (!model) {
debug(`${dirent.name} was present on disk but was not in the database. Removing from disk.`);
@@ -145,7 +175,11 @@ export default class FileCacheProvider {
await fs.access(filePath);
} catch {
debug(`${model.hash} was present in database but was not on disk. Removing from database.`);
- await model.destroy();
+ await prisma.fileCache.delete({
+ where: {
+ hash: model.hash,
+ },
+ });
}
}
}
@@ -156,11 +190,12 @@ export default class FileCacheProvider {
* @returns the total size of the cache in bytes
*/
private async getDiskUsageInBytes() {
- const [{dataValues: {totalSizeBytes}}] = await FileCache.findAll({
- attributes: [
- [sequelize.fn('sum', sequelize.col('bytes')), 'totalSizeBytes'],
- ],
- }) as unknown as [{dataValues: {totalSizeBytes: number}}];
+ const data = await prisma.fileCache.aggregate({
+ _sum: {
+ bytes: true,
+ },
+ });
+ const totalSizeBytes = data._sum.bytes ?? 0;
return totalSizeBytes;
}
@@ -176,24 +211,26 @@ export default class FileCacheProvider {
let models: FileCache[] = [];
const fetchNextBatch = async () => {
- let where = {};
+ let where;
if (previousCreatedAt) {
where = {
createdAt: {
- [sequelize.Op.gt]: previousCreatedAt,
+ gt: previousCreatedAt,
},
};
}
- models = await FileCache.findAll({
+ models = await prisma.fileCache.findMany({
where,
- limit,
- order: ['createdAt'],
+ orderBy: {
+ createdAt: 'asc',
+ },
+ take: limit,
});
if (models.length > 0) {
- previousCreatedAt = models[models.length - 1].createdAt as Date;
+ previousCreatedAt = models[models.length - 1].createdAt;
}
};
diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts
index ebf779a..780f7ec 100644
--- a/src/services/get-songs.ts
+++ b/src/services/get-songs.ts
@@ -42,59 +42,51 @@ export default class {
this.ytsrQueue = new PQueue({concurrency: 4});
}
- async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel | null> {
- try {
- const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
- ytsr,
- query,
- {
- limit: 10,
- },
- {
- expiresIn: ONE_HOUR_IN_SECONDS,
- },
- ));
-
- let firstVideo: Video | undefined;
+ async youtubeVideoSearch(query: string): Promise<QueuedSongWithoutChannel> {
+ const {items} = await this.ytsrQueue.add(async () => this.cache.wrap(
+ ytsr,
+ query,
+ {
+ limit: 10,
+ },
+ {
+ expiresIn: ONE_HOUR_IN_SECONDS,
+ },
+ ));
- for (const item of items) {
- if (item.type === 'video') {
- firstVideo = item;
- break;
- }
- }
+ let firstVideo: Video | undefined;
- if (!firstVideo) {
- throw new Error('No video found.');
+ for (const item of items) {
+ if (item.type === 'video') {
+ firstVideo = item;
+ break;
}
+ }
- return await this.youtubeVideo(firstVideo.id);
- } catch (_: unknown) {
- return null;
+ if (!firstVideo) {
+ throw new Error('No video found.');
}
+
+ return this.youtubeVideo(firstVideo.id);
}
- async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel | null> {
- try {
- const videoDetails = await this.cache.wrap(
- this.youtube.videos.get,
- cleanUrl(url),
- {
- expiresIn: ONE_HOUR_IN_SECONDS,
- },
- );
+ async youtubeVideo(url: string): Promise<QueuedSongWithoutChannel> {
+ const videoDetails = await this.cache.wrap(
+ this.youtube.videos.get,
+ cleanUrl(url),
+ {
+ expiresIn: ONE_HOUR_IN_SECONDS,
+ },
+ );
- return {
- title: videoDetails.snippet.title,
- artist: videoDetails.snippet.channelTitle,
- length: toSeconds(parse(videoDetails.contentDetails.duration)),
- url: videoDetails.id,
- playlist: null,
- isLive: videoDetails.snippet.liveBroadcastContent === 'live',
- };
- } catch (_: unknown) {
- return null;
- }
+ return {
+ title: videoDetails.snippet.title,
+ artist: videoDetails.snippet.channelTitle,
+ length: toSeconds(parse(videoDetails.contentDetails.duration)),
+ url: videoDetails.id,
+ playlist: null,
+ isLive: videoDetails.snippet.liveBroadcastContent === 'live',
+ };
}
async youtubePlaylist(listId: string): Promise<QueuedSongWithoutChannel[]> {
@@ -279,11 +271,7 @@ export default class {
return [songs as QueuedSongWithoutChannel[], nSongsNotFound, originalNSongs];
}
- private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel | null> {
- try {
- return await this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
- } catch (_: unknown) {
- return null;
- }
+ private async spotifyToYouTube(track: SpotifyApi.TrackObjectSimplified, _: QueuedPlaylist | null): Promise<QueuedSongWithoutChannel> {
+ return this.youtubeVideoSearch(`"${track.name}" "${track.artists[0].name}"`);
}
}
diff --git a/src/services/key-value-cache.ts b/src/services/key-value-cache.ts
index 7f1164d..bfe4f8c 100644
--- a/src/services/key-value-cache.ts
+++ b/src/services/key-value-cache.ts
@@ -1,5 +1,5 @@
import {injectable} from 'inversify';
-import {KeyValueCache} from '../models/index.js';
+import {prisma} from '../utils/db.js';
import debug from '../utils/debug.js';
type Seconds = number;
@@ -29,7 +29,11 @@ export default class KeyValueCacheProvider {
throw new Error(`Cache key ${key} is too short.`);
}
- const cachedResult = await KeyValueCache.findByPk(key);
+ const cachedResult = await prisma.keyValueCache.findUnique({
+ where: {
+ key,
+ },
+ });
if (cachedResult) {
if (new Date() < cachedResult.expiresAt) {
@@ -37,7 +41,11 @@ export default class KeyValueCacheProvider {
return JSON.parse(cachedResult.value) as F;
}
- await cachedResult.destroy();
+ await prisma.keyValueCache.delete({
+ where: {
+ key,
+ },
+ });
}
debug(`Cache miss: ${key}`);
@@ -45,10 +53,21 @@ export default class KeyValueCacheProvider {
const result = await func(...options as any[]);
// Save result
- await KeyValueCache.upsert({
- key,
- value: JSON.stringify(result),
- expiresAt: futureTimeToDate(expiresIn),
+ const value = JSON.stringify(result);
+ const expiresAt = futureTimeToDate(expiresIn);
+ await prisma.keyValueCache.upsert({
+ where: {
+ key,
+ },
+ update: {
+ value,
+ expiresAt,
+ },
+ create: {
+ key,
+ value,
+ expiresAt,
+ },
});
return result;
diff --git a/src/services/player.ts b/src/services/player.ts
index 26e1e36..87a33e6 100644
--- a/src/services/player.ts
+++ b/src/services/player.ts
@@ -61,6 +61,7 @@ export default class extends (EventEmitter as new () => TypedEmitter<PlayerEvent
const conn = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
+ // @ts-expect-error (see https://github.com/discordjs/voice/issues/166)
adapterCreator: channel.guild.voiceAdapterCreator,
});
diff --git a/src/utils/channels.ts b/src/utils/channels.ts
index 2d074b4..f4e576a 100644
--- a/src/utils/channels.ts
+++ b/src/utils/channels.ts
@@ -42,10 +42,10 @@ export const getMostPopularVoiceChannel = (guild: Guild): [VoiceChannel, number]
for (const [_, channel] of guild.channels.cache) {
if (channel.type === 'GUILD_VOICE') {
- const size = getSizeWithoutBots(channel as VoiceChannel);
+ const size = getSizeWithoutBots(channel);
voiceChannels.push({
- channel: channel as VoiceChannel,
+ channel,
n: size,
});
}
diff --git a/src/utils/create-database-url.ts b/src/utils/create-database-url.ts
new file mode 100644
index 0000000..becc973
--- /dev/null
+++ b/src/utils/create-database-url.ts
@@ -0,0 +1,7 @@
+import {join} from 'path';
+
+export const createDatabasePath = (directory: string) => join(directory, 'db.sqlite');
+
+const createDatabaseUrl = (directory: string) => `file:${createDatabasePath(directory)}`;
+
+export default createDatabaseUrl;
diff --git a/src/utils/db.ts b/src/utils/db.ts
index 15a2d79..b630320 100644
--- a/src/utils/db.ts
+++ b/src/utils/db.ts
@@ -1,12 +1,3 @@
-import {Sequelize} from 'sequelize-typescript';
-import path from 'path';
-import {DATA_DIR} from '../services/config.js';
-import {FileCache, KeyValueCache, Settings, Shortcut} from '../models/index.js';
+import Prisma from '@prisma/client';
-export const sequelize = new Sequelize({
- dialect: 'sqlite',
- database: 'muse',
- storage: path.join(DATA_DIR, 'db.sqlite'),
- models: [FileCache, KeyValueCache, Settings, Shortcut],
- logging: false,
-});
+export const prisma = new Prisma.PrismaClient();
diff --git a/src/utils/log-banner.ts b/src/utils/log-banner.ts
new file mode 100644
index 0000000..0ad6466
--- /dev/null
+++ b/src/utils/log-banner.ts
@@ -0,0 +1,16 @@
+import {makeLines} from 'nodesplash';
+import metadata from '../../package.json';
+
+const logBanner = () => {
+ console.log(makeLines({
+ user: 'codetheweb',
+ repository: 'muse',
+ version: metadata.version,
+ paypalUser: 'codetheweb',
+ githubSponsor: 'codetheweb',
+ madeByPrefix: 'Made with 🎶 by ',
+ }).join('\n'));
+ console.log('\n');
+};
+
+export default logBanner;