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 ,
EmbedBuilder ,
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' ;
import { DefaultJellyfinColor } from '../types/colors' ;
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 { createAudioResource } from '@discordjs/voice' ;
2022-12-17 14:13:03 +01:00
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 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 ) = >
` :white_small_square: ${ this . markSearchTermOverlap (
item . Name ,
dto . search ,
) } * ( $ { item . Type } ) * ` ,
) ;
const description = ` I have found ** ${
items . length
} * * results for your search \ ` \` ${
dto . search
} \ ` \` . \ nFor better readability, I have limited the search results to 10 \ n \ n ${ lines . join (
'\n' ,
) } ` ;
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 ) = > ( {
label : item.Name ,
value : item.Id ,
emoji : emojiForType ( item . Type ) ,
} ) ) ;
return {
embeds : [
2022-12-17 19:52:32 +01:00
this . discordMessageService . buildMessage ( {
title : '' ,
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 ] ,
) ;
const milliseconds = item . RunTimeTicks / 10000 ;
const duration = formatDuration (
intervalToDuration ( {
start : milliseconds ,
end : 0 ,
} ) ,
) ;
const artists = item . Artists . join ( ', ' ) ;
const addedIndex = this . playbackService . eneuqueTrack ( {
jellyfinId : item.Id ,
name : item.Name ,
durationInMilliseconds : milliseconds ,
} ) ;
2022-12-17 16:58:38 +01:00
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 ) ;
} ) ;
2022-12-17 14:13:03 +01:00
await interaction . update ( {
embeds : [
new EmbedBuilder ( )
. setAuthor ( {
name : 'Jellyfin Search' ,
iconURL :
'https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true' ,
} )
. setTitle ( item . Name )
. setDescription (
` **Duration**: ${ duration } \ n**Artists**: ${ artists } \ n \ nTrack was added to the queue at position ${ addedIndex } ` ,
)
. setColor ( DefaultJellyfinColor )
. toJSON ( ) ,
] ,
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 ,
) } ` ;
}
}