aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Isom <[email protected]>2021-12-18 12:13:13 -0600
committerMax Isom <[email protected]>2021-12-18 12:13:13 -0600
commite8e75917308602782350b77e89787b22e45234a3 (patch)
treeb3aaa6b52705f62733096214215d04b841fb0244 /src
parent5ce3f92023d56b16c8b15e584084fe90e7ab9b55 (diff)
parentd4827b86d5c848c68cb9868bfde28a2db932c630 (diff)
downloadmuse-e8e75917308602782350b77e89787b22e45234a3.tar.xz
muse-e8e75917308602782350b77e89787b22e45234a3.zip
Merge branch 'master' into playlist-limit-config
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts8
-rw-r--r--src/commands/play.ts32
-rw-r--r--src/index.ts13
-rw-r--r--src/services/file-cache.ts124
-rw-r--r--src/services/player.ts4
5 files changed, 158 insertions, 23 deletions
diff --git a/src/bot.ts b/src/bot.ts
index b49c1f6..5260ae0 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -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);
diff --git a/src/commands/play.ts b/src/commands/play.ts
index c791930..6561fa9 100644
--- a/src/commands/play.ts
+++ b/src/commands/play.ts
@@ -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();
- }
}
}
diff --git a/src/index.ts b/src/index.ts
index 383faef..a6b6d35 100644
--- a/src/index.ts
+++ b/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);
diff --git a/src/services/file-cache.ts b/src/services/file-cache.ts
index fa4f1f7..1ffb824 100644
--- a/src/services/file-cache.ts
+++ b/src/services/file-cache.ts
@@ -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));
+
+ return FileCacheProvider.evictionQueue.onEmpty();
+ }
- if (totalSizeBytes > this.config.CACHE_LIMIT_IN_BYTES) {
+ 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};
+ },
+ };
+ },
+ };
}
}
diff --git a/src/services/player.ts b/src/services/player.ts
index 67b8b38..4a226c5 100644
--- a/src/services/player.ts
+++ b/src/services/player.ts
@@ -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);
}