aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--migrations/20240312135407_add_default_volume/migration.sql17
-rw-r--r--schema.prisma1
-rw-r--r--src/commands/config.ts27
-rw-r--r--src/commands/volume.ts42
-rw-r--r--src/inversify.config.ts2
-rw-r--r--src/services/player.ts54
-rw-r--r--src/utils/build-embed.ts5
8 files changed, 141 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97009b7..db94305 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added 🔊
+- A `/volume` command is now available.
+- Set the default volume with `/config set-default-volume`
+
## [2.6.0] - 2024-03-03
### Added
diff --git a/migrations/20240312135407_add_default_volume/migration.sql b/migrations/20240312135407_add_default_volume/migration.sql
new file mode 100644
index 0000000..569dcfe
--- /dev/null
+++ b/migrations/20240312135407_add_default_volume/migration.sql
@@ -0,0 +1,17 @@
+-- RedefineTables
+PRAGMA foreign_keys=OFF;
+CREATE TABLE "new_Setting" (
+ "guildId" TEXT NOT NULL PRIMARY KEY,
+ "playlistLimit" INTEGER NOT NULL DEFAULT 50,
+ "secondsToWaitAfterQueueEmpties" INTEGER NOT NULL DEFAULT 30,
+ "leaveIfNoListeners" BOOLEAN NOT NULL DEFAULT true,
+ "autoAnnounceNextSong" BOOLEAN NOT NULL DEFAULT false,
+ "defaultVolume" INTEGER NOT NULL DEFAULT 100,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL
+);
+INSERT INTO "new_Setting" ("autoAnnounceNextSong", "createdAt", "guildId", "leaveIfNoListeners", "playlistLimit", "secondsToWaitAfterQueueEmpties", "updatedAt") SELECT "autoAnnounceNextSong", "createdAt", "guildId", "leaveIfNoListeners", "playlistLimit", "secondsToWaitAfterQueueEmpties", "updatedAt" FROM "Setting";
+DROP TABLE "Setting";
+ALTER TABLE "new_Setting" RENAME TO "Setting";
+PRAGMA foreign_key_check;
+PRAGMA foreign_keys=ON;
diff --git a/schema.prisma b/schema.prisma
index 10cc624..ab8a46a 100644
--- a/schema.prisma
+++ b/schema.prisma
@@ -29,6 +29,7 @@ model Setting {
secondsToWaitAfterQueueEmpties Int @default(30)
leaveIfNoListeners Boolean @default(true)
autoAnnounceNextSong Boolean @default(false)
+ defaultVolume Int @default(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
diff --git a/src/commands/config.ts b/src/commands/config.ts
index 9441dab..a2bfe93 100644
--- a/src/commands/config.ts
+++ b/src/commands/config.ts
@@ -41,6 +41,15 @@ export default class implements Command {
.setDescription('whether to announce the next song in the queue automatically')
.setRequired(true)))
.addSubcommand(subcommand => subcommand
+ .setName('set-default-volume')
+ .setDescription('set default volume used when entering the voice channel')
+ .addIntegerOption(option => option
+ .setName('level')
+ .setDescription('volume percentage (0 is muted, 100 is max & default)')
+ .setMinValue(0)
+ .setMaxValue(100)
+ .setRequired(true)))
+ .addSubcommand(subcommand => subcommand
.setName('get')
.setDescription('show all settings'));
@@ -121,6 +130,23 @@ export default class implements Command {
break;
}
+ case 'set-default-volume': {
+ const value = interaction.options.getInteger('level')!;
+
+ await prisma.setting.update({
+ where: {
+ guildId: interaction.guild!.id,
+ },
+ data: {
+ defaultVolume: value,
+ },
+ });
+
+ await interaction.reply('👍 volume setting updated');
+
+ break;
+ }
+
case 'get': {
const embed = new EmbedBuilder().setTitle('Config');
@@ -133,6 +159,7 @@ export default class implements Command {
: `${config.secondsToWaitAfterQueueEmpties}s`,
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
'Auto announce next song in queue': config.autoAnnounceNextSong ? 'yes' : 'no',
+ 'Default Volume': config.defaultVolume,
};
let description = '';
diff --git a/src/commands/volume.ts b/src/commands/volume.ts
new file mode 100644
index 0000000..4077c40
--- /dev/null
+++ b/src/commands/volume.ts
@@ -0,0 +1,42 @@
+import {ChatInputCommandInteraction} from 'discord.js';
+import {TYPES} from '../types.js';
+import {inject, injectable} from 'inversify';
+import PlayerManager from '../managers/player.js';
+import Command from '.';
+import {SlashCommandBuilder} from '@discordjs/builders';
+
+@injectable()
+export default class implements Command {
+ public readonly slashCommand = new SlashCommandBuilder()
+ .setName('volume')
+ .setDescription('set current player volume level')
+ .addIntegerOption(option =>
+ option.setName('level')
+ .setDescription('volume percentage (0 is muted, 100 is max & default)')
+ .setMinValue(0)
+ .setMaxValue(100)
+ .setRequired(true),
+ );
+
+ public requiresVC = true;
+
+ private readonly playerManager: PlayerManager;
+
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
+ this.playerManager = playerManager;
+ }
+
+ public async execute(interaction: ChatInputCommandInteraction): Promise<void> {
+ const player = this.playerManager.get(interaction.guild!.id);
+
+ const currentSong = player.getCurrent();
+
+ if (!currentSong) {
+ throw new Error('nothing is playing');
+ }
+
+ const level = interaction.options.getInteger('level') ?? 100;
+ player.setVolume(level);
+ await interaction.reply(`Set volume to ${level}%`);
+ }
+}
diff --git a/src/inversify.config.ts b/src/inversify.config.ts
index d0694cf..a02a0a1 100644
--- a/src/inversify.config.ts
+++ b/src/inversify.config.ts
@@ -37,6 +37,7 @@ import Shuffle from './commands/shuffle.js';
import Skip from './commands/skip.js';
import Stop from './commands/stop.js';
import Unskip from './commands/unskip.js';
+import Volume from './commands/volume.js';
import ThirdParty from './services/third-party.js';
import FileCacheProvider from './services/file-cache.js';
import KeyValueCacheProvider from './services/key-value-cache.js';
@@ -85,6 +86,7 @@ container.bind<SpotifyAPI>(TYPES.Services.SpotifyAPI).to(SpotifyAPI).inSingleton
Skip,
Stop,
Unskip,
+ Volume,
].forEach(command => {
container.bind<Command>(TYPES.Command).to(command).inSingletonScope();
});
diff --git a/src/services/player.ts b/src/services/player.ts
index bfd29ad..72c7604 100644
--- a/src/services/player.ts
+++ b/src/services/player.ts
@@ -8,7 +8,7 @@ import shuffle from 'array-shuffle';
import {
AudioPlayer,
AudioPlayerState,
- AudioPlayerStatus,
+ AudioPlayerStatus, AudioResource,
createAudioPlayer,
createAudioResource, DiscordGatewayAdapterCreator,
joinVoiceChannel,
@@ -59,6 +59,8 @@ export interface PlayerEvents {
type YTDLVideoFormat = videoFormat & {loudnessDb?: number};
+export const DEFAULT_VOLUME = 100;
+
export default class {
public voiceConnection: VoiceConnection | null = null;
public status = STATUS.PAUSED;
@@ -69,6 +71,9 @@ export default class {
private queue: QueuedSong[] = [];
private queuePosition = 0;
private audioPlayer: AudioPlayer | null = null;
+ private audioResource: AudioResource | null = null;
+ private volume?: number;
+ private defaultVolume: number = DEFAULT_VOLUME;
private nowPlaying: QueuedSong | null = null;
private playPositionInterval: NodeJS.Timeout | undefined;
private lastSongURL = '';
@@ -83,6 +88,11 @@ export default class {
}
async connect(channel: VoiceChannel): Promise<void> {
+ // Always get freshest default volume setting value
+ const settings = await getGuildSettings(this.guildId);
+ const {defaultVolume = DEFAULT_VOLUME} = settings;
+ this.defaultVolume = defaultVolume;
+
this.voiceConnection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
@@ -120,6 +130,7 @@ export default class {
this.voiceConnection = null;
this.audioPlayer = null;
+ this.audioResource = null;
}
}
@@ -155,9 +166,7 @@ export default class {
},
});
this.voiceConnection.subscribe(this.audioPlayer);
- this.audioPlayer.play(createAudioResource(stream, {
- inputType: StreamType.WebmOpus,
- }));
+ this.playAudioPlayerResource(this.createAudioStream(stream));
this.attachListeners();
this.startTrackingPosition(positionSeconds);
@@ -220,11 +229,7 @@ export default class {
},
});
this.voiceConnection.subscribe(this.audioPlayer);
- const resource = createAudioResource(stream, {
- inputType: StreamType.WebmOpus,
- });
-
- this.audioPlayer.play(resource);
+ this.playAudioPlayerResource(this.createAudioStream(stream));
this.attachListeners();
@@ -408,6 +413,17 @@ export default class {
return this.queue[this.queuePosition + to];
}
+ setVolume(level: number): void {
+ // Level should be a number between 0 and 100 = 0% => 100%
+ this.volume = level;
+ this.setAudioPlayerVolume(level);
+ }
+
+ getVolume(): number {
+ // Only use default volume if player volume is not already set (in the event of a reconnect we shouldn't reset)
+ return this.volume ?? this.defaultVolume;
+ }
+
private getHashForCache(url: string): string {
return hasha(url);
}
@@ -610,4 +626,24 @@ export default class {
resolve(returnedStream);
});
}
+
+ private createAudioStream(stream: Readable) {
+ return createAudioResource(stream, {
+ inputType: StreamType.WebmOpus,
+ inlineVolume: true,
+ });
+ }
+
+ private playAudioPlayerResource(resource: AudioResource) {
+ if (this.audioPlayer !== null) {
+ this.audioResource = resource;
+ this.setAudioPlayerVolume();
+ this.audioPlayer.play(this.audioResource);
+ }
+ }
+
+ private setAudioPlayerVolume(level?: number) {
+ // Audio resource expects a float between 0 and 1 to represent level percentage
+ this.audioResource?.volume?.setVolume((level ?? this.getVolume()) / 100);
+ }
}
diff --git a/src/utils/build-embed.ts b/src/utils/build-embed.ts
index d851e3f..b8e725c 100644
--- a/src/utils/build-embed.ts
+++ b/src/utils/build-embed.ts
@@ -44,10 +44,11 @@ const getPlayerUI = (player: Player) => {
const position = player.getPosition();
const button = player.status === STATUS.PLAYING ? 'âšī¸' : 'â–ļī¸';
- const progressBar = getProgressBar(15, position / song.length);
+ const progressBar = getProgressBar(10, position / song.length);
const elapsedTime = song.isLive ? 'live' : `${prettyTime(position)}/${prettyTime(song.length)}`;
const loop = player.loopCurrentSong ? '🔂' : player.loopCurrentQueue ? '🔁' : '';
- return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉 ${loop}`;
+ const vol: string = typeof player.getVolume() === 'number' ? `${player.getVolume()!}%` : '';
+ return `${button} ${progressBar} \`[${elapsedTime}]\`🔉 ${vol} ${loop}`;
};
export const buildPlayingMessageEmbed = (player: Player): EmbedBuilder => {