diff options
| author | Max Isom <[email protected]> | 2022-01-19 13:40:48 -0600 |
|---|---|---|
| committer | Max Isom <[email protected]> | 2022-01-19 13:40:48 -0600 |
| commit | ed4e7b5ceb146a9baa062426cc0fb1ec8844e056 (patch) | |
| tree | ab01e87b7b0e075fc258ba9ce1fa5558f2eeec7b /src | |
| parent | 86e9936578d19c2115fc03acae1794532a09d62e (diff) | |
| parent | da72cd708bcfbba6e0a91da4878aaef10d2532e2 (diff) | |
| download | muse-ed4e7b5ceb146a9baa062426cc0fb1ec8844e056.tar.xz muse-ed4e7b5ceb146a9baa062426cc0fb1ec8844e056.zip | |
Merge branch 'master' into feature/slash-commands
Diffstat (limited to 'src')
| -rw-r--r-- | src/bot.ts | 7 | ||||
| -rw-r--r-- | src/commands/play.ts | 24 | ||||
| -rw-r--r-- | src/commands/shortcuts.ts | 43 | ||||
| -rw-r--r-- | src/events/guild-create.ts | 25 | ||||
| -rw-r--r-- | src/index.ts | 22 | ||||
| -rw-r--r-- | src/models/file-cache.ts | 14 | ||||
| -rw-r--r-- | src/models/index.ts | 11 | ||||
| -rw-r--r-- | src/models/key-value-cache.ts | 15 | ||||
| -rw-r--r-- | src/models/settings.ts | 22 | ||||
| -rw-r--r-- | src/models/shortcut.ts | 23 | ||||
| -rw-r--r-- | src/scripts/cache-clear-key-value.ts | 10 | ||||
| -rw-r--r-- | src/scripts/migrate-and-start.ts | 83 | ||||
| -rw-r--r-- | src/scripts/run-with-database-url.ts | 13 | ||||
| -rw-r--r-- | src/scripts/start.ts | 9 | ||||
| -rw-r--r-- | src/services/config.ts | 1 | ||||
| -rw-r--r-- | src/services/file-cache.ts | 85 | ||||
| -rw-r--r-- | src/services/get-songs.ts | 90 | ||||
| -rw-r--r-- | src/services/key-value-cache.ts | 33 | ||||
| -rw-r--r-- | src/services/player.ts | 1 | ||||
| -rw-r--r-- | src/utils/channels.ts | 4 | ||||
| -rw-r--r-- | src/utils/create-database-url.ts | 7 | ||||
| -rw-r--r-- | src/utils/db.ts | 13 | ||||
| -rw-r--r-- | src/utils/log-banner.ts | 16 |
23 files changed, 355 insertions, 216 deletions
@@ -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; |
