mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-10-18 11:25:04 +02:00
major commit
This commit is contained in:
parent
e8bc3427c5
commit
12b5526218
@ -4,6 +4,4 @@ RUN apk add ffmpeg
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["yarn", "start:prod"]
|
||||
CMD ["yarn", "start:prod"]
|
||||
|
9896
package-lock.json
generated
Normal file
9896
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellyfin-discord-music-bot",
|
||||
"version": "0.1.1",
|
||||
"version": "1.1.2",
|
||||
"description": "A simple and leightweight Discord Bot, that integrates with your Jellyfin Media server and enables you to listen to your favourite music directly from discord.",
|
||||
"author": "manuel-rw",
|
||||
"private": true,
|
||||
@ -37,8 +37,10 @@
|
||||
"@nestjs/terminus": "^9.2.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"discord.js": "^14.11.0",
|
||||
"ffmpeg": "^0.0.4",
|
||||
"joi": "^17.12.3",
|
||||
"libsodium-wrappers": "^0.7.10",
|
||||
"nest": "^0.1.6",
|
||||
"opusscript": "^0.1.1",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"rimraf": "^5.0.1",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DiscordModule } from '@discord-nestjs/core';
|
||||
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { Injectable, Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
@ -60,7 +60,6 @@ export class AppModule implements OnModuleInit {
|
||||
if (!variables.ALLOW_EVERYONE_FOR_DEFAULT_PERMS) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.warn(
|
||||
'WARNING: You are using a potentially dangerous configuration: Everyone on your server has access to your bot. Ensure, that your bot is properly secured. Disable this by setting the environment variable ALLOW_EVERYONE to false',
|
||||
);
|
||||
@ -68,4 +67,4 @@ export class AppModule implements OnModuleInit {
|
||||
'WARNING: You are using a feature, that will only work for new server invitations. The permissions on existing servers will not be changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks';
|
||||
// discord.module.ts
|
||||
import { Module, OnModuleDestroy } from '@nestjs/common';
|
||||
|
||||
import { JellyfinClientModule } from '../jellyfin/jellyfin.module';
|
||||
import { PlaybackModule } from '../../playback/playback.module';
|
||||
import { DiscordStatusModule } from './discord.status.module'; // Adjust the path as necessary
|
||||
|
||||
import { DiscordConfigService } from './discord.config.service';
|
||||
import { DiscordMessageService } from './discord.message.service';
|
||||
import { DiscordVoiceService } from './discord.voice.service';
|
||||
|
||||
@Module({
|
||||
imports: [PlaybackModule, JellyfinClientModule],
|
||||
imports: [
|
||||
PlaybackModule,
|
||||
JellyfinClientModule,
|
||||
DiscordStatusModule, // Import DiscordStatusModule here
|
||||
],
|
||||
controllers: [],
|
||||
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||
exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||
@ -20,4 +25,4 @@ export class DiscordClientModule implements OnModuleDestroy {
|
||||
onModuleDestroy() {
|
||||
this.discordVoiceService.disconnectGracefully();
|
||||
}
|
||||
}
|
||||
}
|
11
src/clients/discord/discord.status.module.ts
Normal file
11
src/clients/discord/discord.status.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// discord-status.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import StatusService from './discord.status.service';
|
||||
import { DiscordModule } from '@discord-nestjs/core';
|
||||
|
||||
@Module({
|
||||
imports: [DiscordModule.forFeature()],
|
||||
providers: [StatusService],
|
||||
exports: [StatusService],
|
||||
})
|
||||
export class DiscordStatusModule {}
|
29
src/clients/discord/discord.status.service.ts
Normal file
29
src/clients/discord/discord.status.service.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDiscordClient } from '@discord-nestjs/core';
|
||||
import { Client, ActivityType } from 'discord.js';
|
||||
|
||||
@Injectable()
|
||||
export class StatusService {
|
||||
constructor(
|
||||
@InjectDiscordClient()
|
||||
private readonly client: Client,
|
||||
) {}
|
||||
|
||||
updateStatus(text: string): void {
|
||||
this.client.user?.setPresence({
|
||||
activities: [{
|
||||
name: text,
|
||||
type: ActivityType.Listening,
|
||||
}],
|
||||
status: 'dnd',
|
||||
});
|
||||
}
|
||||
clearStatus(): void {
|
||||
this.client.user?.setPresence({
|
||||
activities: [],
|
||||
status: 'idle',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default StatusService;
|
@ -16,15 +16,14 @@ import { Injectable } from '@nestjs/common';
|
||||
import { Logger } from '@nestjs/common/services';
|
||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
|
||||
import { APIEmbed, GuildMember, InteractionEditReplyOptions, InteractionReplyOptions, MessagePayload } from 'discord.js';
|
||||
import { APIEmbed, ChannelType, Guild, GuildMember, InteractionEditReplyOptions, InteractionReplyOptions, MessagePayload, VoiceChannel } from 'discord.js';
|
||||
|
||||
import { TryResult } from '../../models/TryResult';
|
||||
import { Track } from '../../models/music/Track';
|
||||
import { PlaybackService } from '../../playback/playback.service';
|
||||
import { JellyfinStreamBuilderService } from '../jellyfin/jellyfin.stream.builder.service';
|
||||
import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service';
|
||||
|
||||
import { StatusService } from './discord.status.service';
|
||||
import { DiscordMessageService } from './discord.message.service';
|
||||
|
||||
@Injectable()
|
||||
@ -40,8 +39,8 @@ export class DiscordVoiceService {
|
||||
private readonly jellyfinWebSocketService: JellyfinWebSocketService,
|
||||
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly statusService: StatusService
|
||||
) {}
|
||||
|
||||
@OnEvent('internal.audio.track.announce')
|
||||
handleOnNewTrack(track: Track) {
|
||||
const resource = createAudioResource(
|
||||
@ -50,6 +49,9 @@ export class DiscordVoiceService {
|
||||
inlineVolume: true,
|
||||
},
|
||||
);
|
||||
this.statusService.updateStatus(track.name);
|
||||
this.logger.log(track.remoteImages);
|
||||
this.logger.log(`Stream URL: ${track.getStreamUrl(this.jellyfinStreamBuilder)}`);
|
||||
this.playResource(resource);
|
||||
}
|
||||
|
||||
@ -109,6 +111,48 @@ export class DiscordVoiceService {
|
||||
};
|
||||
}
|
||||
|
||||
async tryJoinChannelByIdAndEstablishVoiceConnection(
|
||||
guild: Guild,
|
||||
channelId: string,
|
||||
): Promise<TryResult<InteractionReplyOptions>> {
|
||||
const channel = guild.channels.cache.get(channelId) as VoiceChannel;
|
||||
if (!channel || channel.type !== ChannelType.GuildVoice) {
|
||||
return {
|
||||
success: false,
|
||||
reply: {
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Unable to join the specified channel',
|
||||
description: 'Invalid channel ID or the channel is not a voice channel.',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||
guildId: channel.guild.id,
|
||||
});
|
||||
|
||||
this.jellyfinWebSocketService.initializeAndConnect();
|
||||
|
||||
if (this.voiceConnection === undefined) {
|
||||
this.voiceConnection = getVoiceConnection(guild.id);
|
||||
}
|
||||
this.voiceConnection?.on(VoiceConnectionStatus.Disconnected, () => {
|
||||
if (this.voiceConnection !== undefined) {
|
||||
const playlist = this.playbackService.getPlaylistOrDefault().clear();
|
||||
this.disconnect();
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
reply: {},
|
||||
};
|
||||
}
|
||||
|
||||
changeVolume(volume: number) {
|
||||
if (!this.audioResource || !this.audioResource.volume) {
|
||||
this.logger.error(
|
||||
@ -128,7 +172,6 @@ export class DiscordVoiceService {
|
||||
this.createAndReturnOrGetAudioPlayer().play(resource);
|
||||
this.audioResource = resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the current audio player
|
||||
*/
|
||||
@ -153,6 +196,7 @@ export class DiscordVoiceService {
|
||||
this.eventEmitter.emit('internal.audio.track.finish', playlist.getActiveTrack());
|
||||
playlist.clear();
|
||||
}
|
||||
this.statusService.clearStatus();
|
||||
return hasStopped;
|
||||
}
|
||||
|
||||
@ -333,6 +377,7 @@ export class DiscordVoiceService {
|
||||
|
||||
if (!hasNextTrack) {
|
||||
this.logger.debug('Reached the end of the playlist');
|
||||
this.statusService.clearStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -368,4 +413,4 @@ export class DiscordVoiceService {
|
||||
`Reporting progress: ${progress} on track ${activeTrack.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -28,8 +28,8 @@ export class JellyfinService {
|
||||
version: Constants.Metadata.Version.All(),
|
||||
},
|
||||
deviceInfo: {
|
||||
id: 'jellyfin-discord-bot',
|
||||
name: 'Jellyfin Discord Bot',
|
||||
id: 'hangout-moosich',
|
||||
name: 'discord-bot',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { PreviousTrackCommand } from './previous.command';
|
||||
import { SkipTrackCommand } from './next.command';
|
||||
import { StatusCommand } from './status.command';
|
||||
import { StopPlaybackCommand } from './stop.command';
|
||||
import { SummonCommand } from './summon.command';
|
||||
import { SummonCommand } from './summon/summon.commands';
|
||||
import { PlaylistInteractionCollector } from './playlist/playlist.interaction-collector';
|
||||
import { EnqueueRandomItemsCommand } from './random/random.command';
|
||||
import { VolumeCommand } from './volume/volume.command';
|
||||
|
@ -11,8 +11,8 @@ import { PlaybackService } from 'src/playback/playback.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'disconnect',
|
||||
description: 'Join your current voice channel',
|
||||
name: 'leave',
|
||||
description: 'Leave channel and clear the current playlist',
|
||||
defaultMemberPermissions,
|
||||
})
|
||||
export class DisconnectCommand {
|
||||
@ -24,7 +24,8 @@ export class DisconnectCommand {
|
||||
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.reply({
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Disconnecting...',
|
||||
@ -49,7 +50,7 @@ export class DisconnectCommand {
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Disconnected from your channel',
|
||||
title: 'Disconnected.',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -23,16 +23,10 @@ export class HelpCommand {
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Jellyfin Discord Bot',
|
||||
description:
|
||||
'Jellyfin Discord Bot is an open source and self-hosted Discord bot, that integrates with your Jellyfin Media server and enables you to playback music from your libraries. You can use the Discord Slash Commands to invoke bot commands.',
|
||||
authorUrl: 'https://github.com/manuel-rw/jellyfin-discord-music-bot',
|
||||
'Open source music bot',
|
||||
authorUrl: 'https://jellyfin.org',
|
||||
mixin(embedBuilder) {
|
||||
return embedBuilder.addFields([
|
||||
{
|
||||
name: 'Report an issue',
|
||||
value:
|
||||
'https://github.com/manuel-rw/jellyfin-discord-music-bot/issues/new/choose',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Source code',
|
||||
value:
|
||||
|
@ -22,8 +22,9 @@ export class SkipTrackCommand {
|
||||
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||
await interaction.reply({
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
title: 'There is no next track',
|
||||
@ -34,10 +35,10 @@ export class SkipTrackCommand {
|
||||
}
|
||||
|
||||
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
||||
await interaction.reply({
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Skipped to the next track',
|
||||
title: 'Went to next track',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ import { CommandInteraction } from 'discord.js';
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
import { defaultMemberPermissions } from '../utils/environment';
|
||||
import { PlaybackService } from 'src/playback/playback.service';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
@ -18,13 +19,27 @@ export class PausePlaybackCommand {
|
||||
constructor(
|
||||
private readonly discordVoiceService: DiscordVoiceService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
private readonly playbackService: PlaybackService,
|
||||
) {}
|
||||
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
// Check if the bot is in a voice channel
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
title: "Unable to change playback state",
|
||||
description:
|
||||
'The bot is not playing any music.',
|
||||
}),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const shouldBePaused = this.discordVoiceService.togglePaused();
|
||||
|
||||
await interaction.reply({
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: shouldBePaused ? 'Paused' : 'Unpaused',
|
||||
|
@ -79,7 +79,7 @@ export class PlaylistCommand {
|
||||
await interaction.editReply({
|
||||
components: [],
|
||||
});
|
||||
}, 60 * 1000);
|
||||
}, 360 * 1000);
|
||||
}
|
||||
|
||||
private getChunks() {
|
||||
@ -140,7 +140,7 @@ export class PlaylistCommand {
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Page does not exist',
|
||||
description: 'Please pass a valid page',
|
||||
description: 'Pass a valid page',
|
||||
}),
|
||||
],
|
||||
ephemeral: true,
|
||||
|
@ -22,8 +22,9 @@ export class PreviousTrackCommand {
|
||||
|
||||
@Handler()
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||
await interaction.reply({
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
title: 'There is no previous track',
|
||||
@ -34,7 +35,7 @@ export class PreviousTrackCommand {
|
||||
}
|
||||
|
||||
this.playbackService.getPlaylistOrDefault().setPreviousTrackAsActiveTrack();
|
||||
await interaction.reply({
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Went to previous track',
|
||||
|
@ -19,7 +19,7 @@ import { Constants } from '../utils/constants';
|
||||
|
||||
@Command({
|
||||
name: 'status',
|
||||
description: 'Display the current status for troubleshooting',
|
||||
description: 'Display helpful information.',
|
||||
defaultMemberPermissions: 'ViewChannel',
|
||||
})
|
||||
@Injectable()
|
||||
@ -65,7 +65,7 @@ export class StatusCommand {
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Discord Bot Ping',
|
||||
name: 'Ping to Discord endpoint',
|
||||
value: `${ping}ms`,
|
||||
inline: true,
|
||||
},
|
||||
@ -78,18 +78,7 @@ export class StatusCommand {
|
||||
name: 'Discord Bot Uptime',
|
||||
value: `${formattedDuration}`,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: 'Jellyfin Server Version',
|
||||
value: jellyfinSystemInformation.data.Version ?? 'unknown',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Jellyfin Server Operating System',
|
||||
value:
|
||||
jellyfinSystemInformation.data.OperatingSystem ?? 'unknown',
|
||||
inline: true,
|
||||
},
|
||||
}
|
||||
]);
|
||||
},
|
||||
}),
|
||||
|
@ -30,7 +30,7 @@ export class StopPlaybackCommand {
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildErrorMessage({
|
||||
title: 'Unable to stop when nothing is playing'
|
||||
title: 'Nothing is playing you silly goose'
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -1,17 +1,14 @@
|
||||
import { Command, Handler, IA } from '@discord-nestjs/core';
|
||||
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { CommandInteraction, GuildMember } from 'discord.js';
|
||||
|
||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||
import { defaultMemberPermissions } from '../utils/environment';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'summon',
|
||||
description: 'Join your current voice channel',
|
||||
name: 'join',
|
||||
description: 'Join a specified voice channel or your current voice channel',
|
||||
defaultMemberPermissions,
|
||||
})
|
||||
export class SummonCommand {
|
||||
@ -26,26 +23,46 @@ export class SummonCommand {
|
||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||
await interaction.deferReply();
|
||||
|
||||
// Adjusted to check versions and access patterns
|
||||
let channelId: string | null = null;
|
||||
if (interaction.options.get) {
|
||||
// For discord.js v13+
|
||||
const channelOption = interaction.options.get('channel_id');
|
||||
channelId = channelOption ? channelOption.value as string : null;
|
||||
} else {
|
||||
// Fallback for older versions or unexpected behavior
|
||||
console.error('Unable to access interaction options using standard methods.');
|
||||
}
|
||||
|
||||
const guildMember = interaction.member as GuildMember;
|
||||
|
||||
const tryResult =
|
||||
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
||||
let tryResult;
|
||||
if (channelId) {
|
||||
tryResult = await this.discordVoiceService.tryJoinChannelByIdAndEstablishVoiceConnection(
|
||||
guildMember.guild,
|
||||
channelId
|
||||
);
|
||||
} else {
|
||||
tryResult = await this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
||||
guildMember,
|
||||
);
|
||||
}
|
||||
|
||||
if (!tryResult.success) {
|
||||
await interaction.editReply(tryResult.reply);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get channel name and adjust message
|
||||
const channelName = tryResult.voiceChannel.name;
|
||||
this.logger.debug(`Joined voice channel '${channelName}'`);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: 'Joined your voicehannel',
|
||||
title: `Joined voice channel '${channelName}'`,
|
||||
description:
|
||||
"I'm ready to play media. Use ``Cast to device`` in Jellyfin or the ``/play`` command to get started.",
|
||||
"",
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
74
src/commands/summon/summon.commands.ts
Normal file
74
src/commands/summon/summon.commands.ts
Normal file
@ -0,0 +1,74 @@
|
||||
// summon.command.ts
|
||||
|
||||
import { SlashCommandPipe } from '@discord-nestjs/common';
|
||||
import { Command, Handler, IA, InteractionEvent } from '@discord-nestjs/core';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CommandInteraction, GuildMember } from 'discord.js';
|
||||
|
||||
import { DiscordMessageService } from '../../clients/discord/discord.message.service';
|
||||
import { DiscordVoiceService } from '../../clients/discord/discord.voice.service';
|
||||
import { defaultMemberPermissions } from '../../utils/environment';
|
||||
import { SummonParams } from './summon.params';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
name: 'join',
|
||||
description: 'Join a specified voice channel or your current voice channel',
|
||||
defaultMemberPermissions,
|
||||
})
|
||||
export class SummonCommand {
|
||||
private readonly logger = new Logger(SummonCommand.name);
|
||||
|
||||
constructor(
|
||||
private readonly discordVoiceService: DiscordVoiceService,
|
||||
private readonly discordMessageService: DiscordMessageService,
|
||||
) {}
|
||||
|
||||
@Handler()
|
||||
async handler(
|
||||
@InteractionEvent(SlashCommandPipe) dto: SummonParams,
|
||||
@IA() interaction: CommandInteraction,
|
||||
): Promise<void> {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildMember = interaction.member as GuildMember;
|
||||
let tryResult;
|
||||
|
||||
if (dto.channelId) {
|
||||
tryResult = await this.discordVoiceService.tryJoinChannelByIdAndEstablishVoiceConnection(
|
||||
guildMember.guild,
|
||||
dto.channelId
|
||||
);
|
||||
} else {
|
||||
tryResult = await this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
||||
guildMember
|
||||
);
|
||||
}
|
||||
|
||||
if (!tryResult.success) {
|
||||
await interaction.editReply(tryResult.reply);
|
||||
return;
|
||||
}
|
||||
if (dto.channelId) {
|
||||
this.logger.debug(`Joined voice channel ${dto.channelId?.toString()}`);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: `Joined voice channel '${dto.channelId?.toString()}'`,
|
||||
description: "I'm ready to play music!"
|
||||
}),
|
||||
],
|
||||
});
|
||||
} else {
|
||||
this.logger.debug(`Joined voice channel ${guildMember.voice.channelId}`);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: `Joined voice channel '${guildMember.voice.channelId}'`,
|
||||
description: "I'm ready to play music!"
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
13
src/commands/summon/summon.params.ts
Normal file
13
src/commands/summon/summon.params.ts
Normal file
@ -0,0 +1,13 @@
|
||||
// summon.params.ts
|
||||
|
||||
import { Param, ParamType } from '@discord-nestjs/core';
|
||||
|
||||
export class SummonParams {
|
||||
@Param({
|
||||
name: 'channel_id',
|
||||
description: 'The ID of the voice channel to join',
|
||||
type: ParamType.STRING,
|
||||
required: false,
|
||||
})
|
||||
channelId?: string;
|
||||
}
|
@ -11,6 +11,7 @@ import { PlaybackService } from 'src/playback/playback.service';
|
||||
import { sleepAsync } from '../../utils/timeUtils';
|
||||
import { VolumeCommandParams } from './volume.params';
|
||||
import { defaultMemberPermissions } from '../../utils/environment';
|
||||
import { time } from 'console';
|
||||
|
||||
@Injectable()
|
||||
@Command({
|
||||
@ -32,7 +33,7 @@ export class VolumeCommand {
|
||||
@InteractionEvent(SlashCommandPipe) dto: VolumeCommandParams,
|
||||
@IA() interaction: CommandInteraction,
|
||||
): Promise<void> {
|
||||
await interaction.deferReply();
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||
await interaction.editReply({
|
||||
@ -54,16 +55,37 @@ export class VolumeCommand {
|
||||
);
|
||||
|
||||
this.discordVoiceService.changeVolume(volume);
|
||||
|
||||
// Create embed to start volume change
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: `Changing volume to ${dto.volume.toFixed(0)}%`,
|
||||
description:
|
||||
'This may take a few seconds',
|
||||
}),
|
||||
],
|
||||
});
|
||||
// Discord takes some time to react. Confirmation message should appear after actual change
|
||||
await sleepAsync(1500);
|
||||
|
||||
// Warn the user if the volume is too high
|
||||
if (dto.volume > 200) {
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: `Volume set to ${dto.volume.toFixed(0)}%`,
|
||||
description:
|
||||
'You are playing with fire. Be careful with your ears.',
|
||||
}),
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
this.discordMessageService.buildMessage({
|
||||
title: `Sucessfully set volume to ${dto.volume.toFixed(0)}%`,
|
||||
description:
|
||||
'Updating may take a few seconds to take effect.\nPlease note that listening at a high volume for a long time may damage your hearing',
|
||||
'Take care of your ears.',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -6,7 +6,6 @@ export class VolumeCommandParams {
|
||||
description: 'The desired volume',
|
||||
type: ParamType.INTEGER,
|
||||
minValue: 0,
|
||||
maxValue: 150,
|
||||
})
|
||||
volume: number;
|
||||
}
|
||||
|
17
src/main.ts
17
src/main.ts
@ -1,8 +1,10 @@
|
||||
import { LogLevel } from '@nestjs/common/services';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { StatusService } from './clients/discord/discord.status.service';
|
||||
import { time } from 'console';
|
||||
|
||||
function getLoggingLevels(): LogLevel[] {
|
||||
if (!process.env.LOG_LEVEL) {
|
||||
@ -21,7 +23,7 @@ function getLoggingLevels(): LogLevel[] {
|
||||
case 'verbose':
|
||||
return ['error', 'warn', 'log', 'debug', 'verbose'];
|
||||
default:
|
||||
console.log(`failed to process log level ${process.env.LOG_LEVEL}`);
|
||||
console.log(`Failed to process log level ${process.env.LOG_LEVEL}`);
|
||||
return ['error', 'warn', 'log'];
|
||||
}
|
||||
}
|
||||
@ -32,7 +34,14 @@ async function bootstrap() {
|
||||
app = await NestFactory.create(AppModule, {
|
||||
logger: getLoggingLevels(),
|
||||
});
|
||||
|
||||
// Retrieve the StatusService from the app's dependency injector
|
||||
const statusService = app.get(StatusService);
|
||||
|
||||
// Call clearStatus on the StatusService instance
|
||||
app.enableShutdownHooks();
|
||||
await app.listen(process.env.PORT || 3000);
|
||||
await app.listen(3002);
|
||||
statusService.clearStatus();
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
bootstrap();
|
@ -4,7 +4,6 @@ import {
|
||||
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||
|
||||
import { JellyfinStreamBuilderService } from '../../clients/jellyfin/jellyfin.stream.builder.service';
|
||||
|
||||
export class Track {
|
||||
/**
|
||||
* The identifier of this track, structured as a UID.
|
||||
@ -50,7 +49,7 @@ export class Track {
|
||||
}
|
||||
|
||||
getStreamUrl(streamBuilder: JellyfinStreamBuilderService) {
|
||||
return streamBuilder.buildStreamUrl(this.id, 96000);
|
||||
return streamBuilder.buildStreamUrl(this.id, 320000);
|
||||
}
|
||||
|
||||
getRemoteImages(): RemoteImageInfo[] {
|
||||
|
@ -1,13 +1,13 @@
|
||||
export const Constants = {
|
||||
Metadata: {
|
||||
Version: {
|
||||
Major: 0,
|
||||
Minor: 1,
|
||||
Patch: 1,
|
||||
Major: 1,
|
||||
Minor: 2,
|
||||
Patch: 5,
|
||||
All: () =>
|
||||
`${Constants.Metadata.Version.Major}.${Constants.Metadata.Version.Minor}.${Constants.Metadata.Version.Patch}`,
|
||||
},
|
||||
ApplicationName: 'Discord Jellyfin Music Bot',
|
||||
ApplicationName: 'Moosich-Hangout',
|
||||
},
|
||||
Links: {
|
||||
SourceCode: 'https://github.com/manuel-rw/jellyfin-discord-music-bot/',
|
||||
|
Loading…
Reference in New Issue
Block a user