Add search command and service

This commit is contained in:
Manuel Ruwe 2022-12-17 01:25:45 +01:00
parent 17eee92404
commit 4693b2f75f
6 changed files with 207 additions and 14 deletions

View File

@ -1,15 +1,19 @@
import { Module, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { JellyfinService } from "./jellyfin.service";
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { JellyfinSearchService } from './jellyfin.search.service';
import { JellyfinService } from './jellyfin.service';
import { JellyinWebsocketService } from './jellyfin.websocket.service';
@Module({
imports: [],
controllers: [],
providers: [JellyfinService],
exports: [],
providers: [JellyfinService, JellyinWebsocketService, JellyfinSearchService],
exports: [JellyfinService, JellyfinSearchService],
})
export class JellyfinClientModule implements OnModuleInit, OnModuleDestroy {
constructor(private jellyfinService: JellyfinService) {}
constructor(
private jellyfinService: JellyfinService,
private readonly jellyfinWebsocketService: JellyinWebsocketService,
) {}
onModuleDestroy() {
this.jellyfinService.destroy();
@ -18,5 +22,9 @@ export class JellyfinClientModule implements OnModuleInit, OnModuleDestroy {
onModuleInit() {
this.jellyfinService.init();
this.jellyfinService.authenticate();
setTimeout(() => {
this.jellyfinWebsocketService.openSocket();
}, 5000);
}
}

View File

@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { JellyfinService } from './jellyfin.service';
import { SearchHint } from '@jellyfin/sdk/lib/generated-client/models';
import { getSearchApi } from '@jellyfin/sdk/lib/utils/api/search-api';
import { Logger } from '@nestjs/common/services';
@Injectable()
export class JellyfinSearchService {
private readonly logger = new Logger(JellyfinSearchService.name);
constructor(private readonly jellyfinService: JellyfinService) {}
async search(searchTerm: string): Promise<SearchHint[]> {
const api = this.jellyfinService.getApi();
this.logger.debug(`Searching for '${searchTerm}'`);
const searchApi = getSearchApi(api);
const {
data: { SearchHints, TotalRecordCount },
} = await searchApi.get({
searchTerm: searchTerm,
mediaTypes: ['Audio', 'Album'],
});
this.logger.debug(`Found ${TotalRecordCount} results for '${searchTerm}'`);
return SearchHints;
}
}

View File

@ -2,12 +2,16 @@ import { Injectable, Logger } from '@nestjs/common';
import { Api, Jellyfin } from '@jellyfin/sdk';
import { Constants } from '../../utils/constants';
import { SystemApi } from '@jellyfin/sdk/lib/generated-client/api/system-api';
import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api';
@Injectable()
export class JellyfinService {
private readonly logger = new Logger(JellyfinService.name);
private jellyfin: Jellyfin;
private api: Api;
private systemApi: SystemApi;
private userId: string;
init() {
this.jellyfin = new Jellyfin({
@ -42,6 +46,9 @@ export class JellyfinService {
this.logger.debug(
`Connected using user '${response.data.SessionInfo.UserId}'`,
);
this.userId = response.data.SessionInfo.UserId;
this.systemApi = getSystemApi(this.api);
})
.catch((test) => {
this.logger.error(test);
@ -57,4 +64,20 @@ export class JellyfinService {
}
this.api.logout();
}
getApi() {
return this.api;
}
getJellyfin() {
return this.jellyfin;
}
getSystemApi() {
return this.systemApi;
}
getUserId() {
return this.userId;
}
}

View File

@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import { JellyfinService } from './jellyfin.service';
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
@Injectable()
export class JellyinWebsocketService {
constructor(private readonly jellyfinClientManager: JellyfinService) {}
async openSocket() {
const systemApi = getPlaystateApi(this.jellyfinClientManager.getApi());
// TODO: Write socket playstate api to report playback progress
}
}

View File

@ -1,21 +1,23 @@
import { Module } from '@nestjs/common';
import { DiscordModule } from '@discord-nestjs/core';
import { Module } from '@nestjs/common';
import { HelpCommand } from './help.command';
import { StatusCommand } from './status.command';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { JellyfinClientModule } from '../clients/jellyfin/jellyfin.module';
import { PlaybackService } from '../playback/playback.service';
import { CurrentTrackCommand } from './current.command';
import { DisconnectCommand } from './disconnect.command';
import { EnqueueCommand } from './enqueue.command';
import { HelpCommand } from './help.command';
import { PausePlaybackCommand } from './pause.command';
import { PlayCommand } from './play.command';
import { SearchItemCommand } from './search.comands';
import { SkipTrackCommand } from './skip.command';
import { StatusCommand } from './status.command';
import { StopPlaybackCommand } from './stop.command';
import { SummonCommand } from './summon.command';
import { DiscordMessageService } from '../clients/discord/discord.message.service';
import { PlaybackService } from '../playback/playback.service';
@Module({
imports: [DiscordModule.forFeature()],
imports: [DiscordModule.forFeature(), JellyfinClientModule],
controllers: [],
providers: [
HelpCommand,
@ -28,6 +30,7 @@ import { PlaybackService } from '../playback/playback.service';
SkipTrackCommand,
StopPlaybackCommand,
SummonCommand,
SearchItemCommand,
DiscordMessageService,
PlaybackService,
],

View File

@ -0,0 +1,113 @@
import { TransformPipe } from '@discord-nestjs/common';
import {
Command,
DiscordTransformedCommand,
Param,
Payload,
TransformedCommandExecutionContext,
UsePipes,
} from '@discord-nestjs/core';
import {
APIEmbedField,
ComponentType,
EmbedBuilder,
InteractionReplyOptions,
} from 'discord.js';
import { JellyfinSearchService } from '../clients/jellyfin/jellyfin.search.service';
import { TrackRequestDto } from '../models/track-request.dto';
import { DefaultJellyfinColor } from '../types/colors';
@Command({
name: 'search',
description: 'Search for an item on your Jellyfin instance',
})
@UsePipes(TransformPipe)
export class SearchItemCommand
implements DiscordTransformedCommand<TrackRequestDto>
{
constructor(private readonly jellyfinSearchService: JellyfinSearchService) {}
async handler(
@Payload() dto: TrackRequestDto,
executionContext: TransformedCommandExecutionContext<any>,
): Promise<InteractionReplyOptions | string> {
const items = await this.jellyfinSearchService.search(dto.search);
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: [
new EmbedBuilder()
.setAuthor({
name: 'Jellyfin Search Results',
iconURL:
'https://github.com/walkxcode/dashboard-icons/blob/main/png/jellyfin.png?raw=true',
})
.setColor(DefaultJellyfinColor)
.setDescription(description)
.toJSON(),
],
components: [
{
type: ComponentType.ActionRow,
components: [
{
type: ComponentType.StringSelect,
customId: 'cool',
options: selectOptions,
},
],
},
],
};
}
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,
)}`;
}
}