mirror of
https://github.com/BluemediaDev/muse.git
synced 2025-05-09 19:51:36 +02:00
Merge branch 'master' into feature/slash-commands
This commit is contained in:
commit
ed4e7b5ceb
36 changed files with 1464 additions and 1665 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;
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
22
src/index.ts
22
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};
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
10
src/scripts/cache-clear-key-value.ts
Normal file
10
src/scripts/cache-clear-key-value.ts
Normal file
|
@ -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.');
|
||||
})();
|
83
src/scripts/migrate-and-start.ts
Normal file
83
src/scripts/migrate-and-start.ts
Normal file
|
@ -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();
|
||||
})();
|
13
src/scripts/run-with-database-url.ts
Normal file
13
src/scripts/run-with-database-url.ts
Normal file
|
@ -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,
|
||||
});
|
||||
})();
|
9
src/scripts/start.ts
Normal file
9
src/scripts/start.ts
Normal file
|
@ -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();
|
||||
})();
|
|
@ -1,4 +1,5 @@
|
|||
import dotenv from 'dotenv';
|
||||
import 'reflect-metadata';
|
||||
import {injectable} from 'inversify';
|
||||
import path from 'path';
|
||||
import xbytes from 'xbytes';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
));
|
||||
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,
|
||||
},
|
||||
));
|
||||
|
||||
let firstVideo: Video | undefined;
|
||||
let firstVideo: Video | undefined;
|
||||
|
||||
for (const item of items) {
|
||||
if (item.type === 'video') {
|
||||
firstVideo = item;
|
||||
break;
|
||||
}
|
||||
for (const item of items) {
|
||||
if (item.type === 'video') {
|
||||
firstVideo = item;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!firstVideo) {
|
||||
throw new Error('No video found.');
|
||||
}
|
||||
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
7
src/utils/create-database-url.ts
Normal file
7
src/utils/create-database-url.ts
Normal file
|
@ -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;
|
|
@ -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();
|
||||
|
|
16
src/utils/log-banner.ts
Normal file
16
src/utils/log-banner.ts
Normal file
|
@ -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;
|
Loading…
Add table
Add a link
Reference in a new issue