mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-23 18:21:55 +01:00
✨ Add universal audio streaming endpoint
This commit is contained in:
parent
2aa2d16e40
commit
fe5096acf7
@ -2,6 +2,9 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { APIEmbed, EmbedBuilder } from 'discord.js';
|
import { APIEmbed, EmbedBuilder } from 'discord.js';
|
||||||
import { DefaultJellyfinColor, ErrorJellyfinColor } from '../../types/colors';
|
import { DefaultJellyfinColor, ErrorJellyfinColor } from '../../types/colors';
|
||||||
|
|
||||||
|
import { formatRFC7231 } from 'date-fns';
|
||||||
|
import { Constants } from '../../utils/constants';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DiscordMessageService {
|
export class DiscordMessageService {
|
||||||
buildErrorMessage({
|
buildErrorMessage({
|
||||||
@ -11,40 +14,48 @@ export class DiscordMessageService {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}): APIEmbed {
|
}): APIEmbed {
|
||||||
const embedBuilder = new EmbedBuilder()
|
const date = formatRFC7231(new Date());
|
||||||
.setColor(ErrorJellyfinColor)
|
return this.buildMessage({
|
||||||
.setAuthor({
|
title: title,
|
||||||
name: title,
|
description: description,
|
||||||
iconURL:
|
mixin(embedBuilder) {
|
||||||
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/alert-circle.png?raw=true',
|
return embedBuilder
|
||||||
|
.setFooter({
|
||||||
|
text: `${date} - Report an issue: ${Constants.Links.ReportIssue}`,
|
||||||
|
})
|
||||||
|
.setColor(ErrorJellyfinColor);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (description !== undefined) {
|
|
||||||
embedBuilder.setDescription(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
return embedBuilder.toJSON();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildMessage({
|
buildMessage({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
mixin = (builder) => builder,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
mixin?: (embedBuilder: EmbedBuilder) => EmbedBuilder;
|
||||||
}): APIEmbed {
|
}): APIEmbed {
|
||||||
const embedBuilder = new EmbedBuilder()
|
const date = formatRFC7231(new Date());
|
||||||
|
|
||||||
|
let embedBuilder = new EmbedBuilder()
|
||||||
.setColor(DefaultJellyfinColor)
|
.setColor(DefaultJellyfinColor)
|
||||||
.setAuthor({
|
.setAuthor({
|
||||||
name: title,
|
name: title,
|
||||||
iconURL:
|
iconURL:
|
||||||
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/circle-check.png?raw=true',
|
'https://github.com/manuel-rw/jellyfin-discord-music-bot/blob/nestjs-migration/images/icons/circle-check.png?raw=true',
|
||||||
|
})
|
||||||
|
.setFooter({
|
||||||
|
text: `${date}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (description !== undefined) {
|
if (description !== undefined && description.length > 0) {
|
||||||
embedBuilder.setDescription(description);
|
embedBuilder = embedBuilder.setDescription(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
embedBuilder = mixin(embedBuilder);
|
||||||
|
|
||||||
return embedBuilder.toJSON();
|
return embedBuilder.toJSON();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import { DiscordVoiceService } from './discord.voice.service';
|
|||||||
imports: [],
|
imports: [],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
providers: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||||
exports: [DiscordConfigService, DiscordMessageService],
|
exports: [DiscordConfigService, DiscordVoiceService, DiscordMessageService],
|
||||||
})
|
})
|
||||||
export class DiscordClientModule implements OnModuleDestroy {
|
export class DiscordClientModule implements OnModuleDestroy {
|
||||||
constructor(private readonly discordVoiceService: DiscordVoiceService) {}
|
constructor(private readonly discordVoiceService: DiscordVoiceService) {}
|
||||||
|
@ -1,10 +1,81 @@
|
|||||||
import { getVoiceConnections } from '@discordjs/voice';
|
import {
|
||||||
|
AudioPlayer,
|
||||||
|
AudioResource,
|
||||||
|
createAudioPlayer,
|
||||||
|
getVoiceConnection,
|
||||||
|
getVoiceConnections,
|
||||||
|
joinVoiceChannel,
|
||||||
|
VoiceConnection,
|
||||||
|
} from '@discordjs/voice';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Logger } from '@nestjs/common/services';
|
import { Logger } from '@nestjs/common/services';
|
||||||
|
import { GuildMember } from 'discord.js';
|
||||||
|
import { GenericTryHandler } from '../../models/generic-try-handler';
|
||||||
|
import { DiscordMessageService } from './discord.message.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DiscordVoiceService {
|
export class DiscordVoiceService {
|
||||||
private readonly logger = new Logger(DiscordVoiceService.name);
|
private readonly logger = new Logger(DiscordVoiceService.name);
|
||||||
|
private audioPlayer: AudioPlayer;
|
||||||
|
private voiceConnection: VoiceConnection;
|
||||||
|
|
||||||
|
constructor(private readonly discordMessageService: DiscordMessageService) {}
|
||||||
|
|
||||||
|
tryJoinChannelAndEstablishVoiceConnection(
|
||||||
|
member: GuildMember,
|
||||||
|
): GenericTryHandler {
|
||||||
|
if (this.voiceConnection !== undefined) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reply: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (member.voice.channel === null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reply: {
|
||||||
|
embeds: [
|
||||||
|
this.discordMessageService.buildErrorMessage({
|
||||||
|
title: 'Unable to join your channel',
|
||||||
|
description:
|
||||||
|
"I am unable to join your channel, because you don't seem to be in a voice channel. Connect to a channel first to use this command",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = member.voice.channel;
|
||||||
|
|
||||||
|
joinVoiceChannel({
|
||||||
|
channelId: channel.id,
|
||||||
|
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||||
|
guildId: channel.guildId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.voiceConnection == undefined) {
|
||||||
|
this.voiceConnection = getVoiceConnection(member.guild.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
reply: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
playResource(resource: AudioResource<unknown>) {
|
||||||
|
this.createAndReturnOrGetAudioPlayer().play(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
this.createAndReturnOrGetAudioPlayer().pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
unpause() {
|
||||||
|
this.createAndReturnOrGetAudioPlayer().unpause();
|
||||||
|
}
|
||||||
|
|
||||||
disconnectGracefully() {
|
disconnectGracefully() {
|
||||||
const connections = getVoiceConnections();
|
const connections = getVoiceConnections();
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
@ -17,4 +88,23 @@ export class DiscordVoiceService {
|
|||||||
connection.destroy();
|
connection.destroy();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createAndReturnOrGetAudioPlayer() {
|
||||||
|
if (this.audioPlayer === undefined) {
|
||||||
|
this.logger.debug(
|
||||||
|
`Initialized new instance of Audio Player because it has not been defined yet`,
|
||||||
|
);
|
||||||
|
this.audioPlayer = createAudioPlayer();
|
||||||
|
this.audioPlayer.on('debug', (message) => {
|
||||||
|
this.logger.debug(message);
|
||||||
|
});
|
||||||
|
this.audioPlayer.on('error', (message) => {
|
||||||
|
this.logger.error(message);
|
||||||
|
});
|
||||||
|
this.voiceConnection.subscribe(this.audioPlayer);
|
||||||
|
return this.audioPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.audioPlayer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
import { JellyfinSearchService } from './jellyfin.search.service';
|
import { JellyfinSearchService } from './jellyfin.search.service';
|
||||||
import { JellyfinService } from './jellyfin.service';
|
import { JellyfinService } from './jellyfin.service';
|
||||||
|
import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service';
|
||||||
import { JellyinWebsocketService } from './jellyfin.websocket.service';
|
import { JellyinWebsocketService } from './jellyfin.websocket.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [JellyfinService, JellyinWebsocketService, JellyfinSearchService],
|
providers: [
|
||||||
exports: [JellyfinService, JellyfinSearchService],
|
JellyfinService,
|
||||||
|
JellyinWebsocketService,
|
||||||
|
JellyfinSearchService,
|
||||||
|
JellyfinStreamBuilderService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
JellyfinService,
|
||||||
|
JellyfinSearchService,
|
||||||
|
JellyfinStreamBuilderService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class JellyfinClientModule implements OnModuleInit, OnModuleDestroy {
|
export class JellyfinClientModule implements OnModuleInit, OnModuleDestroy {
|
||||||
constructor(
|
constructor(
|
||||||
|
31
src/clients/jellyfin/jellyfin.stream.builder.service.ts
Normal file
31
src/clients/jellyfin/jellyfin.stream.builder.service.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { JellyfinService } from './jellyfin.service';
|
||||||
|
|
||||||
|
import { getUniversalAudioApi } from '@jellyfin/sdk/lib/utils/api/universal-audio-api';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JellyfinStreamBuilderService {
|
||||||
|
private readonly logger = new Logger(JellyfinStreamBuilderService.name);
|
||||||
|
|
||||||
|
constructor(private readonly jellyfinService: JellyfinService) {}
|
||||||
|
|
||||||
|
async buildStreamUrl(jellyfinItemId: string, bitrate: number) {
|
||||||
|
const api = this.jellyfinService.getApi();
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Attempting to build stream resource for item ${jellyfinItemId} with bitrate ${bitrate}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const accessToken = this.jellyfinService.getApi().accessToken;
|
||||||
|
|
||||||
|
const url = encodeURI(
|
||||||
|
`${
|
||||||
|
api.basePath
|
||||||
|
}/Audio/${jellyfinItemId}/universal?UserId=${this.jellyfinService.getUserId()}&DeviceId=${
|
||||||
|
this.jellyfinService.getJellyfin().clientInfo.name
|
||||||
|
}&MaxStreamingBitrate=${bitrate}&Container=ogg,opus&AudioCodec=opus&TranscodingContainer=ts&TranscodingProtocol=hls&api_key=${accessToken}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { DiscordModule } from '@discord-nestjs/core';
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||||
|
import { DiscordClientModule } from '../clients/discord/discord.module';
|
||||||
import { JellyfinClientModule } from '../clients/jellyfin/jellyfin.module';
|
import { JellyfinClientModule } from '../clients/jellyfin/jellyfin.module';
|
||||||
import { PlaybackService } from '../playback/playback.service';
|
import { PlaybackService } from '../playback/playback.service';
|
||||||
import { CurrentTrackCommand } from './current.command';
|
import { CurrentTrackCommand } from './current.command';
|
||||||
@ -17,7 +18,11 @@ import { StopPlaybackCommand } from './stop.command';
|
|||||||
import { SummonCommand } from './summon.command';
|
import { SummonCommand } from './summon.command';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DiscordModule.forFeature(), JellyfinClientModule],
|
imports: [
|
||||||
|
DiscordModule.forFeature(),
|
||||||
|
JellyfinClientModule,
|
||||||
|
DiscordClientModule,
|
||||||
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [
|
providers: [
|
||||||
HelpCommand,
|
HelpCommand,
|
||||||
|
@ -2,24 +2,39 @@ import { TransformPipe } from '@discord-nestjs/common';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
|
CommandExecutionContext,
|
||||||
|
DiscordCommand,
|
||||||
DiscordTransformedCommand,
|
DiscordTransformedCommand,
|
||||||
TransformedCommandExecutionContext,
|
TransformedCommandExecutionContext,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
} from '@discord-nestjs/core';
|
} from '@discord-nestjs/core';
|
||||||
import { InteractionReplyOptions } from 'discord.js';
|
import {
|
||||||
|
ButtonInteraction,
|
||||||
|
CacheType,
|
||||||
|
ChatInputCommandInteraction,
|
||||||
|
ContextMenuCommandInteraction,
|
||||||
|
Interaction,
|
||||||
|
InteractionReplyOptions,
|
||||||
|
MessagePayload,
|
||||||
|
StringSelectMenuInteraction,
|
||||||
|
} from 'discord.js';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'pause',
|
name: 'pause',
|
||||||
description: 'Pause or resume the playback of the current track',
|
description: 'Pause or resume the playback of the current track',
|
||||||
})
|
})
|
||||||
@UsePipes(TransformPipe)
|
@UsePipes(TransformPipe)
|
||||||
export class PausePlaybackCommand
|
export class PausePlaybackCommand implements DiscordCommand {
|
||||||
implements DiscordTransformedCommand<unknown>
|
|
||||||
{
|
|
||||||
handler(
|
handler(
|
||||||
dto: unknown,
|
interaction:
|
||||||
executionContext: TransformedCommandExecutionContext<any>,
|
| ChatInputCommandInteraction<CacheType>
|
||||||
): InteractionReplyOptions | string {
|
| ContextMenuCommandInteraction<CacheType>,
|
||||||
return 'nice';
|
executionContext: CommandExecutionContext<
|
||||||
|
StringSelectMenuInteraction<CacheType> | ButtonInteraction<CacheType>
|
||||||
|
>,
|
||||||
|
): string | InteractionReplyOptions {
|
||||||
|
return {
|
||||||
|
content: 'test',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,23 +6,14 @@ import {
|
|||||||
TransformedCommandExecutionContext,
|
TransformedCommandExecutionContext,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
} from '@discord-nestjs/core';
|
} from '@discord-nestjs/core';
|
||||||
import {
|
import { GuildMember, InteractionReplyOptions } from 'discord.js';
|
||||||
EmbedBuilder,
|
|
||||||
GuildMember,
|
|
||||||
InteractionReplyOptions,
|
|
||||||
MessagePayload,
|
|
||||||
} from 'discord.js';
|
|
||||||
|
|
||||||
|
import { createAudioResource } from '@discordjs/voice';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrackRequestDto } from '../models/track-request.dto';
|
|
||||||
import {
|
|
||||||
createAudioPlayer,
|
|
||||||
createAudioResource,
|
|
||||||
getVoiceConnection,
|
|
||||||
joinVoiceChannel,
|
|
||||||
} from '@discordjs/voice';
|
|
||||||
import { Logger } from '@nestjs/common/services';
|
import { Logger } from '@nestjs/common/services';
|
||||||
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 { TrackRequestDto } from '../models/track-request.dto';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'play',
|
name: 'play',
|
||||||
@ -33,64 +24,37 @@ import { DiscordMessageService } from '../clients/discord/discord.message.servic
|
|||||||
export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
|
export class PlayCommand implements DiscordTransformedCommand<TrackRequestDto> {
|
||||||
private readonly logger = new Logger(PlayCommand.name);
|
private readonly logger = new Logger(PlayCommand.name);
|
||||||
|
|
||||||
constructor(private readonly discordMessageService: DiscordMessageService) {}
|
constructor(
|
||||||
|
private readonly discordMessageService: DiscordMessageService,
|
||||||
|
private readonly discordVoiceService: DiscordVoiceService,
|
||||||
|
) {}
|
||||||
|
|
||||||
handler(
|
handler(
|
||||||
@Payload() dto: TrackRequestDto,
|
@Payload() dto: TrackRequestDto,
|
||||||
executionContext: TransformedCommandExecutionContext<any>,
|
executionContext: TransformedCommandExecutionContext<any>,
|
||||||
):
|
):
|
||||||
| string
|
| string
|
||||||
| void
|
|
||||||
| MessagePayload
|
|
||||||
| InteractionReplyOptions
|
| InteractionReplyOptions
|
||||||
| Promise<string | void | MessagePayload | InteractionReplyOptions> {
|
| Promise<string | InteractionReplyOptions> {
|
||||||
const guildMember = executionContext.interaction.member as GuildMember;
|
const guildMember = executionContext.interaction.member as GuildMember;
|
||||||
|
|
||||||
if (guildMember.voice.channel === null) {
|
const joinVoiceChannel =
|
||||||
|
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
||||||
|
guildMember,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!joinVoiceChannel.success) {
|
||||||
|
return joinVoiceChannel.reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.discordVoiceService.playResource(createAudioResource(dto.search));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
embeds: [
|
embeds: [
|
||||||
this.discordMessageService.buildErrorMessage({
|
this.discordMessageService.buildMessage({
|
||||||
title: 'Unable to join your channel',
|
title: `Playing ${dto.search}`,
|
||||||
description:
|
|
||||||
'You are in a channel, I am either unabelt to connect to or you aren&apost in a channel yet',
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = guildMember.voice.channel;
|
|
||||||
|
|
||||||
joinVoiceChannel({
|
|
||||||
channelId: channel.id,
|
|
||||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
|
||||||
guildId: channel.guildId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const connection = getVoiceConnection(executionContext.interaction.guildId);
|
|
||||||
|
|
||||||
if (!connection) {
|
|
||||||
return {
|
|
||||||
embeds: [
|
|
||||||
this.discordMessageService.buildErrorMessage({
|
|
||||||
title: 'Unable to establish audio connection',
|
|
||||||
description:
|
|
||||||
'I was unable to establish an audio connection to your voice channel',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = createAudioPlayer();
|
|
||||||
|
|
||||||
const resource = createAudioResource(dto.search);
|
|
||||||
|
|
||||||
connection.subscribe(player);
|
|
||||||
|
|
||||||
player.play(resource);
|
|
||||||
player.unpause();
|
|
||||||
|
|
||||||
return {
|
|
||||||
embeds: [new EmbedBuilder().setTitle(`Playing ${dto.search}`).toJSON()],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,12 @@ import {
|
|||||||
TransformedCommandExecutionContext,
|
TransformedCommandExecutionContext,
|
||||||
UsePipes,
|
UsePipes,
|
||||||
} from '@discord-nestjs/core';
|
} from '@discord-nestjs/core';
|
||||||
import { SearchHint } from '@jellyfin/sdk/lib/generated-client/models';
|
|
||||||
import { Logger } from '@nestjs/common/services';
|
import { Logger } from '@nestjs/common/services';
|
||||||
import {
|
import {
|
||||||
ComponentType,
|
ComponentType,
|
||||||
EmbedBuilder,
|
EmbedBuilder,
|
||||||
Events,
|
Events,
|
||||||
|
GuildMember,
|
||||||
Interaction,
|
Interaction,
|
||||||
InteractionReplyOptions,
|
InteractionReplyOptions,
|
||||||
} from 'discord.js';
|
} from 'discord.js';
|
||||||
@ -21,11 +21,12 @@ import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.servi
|
|||||||
import { TrackRequestDto } from '../models/track-request.dto';
|
import { TrackRequestDto } from '../models/track-request.dto';
|
||||||
import { DefaultJellyfinColor } from '../types/colors';
|
import { DefaultJellyfinColor } from '../types/colors';
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
import { DiscordMessageService } from '../clients/discord/discord.message.service';
|
||||||
|
|
||||||
|
import { createAudioResource } from '@discordjs/voice';
|
||||||
import { formatDuration, intervalToDuration } from 'date-fns';
|
import { formatDuration, intervalToDuration } from 'date-fns';
|
||||||
import { format } from 'path';
|
import { DiscordVoiceService } from '../clients/discord/discord.voice.service';
|
||||||
|
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service';
|
||||||
import { PlaybackService } from '../playback/playback.service';
|
import { PlaybackService } from '../playback/playback.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
@ -41,7 +42,9 @@ export class SearchItemCommand
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly jellyfinSearchService: JellyfinSearchService,
|
private readonly jellyfinSearchService: JellyfinSearchService,
|
||||||
private readonly discordMessageService: DiscordMessageService,
|
private readonly discordMessageService: DiscordMessageService,
|
||||||
|
private readonly discordVoiceService: DiscordVoiceService,
|
||||||
private readonly playbackService: PlaybackService,
|
private readonly playbackService: PlaybackService,
|
||||||
|
private readonly jellyfinStreamBuilder: JellyfinStreamBuilderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handler(
|
async handler(
|
||||||
@ -149,6 +152,20 @@ export class SearchItemCommand
|
|||||||
durationInMilliseconds: milliseconds,
|
durationInMilliseconds: milliseconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const guildMember = interaction.member as GuildMember;
|
||||||
|
const bitrate = guildMember.voice.channel.bitrate;
|
||||||
|
|
||||||
|
this.discordVoiceService.tryJoinChannelAndEstablishVoiceConnection(
|
||||||
|
guildMember,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.jellyfinStreamBuilder
|
||||||
|
.buildStreamUrl(item.Id, bitrate)
|
||||||
|
.then((stream) => {
|
||||||
|
const resource = createAudioResource(stream);
|
||||||
|
this.discordVoiceService.playResource(resource);
|
||||||
|
});
|
||||||
|
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
embeds: [
|
embeds: [
|
||||||
new EmbedBuilder()
|
new EmbedBuilder()
|
||||||
|
9
src/models/generic-try-handler.ts
Normal file
9
src/models/generic-try-handler.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { InteractionReplyOptions } from 'discord.js';
|
||||||
|
|
||||||
|
export interface GenericTryHandler {
|
||||||
|
success: boolean;
|
||||||
|
reply:
|
||||||
|
| string
|
||||||
|
| InteractionReplyOptions
|
||||||
|
| Promise<string | InteractionReplyOptions>;
|
||||||
|
}
|
@ -3,4 +3,9 @@ export const Constants = {
|
|||||||
Version: '0.0.1',
|
Version: '0.0.1',
|
||||||
ApplicationName: 'Discord Jellyfin Music Bot',
|
ApplicationName: 'Discord Jellyfin Music Bot',
|
||||||
},
|
},
|
||||||
|
Links: {
|
||||||
|
SourceCode: 'https://github.com/manuel-rw/jellyfin-discord-music-bot/',
|
||||||
|
ReportIssue:
|
||||||
|
'https://github.com/manuel-rw/jellyfin-discord-music-bot/issues/new/choose',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user