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
|
COPY . /app
|
||||||
WORKDIR /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",
|
"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.",
|
"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",
|
"author": "manuel-rw",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -37,8 +37,10 @@
|
|||||||
"@nestjs/terminus": "^9.2.2",
|
"@nestjs/terminus": "^9.2.2",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"discord.js": "^14.11.0",
|
"discord.js": "^14.11.0",
|
||||||
|
"ffmpeg": "^0.0.4",
|
||||||
"joi": "^17.12.3",
|
"joi": "^17.12.3",
|
||||||
"libsodium-wrappers": "^0.7.10",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
|
"nest": "^0.1.6",
|
||||||
"opusscript": "^0.1.1",
|
"opusscript": "^0.1.1",
|
||||||
"reflect-metadata": "^0.1.14",
|
"reflect-metadata": "^0.1.14",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DiscordModule } from '@discord-nestjs/core';
|
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 { ConfigModule } from '@nestjs/config';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
@ -60,7 +60,6 @@ export class AppModule implements OnModuleInit {
|
|||||||
if (!variables.ALLOW_EVERYONE_FOR_DEFAULT_PERMS) {
|
if (!variables.ALLOW_EVERYONE_FOR_DEFAULT_PERMS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.warn(
|
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',
|
'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',
|
'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';
|
// discord.module.ts
|
||||||
import { OnModuleDestroy } from '@nestjs/common/interfaces/hooks';
|
import { Module, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
|
||||||
import { JellyfinClientModule } from '../jellyfin/jellyfin.module';
|
import { JellyfinClientModule } from '../jellyfin/jellyfin.module';
|
||||||
import { PlaybackModule } from '../../playback/playback.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 { DiscordConfigService } from './discord.config.service';
|
||||||
import { DiscordMessageService } from './discord.message.service';
|
import { DiscordMessageService } from './discord.message.service';
|
||||||
import { DiscordVoiceService } from './discord.voice.service';
|
import { DiscordVoiceService } from './discord.voice.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PlaybackModule, JellyfinClientModule],
|
imports: [
|
||||||
|
PlaybackModule,
|
||||||
|
JellyfinClientModule,
|
||||||
|
DiscordStatusModule, // Import DiscordStatusModule here
|
||||||
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||||
exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||||
@ -20,4 +25,4 @@ export class DiscordClientModule implements OnModuleDestroy {
|
|||||||
onModuleDestroy() {
|
onModuleDestroy() {
|
||||||
this.discordVoiceService.disconnectGracefully();
|
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 { Logger } from '@nestjs/common/services';
|
||||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
||||||
import { Interval } from '@nestjs/schedule';
|
import { Interval } from '@nestjs/schedule';
|
||||||
|
import { APIEmbed, ChannelType, Guild, GuildMember, InteractionEditReplyOptions, InteractionReplyOptions, MessagePayload, VoiceChannel } from 'discord.js';
|
||||||
import { APIEmbed, GuildMember, InteractionEditReplyOptions, InteractionReplyOptions, MessagePayload } from 'discord.js';
|
|
||||||
|
|
||||||
import { TryResult } from '../../models/TryResult';
|
import { TryResult } from '../../models/TryResult';
|
||||||
import { Track } from '../../models/music/Track';
|
import { Track } from '../../models/music/Track';
|
||||||
import { PlaybackService } from '../../playback/playback.service';
|
import { PlaybackService } from '../../playback/playback.service';
|
||||||
import { JellyfinStreamBuilderService } from '../jellyfin/jellyfin.stream.builder.service';
|
import { JellyfinStreamBuilderService } from '../jellyfin/jellyfin.stream.builder.service';
|
||||||
import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service';
|
import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service';
|
||||||
|
import { StatusService } from './discord.status.service';
|
||||||
import { DiscordMessageService } from './discord.message.service';
|
import { DiscordMessageService } from './discord.message.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -40,8 +39,8 @@ export class DiscordVoiceService {
|
|||||||
private readonly jellyfinWebSocketService: JellyfinWebSocketService,
|
private readonly jellyfinWebSocketService: JellyfinWebSocketService,
|
||||||
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
|
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
|
private readonly statusService: StatusService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnEvent('internal.audio.track.announce')
|
@OnEvent('internal.audio.track.announce')
|
||||||
handleOnNewTrack(track: Track) {
|
handleOnNewTrack(track: Track) {
|
||||||
const resource = createAudioResource(
|
const resource = createAudioResource(
|
||||||
@ -50,6 +49,9 @@ export class DiscordVoiceService {
|
|||||||
inlineVolume: true,
|
inlineVolume: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
this.statusService.updateStatus(track.name);
|
||||||
|
this.logger.log(track.remoteImages);
|
||||||
|
this.logger.log(`Stream URL: ${track.getStreamUrl(this.jellyfinStreamBuilder)}`);
|
||||||
this.playResource(resource);
|
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) {
|
changeVolume(volume: number) {
|
||||||
if (!this.audioResource || !this.audioResource.volume) {
|
if (!this.audioResource || !this.audioResource.volume) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@ -128,7 +172,6 @@ export class DiscordVoiceService {
|
|||||||
this.createAndReturnOrGetAudioPlayer().play(resource);
|
this.createAndReturnOrGetAudioPlayer().play(resource);
|
||||||
this.audioResource = resource;
|
this.audioResource = resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pauses the current audio player
|
* Pauses the current audio player
|
||||||
*/
|
*/
|
||||||
@ -153,6 +196,7 @@ export class DiscordVoiceService {
|
|||||||
this.eventEmitter.emit('internal.audio.track.finish', playlist.getActiveTrack());
|
this.eventEmitter.emit('internal.audio.track.finish', playlist.getActiveTrack());
|
||||||
playlist.clear();
|
playlist.clear();
|
||||||
}
|
}
|
||||||
|
this.statusService.clearStatus();
|
||||||
return hasStopped;
|
return hasStopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,6 +377,7 @@ export class DiscordVoiceService {
|
|||||||
|
|
||||||
if (!hasNextTrack) {
|
if (!hasNextTrack) {
|
||||||
this.logger.debug('Reached the end of the playlist');
|
this.logger.debug('Reached the end of the playlist');
|
||||||
|
this.statusService.clearStatus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,4 +413,4 @@ export class DiscordVoiceService {
|
|||||||
`Reporting progress: ${progress} on track ${activeTrack.id}`,
|
`Reporting progress: ${progress} on track ${activeTrack.id}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,8 +28,8 @@ export class JellyfinService {
|
|||||||
version: Constants.Metadata.Version.All(),
|
version: Constants.Metadata.Version.All(),
|
||||||
},
|
},
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
id: 'jellyfin-discord-bot',
|
id: 'hangout-moosich',
|
||||||
name: 'Jellyfin Discord Bot',
|
name: 'discord-bot',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import { PreviousTrackCommand } from './previous.command';
|
|||||||
import { SkipTrackCommand } from './next.command';
|
import { SkipTrackCommand } from './next.command';
|
||||||
import { StatusCommand } from './status.command';
|
import { StatusCommand } from './status.command';
|
||||||
import { StopPlaybackCommand } from './stop.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 { PlaylistInteractionCollector } from './playlist/playlist.interaction-collector';
|
||||||
import { EnqueueRandomItemsCommand } from './random/random.command';
|
import { EnqueueRandomItemsCommand } from './random/random.command';
|
||||||
import { VolumeCommand } from './volume/volume.command';
|
import { VolumeCommand } from './volume/volume.command';
|
||||||
|
@ -11,8 +11,8 @@ import { PlaybackService } from 'src/playback/playback.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Command({
|
@Command({
|
||||||
name: 'disconnect',
|
name: 'leave',
|
||||||
description: 'Join your current voice channel',
|
description: 'Leave channel and clear the current playlist',
|
||||||
defaultMemberPermissions,
|
defaultMemberPermissions,
|
||||||
})
|
})
|
||||||
export class DisconnectCommand {
|
export class DisconnectCommand {
|
||||||
@ -24,7 +24,8 @@ export class DisconnectCommand {
|
|||||||
|
|
||||||
@Handler()
|
@Handler()
|
||||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||||
await interaction.reply({
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Disconnecting...',
|
title: 'Disconnecting...',
|
||||||
@ -49,7 +50,7 @@ export class DisconnectCommand {
|
|||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Disconnected from your channel',
|
title: 'Disconnected.',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -23,16 +23,10 @@ export class HelpCommand {
|
|||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Jellyfin Discord Bot',
|
title: 'Jellyfin Discord Bot',
|
||||||
description:
|
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.',
|
'Open source music bot',
|
||||||
authorUrl: 'https://github.com/manuel-rw/jellyfin-discord-music-bot',
|
authorUrl: 'https://jellyfin.org',
|
||||||
mixin(embedBuilder) {
|
mixin(embedBuilder) {
|
||||||
return embedBuilder.addFields([
|
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',
|
name: 'Source code',
|
||||||
value:
|
value:
|
||||||
|
@ -22,8 +22,9 @@ export class SkipTrackCommand {
|
|||||||
|
|
||||||
@Handler()
|
@Handler()
|
||||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||||
await interaction.reply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildErrorMessage({
|
this.discordMessageService.buildErrorMessage({
|
||||||
title: 'There is no next track',
|
title: 'There is no next track',
|
||||||
@ -34,10 +35,10 @@ export class SkipTrackCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
this.playbackService.getPlaylistOrDefault().setNextTrackAsActiveTrack();
|
||||||
await interaction.reply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
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 { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||||
import { defaultMemberPermissions } from '../utils/environment';
|
import { defaultMemberPermissions } from '../utils/environment';
|
||||||
|
import { PlaybackService } from 'src/playback/playback.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Command({
|
@Command({
|
||||||
@ -18,13 +19,27 @@ export class PausePlaybackCommand {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly discordVoiceService: DiscordVoiceService,
|
private readonly discordVoiceService: DiscordVoiceService,
|
||||||
private readonly discordMessageService: DiscordMessageService,
|
private readonly discordMessageService: DiscordMessageService,
|
||||||
|
private readonly playbackService: PlaybackService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Handler()
|
@Handler()
|
||||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
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();
|
const shouldBePaused = this.discordVoiceService.togglePaused();
|
||||||
|
await interaction.editReply({
|
||||||
await interaction.reply({
|
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: shouldBePaused ? 'Paused' : 'Unpaused',
|
title: shouldBePaused ? 'Paused' : 'Unpaused',
|
||||||
|
@ -79,7 +79,7 @@ export class PlaylistCommand {
|
|||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
components: [],
|
components: [],
|
||||||
});
|
});
|
||||||
}, 60 * 1000);
|
}, 360 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getChunks() {
|
private getChunks() {
|
||||||
@ -140,7 +140,7 @@ export class PlaylistCommand {
|
|||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Page does not exist',
|
title: 'Page does not exist',
|
||||||
description: 'Please pass a valid page',
|
description: 'Pass a valid page',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
ephemeral: true,
|
ephemeral: true,
|
||||||
|
@ -22,8 +22,9 @@ export class PreviousTrackCommand {
|
|||||||
|
|
||||||
@Handler()
|
@Handler()
|
||||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||||
await interaction.reply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildErrorMessage({
|
this.discordMessageService.buildErrorMessage({
|
||||||
title: 'There is no previous track',
|
title: 'There is no previous track',
|
||||||
@ -34,7 +35,7 @@ export class PreviousTrackCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.playbackService.getPlaylistOrDefault().setPreviousTrackAsActiveTrack();
|
this.playbackService.getPlaylistOrDefault().setPreviousTrackAsActiveTrack();
|
||||||
await interaction.reply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Went to previous track',
|
title: 'Went to previous track',
|
||||||
|
@ -19,7 +19,7 @@ import { Constants } from '../utils/constants';
|
|||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'status',
|
name: 'status',
|
||||||
description: 'Display the current status for troubleshooting',
|
description: 'Display helpful information.',
|
||||||
defaultMemberPermissions: 'ViewChannel',
|
defaultMemberPermissions: 'ViewChannel',
|
||||||
})
|
})
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -65,7 +65,7 @@ export class StatusCommand {
|
|||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Discord Bot Ping',
|
name: 'Ping to Discord endpoint',
|
||||||
value: `${ping}ms`,
|
value: `${ping}ms`,
|
||||||
inline: true,
|
inline: true,
|
||||||
},
|
},
|
||||||
@ -78,18 +78,7 @@ export class StatusCommand {
|
|||||||
name: 'Discord Bot Uptime',
|
name: 'Discord Bot Uptime',
|
||||||
value: `${formattedDuration}`,
|
value: `${formattedDuration}`,
|
||||||
inline: false,
|
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({
|
await interaction.reply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildErrorMessage({
|
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 { Command, Handler, IA } from '@discord-nestjs/core';
|
||||||
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { CommandInteraction, GuildMember } from 'discord.js';
|
import { CommandInteraction, GuildMember } from 'discord.js';
|
||||||
|
|
||||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||||
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||||
import { defaultMemberPermissions } from '../utils/environment';
|
import { defaultMemberPermissions } from '../utils/environment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Command({
|
@Command({
|
||||||
name: 'summon',
|
name: 'join',
|
||||||
description: 'Join your current voice channel',
|
description: 'Join a specified voice channel or your current voice channel',
|
||||||
defaultMemberPermissions,
|
defaultMemberPermissions,
|
||||||
})
|
})
|
||||||
export class SummonCommand {
|
export class SummonCommand {
|
||||||
@ -26,26 +23,46 @@ export class SummonCommand {
|
|||||||
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
async handler(@IA() interaction: CommandInteraction): Promise<void> {
|
||||||
await interaction.deferReply();
|
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 guildMember = interaction.member as GuildMember;
|
||||||
|
|
||||||
const tryResult =
|
let tryResult;
|
||||||
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
if (channelId) {
|
||||||
|
tryResult = await this.discordVoiceService.tryJoinChannelByIdAndEstablishVoiceConnection(
|
||||||
|
guildMember.guild,
|
||||||
|
channelId
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tryResult = await this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
||||||
guildMember,
|
guildMember,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!tryResult.success) {
|
if (!tryResult.success) {
|
||||||
await interaction.editReply(tryResult.reply);
|
await interaction.editReply(tryResult.reply);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Get channel name and adjust message
|
||||||
|
const channelName = tryResult.voiceChannel.name;
|
||||||
|
this.logger.debug(`Joined voice channel '${channelName}'`);
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Joined your voicehannel',
|
title: `Joined voice channel '${channelName}'`,
|
||||||
description:
|
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 { sleepAsync } from '../../utils/timeUtils';
|
||||||
import { VolumeCommandParams } from './volume.params';
|
import { VolumeCommandParams } from './volume.params';
|
||||||
import { defaultMemberPermissions } from '../../utils/environment';
|
import { defaultMemberPermissions } from '../../utils/environment';
|
||||||
|
import { time } from 'console';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@Command({
|
@Command({
|
||||||
@ -32,7 +33,7 @@ export class VolumeCommand {
|
|||||||
@InteractionEvent(SlashCommandPipe) dto: VolumeCommandParams,
|
@InteractionEvent(SlashCommandPipe) dto: VolumeCommandParams,
|
||||||
@IA() interaction: CommandInteraction,
|
@IA() interaction: CommandInteraction,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await interaction.deferReply();
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
if (!this.playbackService.getPlaylistOrDefault().hasActiveTrack()) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
@ -54,16 +55,37 @@ export class VolumeCommand {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.discordVoiceService.changeVolume(volume);
|
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
|
// Discord takes some time to react. Confirmation message should appear after actual change
|
||||||
await sleepAsync(1500);
|
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({
|
await interaction.editReply({
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: `Sucessfully set volume to ${dto.volume.toFixed(0)}%`,
|
title: `Sucessfully set volume to ${dto.volume.toFixed(0)}%`,
|
||||||
description:
|
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',
|
description: 'The desired volume',
|
||||||
type: ParamType.INTEGER,
|
type: ParamType.INTEGER,
|
||||||
minValue: 0,
|
minValue: 0,
|
||||||
maxValue: 150,
|
|
||||||
})
|
})
|
||||||
volume: number;
|
volume: number;
|
||||||
}
|
}
|
||||||
|
17
src/main.ts
17
src/main.ts
@ -1,8 +1,10 @@
|
|||||||
import { LogLevel } from '@nestjs/common/services';
|
import { LogLevel } from '@nestjs/common/services';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
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[] {
|
function getLoggingLevels(): LogLevel[] {
|
||||||
if (!process.env.LOG_LEVEL) {
|
if (!process.env.LOG_LEVEL) {
|
||||||
@ -21,7 +23,7 @@ function getLoggingLevels(): LogLevel[] {
|
|||||||
case 'verbose':
|
case 'verbose':
|
||||||
return ['error', 'warn', 'log', 'debug', 'verbose'];
|
return ['error', 'warn', 'log', 'debug', 'verbose'];
|
||||||
default:
|
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'];
|
return ['error', 'warn', 'log'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,7 +34,14 @@ async function bootstrap() {
|
|||||||
app = await NestFactory.create(AppModule, {
|
app = await NestFactory.create(AppModule, {
|
||||||
logger: getLoggingLevels(),
|
logger: getLoggingLevels(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Retrieve the StatusService from the app's dependency injector
|
||||||
|
const statusService = app.get(StatusService);
|
||||||
|
|
||||||
|
// Call clearStatus on the StatusService instance
|
||||||
app.enableShutdownHooks();
|
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';
|
} from '@jellyfin/sdk/lib/generated-client/models';
|
||||||
|
|
||||||
import { JellyfinStreamBuilderService } from '../../clients/jellyfin/jellyfin.stream.builder.service';
|
import { JellyfinStreamBuilderService } from '../../clients/jellyfin/jellyfin.stream.builder.service';
|
||||||
|
|
||||||
export class Track {
|
export class Track {
|
||||||
/**
|
/**
|
||||||
* The identifier of this track, structured as a UID.
|
* The identifier of this track, structured as a UID.
|
||||||
@ -50,7 +49,7 @@ export class Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getStreamUrl(streamBuilder: JellyfinStreamBuilderService) {
|
getStreamUrl(streamBuilder: JellyfinStreamBuilderService) {
|
||||||
return streamBuilder.buildStreamUrl(this.id, 96000);
|
return streamBuilder.buildStreamUrl(this.id, 320000);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRemoteImages(): RemoteImageInfo[] {
|
getRemoteImages(): RemoteImageInfo[] {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
export const Constants = {
|
export const Constants = {
|
||||||
Metadata: {
|
Metadata: {
|
||||||
Version: {
|
Version: {
|
||||||
Major: 0,
|
Major: 1,
|
||||||
Minor: 1,
|
Minor: 2,
|
||||||
Patch: 1,
|
Patch: 5,
|
||||||
All: () =>
|
All: () =>
|
||||||
`${Constants.Metadata.Version.Major}.${Constants.Metadata.Version.Minor}.${Constants.Metadata.Version.Patch}`,
|
`${Constants.Metadata.Version.Major}.${Constants.Metadata.Version.Minor}.${Constants.Metadata.Version.Patch}`,
|
||||||
},
|
},
|
||||||
ApplicationName: 'Discord Jellyfin Music Bot',
|
ApplicationName: 'Moosich-Hangout',
|
||||||
},
|
},
|
||||||
Links: {
|
Links: {
|
||||||
SourceCode: 'https://github.com/manuel-rw/jellyfin-discord-music-bot/',
|
SourceCode: 'https://github.com/manuel-rw/jellyfin-discord-music-bot/',
|
||||||
|
Loading…
Reference in New Issue
Block a user