2022-12-17 01:25:45 +01:00
import { TransformPipe } from '@discord-nestjs/common' ;
import {
Command ,
DiscordTransformedCommand ,
2022-12-17 14:13:03 +01:00
On ,
2022-12-17 01:25:45 +01:00
Payload ,
TransformedCommandExecutionContext ,
UsePipes ,
} from '@discord-nestjs/core' ;
2022-12-17 14:13:03 +01:00
import { Logger } from '@nestjs/common/services' ;
2022-12-17 01:25:45 +01:00
import {
ComponentType ,
2022-12-17 14:13:03 +01:00
Events ,
2022-12-17 16:58:38 +01:00
GuildMember ,
2022-12-17 14:13:03 +01:00
Interaction ,
2022-12-17 01:25:45 +01:00
InteractionReplyOptions ,
} from 'discord.js' ;
import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service' ;
import { TrackRequestDto } from '../models/track-request.dto' ;
2022-12-17 14:13:03 +01:00
import { DiscordMessageService } from '../clients/discord/discord.message.service' ;
2022-12-17 16:58:38 +01:00
import { DiscordVoiceService } from '../clients/discord/discord.voice.service' ;
import { JellyfinStreamBuilderService } from '../clients/jellyfin/jellyfin.stream.builder.service' ;
2022-12-18 16:34:06 +01:00
import {
BaseJellyfinAudioPlayable ,
searchResultAsJellyfinAudio ,
} from '../models/jellyfinAudioItems' ;
2022-12-17 14:13:03 +01:00
import { PlaybackService } from '../playback/playback.service' ;
2022-12-17 01:25:45 +01:00
@Command ( {
2022-12-17 19:52:32 +01:00
name : 'play' ,
2022-12-17 01:25:45 +01:00
description : 'Search for an item on your Jellyfin instance' ,
} )
@UsePipes ( TransformPipe )
2022-12-17 19:52:32 +01:00
export class PlayItemCommand
2022-12-17 01:25:45 +01:00
implements DiscordTransformedCommand < TrackRequestDto >
{
2022-12-17 19:52:32 +01:00
private readonly logger : Logger = new Logger ( PlayItemCommand . name ) ;
2022-12-17 14:13:03 +01:00
constructor (
private readonly jellyfinSearchService : JellyfinSearchService ,
private readonly discordMessageService : DiscordMessageService ,
2022-12-17 16:58:38 +01:00
private readonly discordVoiceService : DiscordVoiceService ,
2022-12-17 14:13:03 +01:00
private readonly playbackService : PlaybackService ,
2022-12-17 16:58:38 +01:00
private readonly jellyfinStreamBuilder : JellyfinStreamBuilderService ,
2022-12-17 14:13:03 +01:00
) { }
2022-12-17 01:25:45 +01:00
async handler (
@Payload ( ) dto : TrackRequestDto ,
executionContext : TransformedCommandExecutionContext < any > ,
) : Promise < InteractionReplyOptions | string > {
const items = await this . jellyfinSearchService . search ( dto . search ) ;
2022-12-18 16:34:06 +01:00
const parsedItems = await Promise . all (
items . map (
async ( item ) = >
await searchResultAsJellyfinAudio (
this . logger ,
this . jellyfinSearchService ,
item ,
) ,
) ,
) ;
2022-12-17 01:25:45 +01:00
2022-12-18 19:21:21 +01:00
if ( parsedItems . length === 0 ) {
2022-12-17 18:31:58 +01:00
return {
embeds : [
this . discordMessageService . buildErrorMessage ( {
title : 'No results for your search query found' ,
description : ` I was not able to find any matches for your query \` \` ${ dto . search } \` \` . Please check that I have access to the desired libraries and that your query is not misspelled ` ,
} ) ,
] ,
} ;
}
2022-12-18 16:34:06 +01:00
const firstItems = parsedItems . slice ( 0 , 10 ) ;
2022-12-17 01:25:45 +01:00
2022-12-18 16:34:06 +01:00
const lines : string [ ] = firstItems . map ( ( item , index ) = > {
let line = ` ${ index + 1 } . ` ;
line += item . prettyPrint ( dto . search ) ;
return line ;
} ) ;
2022-12-17 01:25:45 +01:00
2022-12-17 22:35:11 +01:00
let description =
'I have found **' +
items . length +
'** results for your search ``' +
dto . search +
'``.' ;
if ( items . length > 10 ) {
description +=
'\nSince the results exceed 10 items, I truncated them for better readability.' ;
}
description += '\n\n' + lines . join ( '\n' ) ;
2022-12-17 01:25:45 +01:00
const selectOptions : { label : string ; value : string ; emoji? : string } [ ] =
firstItems . map ( ( item ) = > ( {
2022-12-18 19:39:03 +01:00
label : item.prettyPrint ( dto . search ) . replace ( /\*/g , '' ) ,
2022-12-18 16:34:06 +01:00
value : item.getValueId ( ) ,
emoji : item.getEmoji ( ) ,
2022-12-17 01:25:45 +01:00
} ) ) ;
return {
embeds : [
2022-12-17 19:52:32 +01:00
this . discordMessageService . buildMessage ( {
2022-12-18 19:39:03 +01:00
title : 'Jellyfin Search Results' ,
2022-12-17 20:19:25 +01:00
description : description ,
2022-12-17 19:52:32 +01:00
} ) ,
2022-12-17 01:25:45 +01:00
] ,
components : [
{
type : ComponentType . ActionRow ,
components : [
{
type : ComponentType . StringSelect ,
2022-12-17 14:13:03 +01:00
customId : 'searchItemSelect' ,
2022-12-17 01:25:45 +01:00
options : selectOptions ,
} ,
] ,
} ,
] ,
} ;
}
2022-12-17 14:13:03 +01:00
@On ( Events . InteractionCreate )
async onStringSelect ( interaction : Interaction ) {
if ( ! interaction . isStringSelectMenu ( ) ) return ;
if ( interaction . customId !== 'searchItemSelect' ) {
return ;
}
if ( interaction . values . length !== 1 ) {
this . logger . warn (
` Failed to process interaction select with values [ ${ interaction . values . length } ] ` ,
) ;
return ;
}
2022-12-17 16:58:38 +01:00
const guildMember = interaction . member as GuildMember ;
2022-12-17 22:18:31 +01:00
const tryResult =
this . discordVoiceService . tryJoinChannelAndEstablishVoiceConnection (
guildMember ,
) ;
if ( ! tryResult . success ) {
2022-12-17 22:35:11 +01:00
this . logger . warn (
` Unable to process select result because the member was not in a voice channcel ` ,
) ;
2022-12-17 22:18:31 +01:00
const replyOptions = tryResult . reply as InteractionReplyOptions ;
await interaction . update ( {
embeds : replyOptions.embeds ,
content : undefined ,
components : [ ] ,
} ) ;
return ;
}
const bitrate = guildMember . voice . channel . bitrate ;
2022-12-17 16:58:38 +01:00
2022-12-18 16:34:06 +01:00
const valueParts = interaction . values [ 0 ] . split ( '_' ) ;
const type = valueParts [ 0 ] ;
const id = valueParts [ 1 ] ;
switch ( type ) {
case 'track' :
const item = await this . jellyfinSearchService . getById ( id ) ;
const addedIndex = this . enqueueSingleTrack (
item as BaseJellyfinAudioPlayable ,
bitrate ,
) ;
interaction . update ( {
embeds : [
this . discordMessageService . buildMessage ( {
title : item.Name ,
description : ` Your track was added to the position ${ addedIndex } in the playlist ` ,
} ) ,
] ,
components : [ ] ,
} ) ;
break ;
2022-12-18 17:46:31 +01:00
case 'album' :
const album = await this . jellyfinSearchService . getItemsByAlbum ( id ) ;
album . SearchHints . forEach ( ( item ) = > {
this . enqueueSingleTrack ( item as BaseJellyfinAudioPlayable , bitrate ) ;
} ) ;
interaction . update ( {
embeds : [
this . discordMessageService . buildMessage ( {
title : ` Added ${ album . TotalRecordCount } items from your album ` ,
} ) ,
] ,
components : [ ] ,
} ) ;
break ;
2022-12-18 16:34:06 +01:00
case 'playlist' :
const playlist = await this . jellyfinSearchService . getPlaylistById ( id ) ;
playlist . Items . forEach ( ( item ) = > {
this . enqueueSingleTrack ( item as BaseJellyfinAudioPlayable , bitrate ) ;
} ) ;
interaction . update ( {
embeds : [
this . discordMessageService . buildMessage ( {
title : ` Added ${ playlist . TotalRecordCount } items from your playlist ` ,
} ) ,
] ,
components : [ ] ,
} ) ;
break ;
default :
interaction . update ( {
embeds : [
this . discordMessageService . buildErrorMessage ( {
title : 'Unable to process your selection' ,
2022-12-18 17:46:31 +01:00
description : ` Sorry. I don't know the type you selected: \` \` ${ type } \` \` . Please report this bug to the developers. \ n \ nDebug Information: \` \` ${ interaction . values . join (
2022-12-18 16:34:06 +01:00
', ' ,
) } \ ` \` ` ,
} ) ,
] ,
components : [ ] ,
} ) ;
break ;
}
}
private enqueueSingleTrack (
jellyfinPlayable : BaseJellyfinAudioPlayable ,
bitrate : number ,
) {
const stream = this . jellyfinStreamBuilder . buildStreamUrl (
jellyfinPlayable . Id ,
2022-12-17 20:19:25 +01:00
bitrate ,
) ;
2022-12-18 16:34:06 +01:00
const milliseconds = jellyfinPlayable . RunTimeTicks / 10000 ;
2022-12-17 22:35:11 +01:00
2022-12-18 19:21:33 +01:00
return this . playbackService . enqueueTrack ( {
2022-12-18 16:34:06 +01:00
jellyfinId : jellyfinPlayable.Id ,
name : jellyfinPlayable.Name ,
2022-12-17 20:19:25 +01:00
durationInMilliseconds : milliseconds ,
streamUrl : stream ,
} ) ;
2022-12-17 01:25:45 +01:00
}
}