mirror of
https://github.com/BluemediaDev/muse.git
synced 2025-06-27 01:02:41 +02:00
Co-authored-by: Max Isom <hi@maxisom.me>
This commit is contained in:
parent
786e6fd8f5
commit
6baaffb730
8 changed files with 141 additions and 11 deletions
|
@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added 🔊
|
||||||
|
- A `/volume` command is now available.
|
||||||
|
- Set the default volume with `/config set-default-volume`
|
||||||
|
|
||||||
## [2.6.0] - 2024-03-03
|
## [2.6.0] - 2024-03-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
17
migrations/20240312135407_add_default_volume/migration.sql
Normal file
17
migrations/20240312135407_add_default_volume/migration.sql
Normal file
|
@ -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;
|
|
@ -29,6 +29,7 @@ model Setting {
|
||||||
secondsToWaitAfterQueueEmpties Int @default(30)
|
secondsToWaitAfterQueueEmpties Int @default(30)
|
||||||
leaveIfNoListeners Boolean @default(true)
|
leaveIfNoListeners Boolean @default(true)
|
||||||
autoAnnounceNextSong Boolean @default(false)
|
autoAnnounceNextSong Boolean @default(false)
|
||||||
|
defaultVolume Int @default(100)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,15 @@ export default class implements Command {
|
||||||
.setName('value')
|
.setName('value')
|
||||||
.setDescription('whether to announce the next song in the queue automatically')
|
.setDescription('whether to announce the next song in the queue automatically')
|
||||||
.setRequired(true)))
|
.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
|
.addSubcommand(subcommand => subcommand
|
||||||
.setName('get')
|
.setName('get')
|
||||||
.setDescription('show all settings'));
|
.setDescription('show all settings'));
|
||||||
|
@ -121,6 +130,23 @@ export default class implements Command {
|
||||||
break;
|
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': {
|
case 'get': {
|
||||||
const embed = new EmbedBuilder().setTitle('Config');
|
const embed = new EmbedBuilder().setTitle('Config');
|
||||||
|
|
||||||
|
@ -133,6 +159,7 @@ export default class implements Command {
|
||||||
: `${config.secondsToWaitAfterQueueEmpties}s`,
|
: `${config.secondsToWaitAfterQueueEmpties}s`,
|
||||||
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
|
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
|
||||||
'Auto announce next song in queue': config.autoAnnounceNextSong ? 'yes' : 'no',
|
'Auto announce next song in queue': config.autoAnnounceNextSong ? 'yes' : 'no',
|
||||||
|
'Default Volume': config.defaultVolume,
|
||||||
};
|
};
|
||||||
|
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
42
src/commands/volume.ts
Normal file
42
src/commands/volume.ts
Normal file
|
@ -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}%`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import Shuffle from './commands/shuffle.js';
|
||||||
import Skip from './commands/skip.js';
|
import Skip from './commands/skip.js';
|
||||||
import Stop from './commands/stop.js';
|
import Stop from './commands/stop.js';
|
||||||
import Unskip from './commands/unskip.js';
|
import Unskip from './commands/unskip.js';
|
||||||
|
import Volume from './commands/volume.js';
|
||||||
import ThirdParty from './services/third-party.js';
|
import ThirdParty from './services/third-party.js';
|
||||||
import FileCacheProvider from './services/file-cache.js';
|
import FileCacheProvider from './services/file-cache.js';
|
||||||
import KeyValueCacheProvider from './services/key-value-cache.js';
|
import KeyValueCacheProvider from './services/key-value-cache.js';
|
||||||
|
@ -85,6 +86,7 @@ container.bind<SpotifyAPI>(TYPES.Services.SpotifyAPI).to(SpotifyAPI).inSingleton
|
||||||
Skip,
|
Skip,
|
||||||
Stop,
|
Stop,
|
||||||
Unskip,
|
Unskip,
|
||||||
|
Volume,
|
||||||
].forEach(command => {
|
].forEach(command => {
|
||||||
container.bind<Command>(TYPES.Command).to(command).inSingletonScope();
|
container.bind<Command>(TYPES.Command).to(command).inSingletonScope();
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ import shuffle from 'array-shuffle';
|
||||||
import {
|
import {
|
||||||
AudioPlayer,
|
AudioPlayer,
|
||||||
AudioPlayerState,
|
AudioPlayerState,
|
||||||
AudioPlayerStatus,
|
AudioPlayerStatus, AudioResource,
|
||||||
createAudioPlayer,
|
createAudioPlayer,
|
||||||
createAudioResource, DiscordGatewayAdapterCreator,
|
createAudioResource, DiscordGatewayAdapterCreator,
|
||||||
joinVoiceChannel,
|
joinVoiceChannel,
|
||||||
|
@ -59,6 +59,8 @@ export interface PlayerEvents {
|
||||||
|
|
||||||
type YTDLVideoFormat = videoFormat & {loudnessDb?: number};
|
type YTDLVideoFormat = videoFormat & {loudnessDb?: number};
|
||||||
|
|
||||||
|
export const DEFAULT_VOLUME = 100;
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
public voiceConnection: VoiceConnection | null = null;
|
public voiceConnection: VoiceConnection | null = null;
|
||||||
public status = STATUS.PAUSED;
|
public status = STATUS.PAUSED;
|
||||||
|
@ -69,6 +71,9 @@ export default class {
|
||||||
private queue: QueuedSong[] = [];
|
private queue: QueuedSong[] = [];
|
||||||
private queuePosition = 0;
|
private queuePosition = 0;
|
||||||
private audioPlayer: AudioPlayer | null = null;
|
private audioPlayer: AudioPlayer | null = null;
|
||||||
|
private audioResource: AudioResource | null = null;
|
||||||
|
private volume?: number;
|
||||||
|
private defaultVolume: number = DEFAULT_VOLUME;
|
||||||
private nowPlaying: QueuedSong | null = null;
|
private nowPlaying: QueuedSong | null = null;
|
||||||
private playPositionInterval: NodeJS.Timeout | undefined;
|
private playPositionInterval: NodeJS.Timeout | undefined;
|
||||||
private lastSongURL = '';
|
private lastSongURL = '';
|
||||||
|
@ -83,6 +88,11 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(channel: VoiceChannel): Promise<void> {
|
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({
|
this.voiceConnection = joinVoiceChannel({
|
||||||
channelId: channel.id,
|
channelId: channel.id,
|
||||||
guildId: channel.guild.id,
|
guildId: channel.guild.id,
|
||||||
|
@ -120,6 +130,7 @@ export default class {
|
||||||
|
|
||||||
this.voiceConnection = null;
|
this.voiceConnection = null;
|
||||||
this.audioPlayer = null;
|
this.audioPlayer = null;
|
||||||
|
this.audioResource = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,9 +166,7 @@ export default class {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.voiceConnection.subscribe(this.audioPlayer);
|
this.voiceConnection.subscribe(this.audioPlayer);
|
||||||
this.audioPlayer.play(createAudioResource(stream, {
|
this.playAudioPlayerResource(this.createAudioStream(stream));
|
||||||
inputType: StreamType.WebmOpus,
|
|
||||||
}));
|
|
||||||
this.attachListeners();
|
this.attachListeners();
|
||||||
this.startTrackingPosition(positionSeconds);
|
this.startTrackingPosition(positionSeconds);
|
||||||
|
|
||||||
|
@ -220,11 +229,7 @@ export default class {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.voiceConnection.subscribe(this.audioPlayer);
|
this.voiceConnection.subscribe(this.audioPlayer);
|
||||||
const resource = createAudioResource(stream, {
|
this.playAudioPlayerResource(this.createAudioStream(stream));
|
||||||
inputType: StreamType.WebmOpus,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.audioPlayer.play(resource);
|
|
||||||
|
|
||||||
this.attachListeners();
|
this.attachListeners();
|
||||||
|
|
||||||
|
@ -408,6 +413,17 @@ export default class {
|
||||||
return this.queue[this.queuePosition + to];
|
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 {
|
private getHashForCache(url: string): string {
|
||||||
return hasha(url);
|
return hasha(url);
|
||||||
}
|
}
|
||||||
|
@ -610,4 +626,24 @@ export default class {
|
||||||
resolve(returnedStream);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,10 +44,11 @@ const getPlayerUI = (player: Player) => {
|
||||||
|
|
||||||
const position = player.getPosition();
|
const position = player.getPosition();
|
||||||
const button = player.status === STATUS.PLAYING ? '⏹️' : '▶️';
|
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 elapsedTime = song.isLive ? 'live' : `${prettyTime(position)}/${prettyTime(song.length)}`;
|
||||||
const loop = player.loopCurrentSong ? '🔂' : player.loopCurrentQueue ? '🔁' : '';
|
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 => {
|
export const buildPlayingMessageEmbed = (player: Player): EmbedBuilder => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue