Merge branch 'codetheweb:master' into master

This commit is contained in:
Oliver Traber 2023-03-07 20:47:54 +01:00 committed by GitHub
commit 2db3fd1c40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 108 additions and 45 deletions

View file

@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [2.2.1] - 2023-03-04
### Fixed
- Fixed all lint errors
- Create the guild settings when not found instead of returning an error
- Add temporary workaround to avoid VoiceConnection being stuck in signalling state
## [2.2.0] - 2023-02-26
### Added
- Added a '/replay' to restart the current song. Alias for '/seek time: 0'
## [2.1.9] - 2023-02-14 ## [2.1.9] - 2023-02-14
### Fixed ### Fixed
- Queueing a YouTube playlist sometimes resulted in an infinite loop - Queueing a YouTube playlist sometimes resulted in an infinite loop
@ -215,7 +225,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Initial release - Initial release
[unreleased]: https://github.com/codetheweb/muse/compare/v2.1.9...HEAD [unreleased]: https://github.com/codetheweb/muse/compare/v2.2.1...HEAD
[2.2.1]: https://github.com/codetheweb/muse/compare/v2.2.0...v2.2.1
[2.2.0]: https://github.com/codetheweb/muse/compare/v2.1.9...v2.2.0
[2.1.9]: https://github.com/codetheweb/muse/compare/v2.1.8...v2.1.9 [2.1.9]: https://github.com/codetheweb/muse/compare/v2.1.8...v2.1.9
[2.1.8]: https://github.com/codetheweb/muse/compare/v2.1.7...v2.1.8 [2.1.8]: https://github.com/codetheweb/muse/compare/v2.1.7...v2.1.8
[2.1.7]: https://github.com/codetheweb/muse/compare/v2.1.6...v2.1.7 [2.1.7]: https://github.com/codetheweb/muse/compare/v2.1.6...v2.1.7

View file

@ -1,6 +1,6 @@
{ {
"name": "muse", "name": "muse",
"version": "2.1.9", "version": "2.2.1",
"description": "🎧 a self-hosted Discord music bot that doesn't suck ", "description": "🎧 a self-hosted Discord music bot that doesn't suck ",
"repository": "git@github.com:codetheweb/muse.git", "repository": "git@github.com:codetheweb/muse.git",
"author": "Max Isom <hi@maxisom.me>", "author": "Max Isom <hi@maxisom.me>",

View file

@ -23,9 +23,7 @@ export default class {
private readonly commandsByName!: Collection<string, Command>; private readonly commandsByName!: Collection<string, Command>;
private readonly commandsByButtonId!: Collection<string, Command>; private readonly commandsByButtonId!: Collection<string, Command>;
constructor( constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Config) config: Config) {
@inject(TYPES.Client) client: Client,
@inject(TYPES.Config) config: Config) {
this.client = client; this.client = client;
this.config = config; this.config = config;
this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT; this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT;

View file

@ -3,6 +3,7 @@ import {ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits} from 'di
import {injectable} from 'inversify'; import {injectable} from 'inversify';
import {prisma} from '../utils/db.js'; import {prisma} from '../utils/db.js';
import Command from './index.js'; import Command from './index.js';
import {getGuildSettings} from '../utils/get-guild-settings';
@injectable() @injectable()
export default class implements Command { export default class implements Command {
@ -96,11 +97,7 @@ export default class implements Command {
case 'get': { case 'get': {
const embed = new EmbedBuilder().setTitle('Config'); const embed = new EmbedBuilder().setTitle('Config');
const config = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}}); const config = await getGuildSettings(interaction.guild!.id);
if (!config) {
throw new Error('no config found');
}
const settingsToShow = { const settingsToShow = {
'Playlist Limit': config.playlistLimit, 'Playlist Limit': config.playlistLimit,

42
src/commands/replay.ts Normal file
View 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('replay')
.setDescription('replay the current song');
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');
}
if (currentSong.isLive) {
throw new Error('can\'t replay a livestream');
}
await Promise.all([
player.seek(0),
interaction.deferReply(),
]);
await interaction.editReply('👍 replayed the current song');
}
}

View file

@ -8,20 +8,20 @@ import {REST} from '@discordjs/rest';
import {Setting} from '@prisma/client'; import {Setting} from '@prisma/client';
import registerCommandsOnGuild from '../utils/register-commands-on-guild.js'; import registerCommandsOnGuild from '../utils/register-commands-on-guild.js';
export async function createGuildSettings(guild: Guild): Promise<Setting> { export async function createGuildSettings(guildId: string): Promise<Setting> {
return prisma.setting.upsert({ return prisma.setting.upsert({
where: { where: {
guildId: guild.id, guildId,
}, },
create: { create: {
guildId: guild.id, guildId,
}, },
update: {}, update: {},
}); });
} }
export default async (guild: Guild): Promise<void> => { export default async (guild: Guild): Promise<void> => {
await createGuildSettings(guild); await createGuildSettings(guild.id);
const config = container.get<Config>(TYPES.Config); const config = container.get<Config>(TYPES.Config);

View file

@ -3,7 +3,7 @@ import container from '../inversify.config.js';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
import {getSizeWithoutBots} from '../utils/channels.js'; import {getSizeWithoutBots} from '../utils/channels.js';
import {prisma} from '../utils/db.js'; import {getGuildSettings} from '../utils/get-guild-settings';
export default async (oldState: VoiceState, _: VoiceState): Promise<void> => { export default async (oldState: VoiceState, _: VoiceState): Promise<void> => {
const playerManager = container.get<PlayerManager>(TYPES.Managers.Player); const playerManager = container.get<PlayerManager>(TYPES.Managers.Player);
@ -12,11 +12,7 @@ export default async (oldState: VoiceState, _: VoiceState): Promise<void> => {
if (player.voiceConnection) { if (player.voiceConnection) {
const voiceChannel: VoiceChannel = oldState.guild.channels.cache.get(player.voiceConnection.joinConfig.channelId!) as VoiceChannel; const voiceChannel: VoiceChannel = oldState.guild.channels.cache.get(player.voiceConnection.joinConfig.channelId!) as VoiceChannel;
const settings = await prisma.setting.findUnique({where: {guildId: player.guildId}}); const settings = await getGuildSettings(player.guildId);
if (!settings) {
throw new Error('Could not find settings for guild');
}
const {leaveIfNoListeners} = settings; const {leaveIfNoListeners} = settings;
if (!voiceChannel || (getSizeWithoutBots(voiceChannel) === 0 && leaveIfNoListeners)) { if (!voiceChannel || (getSizeWithoutBots(voiceChannel) === 0 && leaveIfNoListeners)) {

View file

@ -14,30 +14,31 @@ import GetSongs from './services/get-songs.js';
import YoutubeAPI from './services/youtube-api.js'; import YoutubeAPI from './services/youtube-api.js';
import SpotifyAPI from './services/spotify-api.js'; import SpotifyAPI from './services/spotify-api.js';
// Comands // Commands
import Command from './commands'; import Command from './commands';
import Clear from './commands/clear.js'; import Clear from './commands/clear.js';
import Config from './commands/config.js'; import Config from './commands/config.js';
import Disconnect from './commands/disconnect.js'; import Disconnect from './commands/disconnect.js';
import Favorites from './commands/favorites.js'; import Favorites from './commands/favorites.js';
import ForwardSeek from './commands/fseek.js'; import ForwardSeek from './commands/fseek.js';
import Loop from './commands/loop';
import Move from './commands/move.js'; import Move from './commands/move.js';
import Next from './commands/next.js';
import NowPlaying from './commands/now-playing.js'; import NowPlaying from './commands/now-playing.js';
import Pause from './commands/pause.js'; import Pause from './commands/pause.js';
import Play from './commands/play.js'; import Play from './commands/play.js';
import QueueCommand from './commands/queue.js'; import QueueCommand from './commands/queue.js';
import Remove from './commands/remove.js'; import Remove from './commands/remove.js';
import Replay from './commands/replay.js';
import Resume from './commands/resume.js'; import Resume from './commands/resume.js';
import Seek from './commands/seek.js'; import Seek from './commands/seek.js';
import Shuffle from './commands/shuffle.js'; import Shuffle from './commands/shuffle.js';
import Skip from './commands/skip.js'; import Skip from './commands/skip.js';
import Next from './commands/next.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 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';
import Loop from './commands/loop';
const container = new Container(); const container = new Container();
@ -67,20 +68,21 @@ container.bind<SpotifyAPI>(TYPES.Services.SpotifyAPI).to(SpotifyAPI).inSingleton
Disconnect, Disconnect,
Favorites, Favorites,
ForwardSeek, ForwardSeek,
Loop,
Move, Move,
Next,
NowPlaying, NowPlaying,
Pause, Pause,
Play, Play,
QueueCommand, QueueCommand,
Remove, Remove,
Replay,
Resume, Resume,
Seek, Seek,
Shuffle, Shuffle,
Skip, Skip,
Next,
Stop, Stop,
Unskip, Unskip,
Loop,
].forEach(command => { ].forEach(command => {
container.bind<Command>(TYPES.Command).to(command).inSingletonScope(); container.bind<Command>(TYPES.Command).to(command).inSingletonScope();
}); });

View file

@ -1,14 +1,15 @@
/* eslint-disable complexity */ /* eslint-disable complexity */
import {ChatInputCommandInteraction, GuildMember} from 'discord.js'; import {ChatInputCommandInteraction, GuildMember} from 'discord.js';
import {URL} from 'node:url';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import shuffle from 'array-shuffle'; import shuffle from 'array-shuffle';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import GetSongs from '../services/get-songs.js'; import GetSongs from '../services/get-songs.js';
import {SongMetadata, STATUS} from './player.js'; import {SongMetadata, STATUS} from './player.js';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
import {prisma} from '../utils/db.js';
import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js'; import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js';
import {getGuildSettings} from '../utils/get-guild-settings';
@injectable() @injectable()
export default class AddQueryToQueue { export default class AddQueryToQueue {
@ -34,11 +35,7 @@ export default class AddQueryToQueue {
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!); const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
const settings = await prisma.setting.findUnique({where: {guildId}}); const settings = await getGuildSettings(guildId);
if (!settings) {
throw new Error('Could not find settings for guild');
}
const {playlistLimit} = settings; const {playlistLimit} = settings;

View file

@ -1,6 +1,6 @@
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import * as spotifyURI from 'spotify-uri'; import * as spotifyURI from 'spotify-uri';
import {SongMetadata, QueuedPlaylist, MediaSource} from '../services/player.js'; import {SongMetadata, QueuedPlaylist, MediaSource} from './player';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import ffmpeg from 'fluent-ffmpeg'; import ffmpeg from 'fluent-ffmpeg';
import YoutubeAPI from './youtube-api.js'; import YoutubeAPI from './youtube-api.js';
@ -11,9 +11,7 @@ export default class {
private readonly youtubeAPI: YoutubeAPI; private readonly youtubeAPI: YoutubeAPI;
private readonly spotifyAPI: SpotifyAPI; private readonly spotifyAPI: SpotifyAPI;
constructor( constructor(@inject(TYPES.Services.YoutubeAPI) youtubeAPI: YoutubeAPI, @inject(TYPES.Services.SpotifyAPI) spotifyAPI: SpotifyAPI) {
@inject(TYPES.Services.YoutubeAPI) youtubeAPI: YoutubeAPI,
@inject(TYPES.Services.SpotifyAPI) spotifyAPI: SpotifyAPI) {
this.youtubeAPI = youtubeAPI; this.youtubeAPI = youtubeAPI;
this.spotifyAPI = spotifyAPI; this.spotifyAPI = spotifyAPI;
} }

View file

@ -18,7 +18,7 @@ import {
} from '@discordjs/voice'; } from '@discordjs/voice';
import FileCacheProvider from './file-cache.js'; import FileCacheProvider from './file-cache.js';
import debug from '../utils/debug.js'; import debug from '../utils/debug.js';
import {prisma} from '../utils/db.js'; import {getGuildSettings} from '../utils/get-guild-settings';
export enum MediaSource { export enum MediaSource {
Youtube, Youtube,
@ -84,6 +84,22 @@ export default class {
guildId: channel.guild.id, guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator, adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator,
}); });
// Workaround to disable keepAlive
this.voiceConnection.on('stateChange', (oldState, newState) => {
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
const oldNetworking = Reflect.get(oldState, 'networking');
const newNetworking = Reflect.get(newState, 'networking');
const networkStateChangeHandler = (_: any, newNetworkState: any) => {
const newUdp = Reflect.get(newNetworkState, 'udp');
clearInterval(newUdp?.keepAliveInterval);
};
oldNetworking?.off('stateChange', networkStateChangeHandler);
newNetworking?.on('stateChange', networkStateChangeHandler);
/* eslint-enable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
});
} }
disconnect(): void { disconnect(): void {
@ -256,11 +272,7 @@ export default class {
this.audioPlayer?.stop(); this.audioPlayer?.stop();
this.status = STATUS.IDLE; this.status = STATUS.IDLE;
const settings = await prisma.setting.findUnique({where: {guildId: this.guildId}}); const settings = await getGuildSettings(this.guildId);
if (!settings) {
throw new Error('Could not find settings for guild');
}
const {secondsToWaitAfterQueueEmpties} = settings; const {secondsToWaitAfterQueueEmpties} = settings;
if (secondsToWaitAfterQueueEmpties !== 0) { if (secondsToWaitAfterQueueEmpties !== 0) {

View file

@ -29,10 +29,7 @@ export default class {
private readonly ytsrQueue: PQueue; private readonly ytsrQueue: PQueue;
constructor( constructor(@inject(TYPES.ThirdParty) thirdParty: ThirdParty, @inject(TYPES.Config) config: Config, @inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider) {
@inject(TYPES.ThirdParty) thirdParty: ThirdParty,
@inject(TYPES.Config) config: Config,
@inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider) {
this.youtube = thirdParty.youtube; this.youtube = thirdParty.youtube;
this.youtubeKey = config.YOUTUBE_API_KEY; this.youtubeKey = config.YOUTUBE_API_KEY;
this.cache = cache; this.cache = cache;

View file

@ -0,0 +1,12 @@
import {Setting} from '@prisma/client';
import {prisma} from './db';
import {createGuildSettings} from '../events/guild-create';
export async function getGuildSettings(guildId: string): Promise<Setting> {
const config = await prisma.setting.findUnique({where: {guildId}});
if (!config) {
return createGuildSettings(guildId);
}
return config;
}