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' ;
import { formatDuration , intervalToDuration } from 'date-fns' ;
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-17 14:13:03 +01:00
import { PlaybackService } from '../playback/playback.service' ;
2022-12-17 19:52:32 +01:00
import { Constants } from '../utils/constants' ;
2022-12-17 20:19:25 +01:00
import { trimStringToFixedLength } from '../utils/stringUtils' ;
2022-12-17 14:13:03 +01:00
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-17 18:31:58 +01:00
if ( items . length < 1 ) {
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-17 01:25:45 +01:00
const firstItems = items . slice ( 0 , 10 ) ;
const lines : string [ ] = firstItems . map (
( item ) = >
2022-12-17 20:19:25 +01:00
` :white_small_square: ${ trimStringToFixedLength (
this . markSearchTermOverlap ( item . Name , dto . search ) ,
30 ,
2022-12-18 12:37:33 +01:00
) } [ $ { item . Artists . join ( ', ' ) } ] * ( $ { item . Type } ) * ` ,
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 emojiForType = ( type : string ) = > {
switch ( type ) {
case 'Audio' :
return '🎵' ;
case 'Playlist' :
return '📚' ;
default :
return undefined ;
}
} ;
const selectOptions : { label : string ; value : string ; emoji? : string } [ ] =
firstItems . map ( ( item ) = > ( {
2022-12-18 12:37:33 +01:00
label : ` ${ item . Name } [ ${ item . Artists . join ( ', ' ) } ] ` ,
2022-12-17 01:25:45 +01:00
value : item.Id ,
emoji : emojiForType ( item . Type ) ,
} ) ) ;
return {
embeds : [
2022-12-17 19:52:32 +01:00
this . discordMessageService . buildMessage ( {
2022-12-17 20:19:25 +01:00
title : 'a' ,
description : description ,
2022-12-17 19:52:32 +01:00
mixin ( embedBuilder ) {
return embedBuilder . setAuthor ( {
name : 'Jellyfin Search Results' ,
iconURL : Constants.Design.Icons.JellyfinLogo ,
} ) ;
} ,
} ) ,
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 ;
}
const item = await this . jellyfinSearchService . getById (
interaction . values [ 0 ] ,
) ;
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-17 20:19:25 +01:00
const stream = await this . jellyfinStreamBuilder . buildStreamUrl (
item . Id ,
bitrate ,
) ;
2022-12-17 22:35:11 +01:00
const milliseconds = item . RunTimeTicks / 10000 ;
const duration = formatDuration (
intervalToDuration ( {
start : milliseconds ,
end : 0 ,
} ) ,
) ;
2022-12-17 20:19:25 +01:00
const addedIndex = this . playbackService . eneuqueTrack ( {
jellyfinId : item.Id ,
name : item.Name ,
durationInMilliseconds : milliseconds ,
streamUrl : stream ,
} ) ;
2022-12-17 16:58:38 +01:00
2022-12-17 22:35:11 +01:00
const artists = item . Artists . join ( ', ' ) ;
2022-12-17 14:13:03 +01:00
await interaction . update ( {
embeds : [
2022-12-17 22:22:44 +01:00
this . discordMessageService . buildMessage ( {
title : 'Jellyfin Search' ,
description : ` **Duration**: ${ duration } \ n**Artists**: ${ artists } \ n \ nTrack was added to the queue at position ${ addedIndex } ` ,
} ) ,
2022-12-17 14:13:03 +01:00
] ,
components : [ ] ,
} ) ;
}
2022-12-17 01:25:45 +01:00
private markSearchTermOverlap ( value : string , searchTerm : string ) {
const startIndex = value . indexOf ( searchTerm ) ;
const actualValue = value . substring (
startIndex ,
startIndex + 1 + searchTerm . length ,
) ;
return ` ${ value . substring (
0 ,
startIndex ,
) } * * $ { actualValue } * * $ { value . substring (
startIndex + 1 + actualValue . length ,
) } ` ;
}
}