diff options
| author | Max Isom <[email protected]> | 2021-12-18 12:13:13 -0600 |
|---|---|---|
| committer | Max Isom <[email protected]> | 2021-12-18 12:13:13 -0600 |
| commit | e8e75917308602782350b77e89787b22e45234a3 (patch) | |
| tree | b3aaa6b52705f62733096214215d04b841fb0244 /src | |
| parent | 5ce3f92023d56b16c8b15e584084fe90e7ab9b55 (diff) | |
| parent | d4827b86d5c848c68cb9868bfde28a2db932c630 (diff) | |
| download | muse-e8e75917308602782350b77e89787b22e45234a3.tar.xz muse-e8e75917308602782350b77e89787b22e45234a3.zip | |
Merge branch 'master' into playlist-limit-config
Diffstat (limited to 'src')
| -rw-r--r-- | src/bot.ts | 8 | ||||
| -rw-r--r-- | src/commands/play.ts | 32 | ||||
| -rw-r--r-- | src/index.ts | 13 | ||||
| -rw-r--r-- | src/services/file-cache.ts | 124 | ||||
| -rw-r--r-- | src/services/player.ts | 4 |
5 files changed, 158 insertions, 23 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); 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); } |
