mirror of
https://github.com/BluemediaDev/muse.git
synced 2025-05-10 04:01:37 +02:00
Merge branch 'master' into playlist-limit-config
This commit is contained in:
commit
e8e7591730
15 changed files with 598 additions and 166 deletions
|
@ -1,5 +1,6 @@
|
|||
import {Client, Message, Collection} from 'discord.js';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import ora from 'ora';
|
||||
import {TYPES} from './types.js';
|
||||
import {Settings, Shortcut} from './models/index.js';
|
||||
import container from './inversify.config.js';
|
||||
|
@ -96,9 +97,12 @@ export default class {
|
|||
}
|
||||
});
|
||||
|
||||
this.client.on('ready', async () => {
|
||||
const spinner = ora('📡 connecting to Discord...').start();
|
||||
|
||||
this.client.on('ready', () => {
|
||||
debug(generateDependencyReport());
|
||||
console.log(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot&permissions=36752448`);
|
||||
|
||||
spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot&permissions=36752448`);
|
||||
});
|
||||
|
||||
this.client.on('error', console.error);
|
||||
|
|
|
@ -49,7 +49,6 @@ export default class implements Command {
|
|||
|
||||
const player = this.playerManager.get(msg.guild!.id);
|
||||
|
||||
const queueOldSize = player.queueSize();
|
||||
const wasPlayingSong = player.getCurrent() !== null;
|
||||
|
||||
if (args.length === 0) {
|
||||
|
@ -150,6 +149,28 @@ export default class implements Command {
|
|||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Build response message
|
||||
if (statusMsg !== '') {
|
||||
if (extraMsg === '') {
|
||||
extraMsg = statusMsg;
|
||||
} else {
|
||||
extraMsg = `${statusMsg}, ${extraMsg}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (extraMsg !== '') {
|
||||
extraMsg = ` (${extraMsg})`;
|
||||
}
|
||||
|
@ -159,14 +180,5 @@ export default class implements Command {
|
|||
} else {
|
||||
await res.stop(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`);
|
||||
}
|
||||
|
||||
if (queueOldSize === 0 && !wasPlayingSong) {
|
||||
// Only auto-play if queue was empty before and nothing was playing
|
||||
if (player.voiceConnection === null) {
|
||||
await player.connect(targetVoiceChannel);
|
||||
}
|
||||
|
||||
await player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
13
src/index.ts
13
src/index.ts
|
@ -1,15 +1,28 @@
|
|||
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');
|
||||
|
||||
// Create data directories if necessary
|
||||
const config = container.get<Config>(TYPES.Config);
|
||||
|
||||
|
|
|
@ -5,9 +5,12 @@ 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';
|
||||
|
||||
@injectable()
|
||||
export default class FileCacheProvider {
|
||||
private static readonly evictionQueue = new PQueue({concurrency: 1});
|
||||
private readonly config: Config;
|
||||
|
||||
constructor(@inject(TYPES.Config) config: Config) {
|
||||
|
@ -58,10 +61,14 @@ export default class FileCacheProvider {
|
|||
const stats = await fs.stat(tmpPath);
|
||||
|
||||
if (stats.size !== 0) {
|
||||
await fs.rename(tmpPath, finalPath);
|
||||
}
|
||||
try {
|
||||
await fs.rename(tmpPath, finalPath);
|
||||
|
||||
await FileCache.create({hash, bytes: stats.size, accessedAt: new Date()});
|
||||
await FileCache.create({hash, bytes: stats.size, accessedAt: new Date()});
|
||||
} catch (error) {
|
||||
debug('Errored when moving a finished cache file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
await this.evictOldestIfNecessary();
|
||||
});
|
||||
|
@ -80,13 +87,19 @@ export default class FileCacheProvider {
|
|||
}
|
||||
|
||||
private async evictOldestIfNecessary() {
|
||||
const [{dataValues: {totalSizeBytes}}] = await FileCache.findAll({
|
||||
attributes: [
|
||||
[sequelize.fn('sum', sequelize.col('bytes')), 'totalSizeBytes'],
|
||||
],
|
||||
}) as unknown as [{dataValues: {totalSizeBytes: number}}];
|
||||
void FileCacheProvider.evictionQueue.add(this.evictOldest.bind(this));
|
||||
|
||||
if (totalSizeBytes > this.config.CACHE_LIMIT_IN_BYTES) {
|
||||
return FileCacheProvider.evictionQueue.onEmpty();
|
||||
}
|
||||
|
||||
private async evictOldest() {
|
||||
debug('Evicting oldest files...');
|
||||
|
||||
let totalSizeBytes = await this.getDiskUsageInBytes();
|
||||
let numOfEvictedFiles = 0;
|
||||
// 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'],
|
||||
|
@ -96,22 +109,111 @@ export default class FileCacheProvider {
|
|||
if (oldest) {
|
||||
await oldest.destroy();
|
||||
await fs.unlink(path.join(this.config.CACHE_DIR, oldest.hash));
|
||||
debug(`${oldest.hash} has been evicted`);
|
||||
numOfEvictedFiles++;
|
||||
}
|
||||
|
||||
// Continue to evict until we're under the limit
|
||||
await this.evictOldestIfNecessary();
|
||||
totalSizeBytes = await this.getDiskUsageInBytes();
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
if (numOfEvictedFiles > 0) {
|
||||
debug(`${numOfEvictedFiles} files have been evicted`);
|
||||
} else {
|
||||
debug(`No files needed to be evicted. Total size of the cache is currently ${totalSizeBytes} bytes, and the cache limit is ${this.config.CACHE_LIMIT_IN_BYTES} bytes.`);
|
||||
}
|
||||
}
|
||||
|
||||
private async removeOrphans() {
|
||||
// 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);
|
||||
|
||||
if (!model) {
|
||||
debug(`${dirent.name} was present on disk but was not in the database. Removing from disk.`);
|
||||
await fs.unlink(path.join(this.config.CACHE_DIR, dirent.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check database direction (do entries exist in the database but not on the disk?)
|
||||
for await (const model of this.getFindAllIterable()) {
|
||||
const filePath = path.join(this.config.CACHE_DIR, model.hash);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
debug(`${model.hash} was present in database but was not on disk. Removing from database.`);
|
||||
await model.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls from the database rather than the filesystem,
|
||||
* so may be slightly inaccurate.
|
||||
* @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}}];
|
||||
|
||||
return totalSizeBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* An efficient way to iterate over all rows.
|
||||
* @returns an iterable for the result of FileCache.findAll()
|
||||
*/
|
||||
private getFindAllIterable() {
|
||||
const limit = 50;
|
||||
let previousCreatedAt: Date | null = null;
|
||||
|
||||
let models: FileCache[] = [];
|
||||
|
||||
const fetchNextBatch = async () => {
|
||||
let where = {};
|
||||
|
||||
if (previousCreatedAt) {
|
||||
where = {
|
||||
createdAt: {
|
||||
[sequelize.Op.gt]: previousCreatedAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
models = await FileCache.findAll({
|
||||
where,
|
||||
limit,
|
||||
order: ['createdAt'],
|
||||
});
|
||||
|
||||
if (models.length > 0) {
|
||||
previousCreatedAt = models[models.length - 1].createdAt as Date;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
[Symbol.asyncIterator]() {
|
||||
return {
|
||||
async next() {
|
||||
if (models.length === 0) {
|
||||
await fetchNextBatch();
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
// Must return value here for types to be inferred correctly
|
||||
return {done: true, value: null as unknown as FileCache};
|
||||
}
|
||||
|
||||
return {value: models.shift()!, done: false};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -235,6 +235,10 @@ export default class {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns queue, not including the current song.
|
||||
* @returns {QueuedSong[]}
|
||||
*/
|
||||
getQueue(): QueuedSong[] {
|
||||
return this.queue.slice(this.queuePosition + 1);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue