mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-23 18:21:55 +01:00
✨ Add search command and service
This commit is contained in:
parent
17eee92404
commit
4693b2f75f
@ -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);
|
||||
}
|
||||
}
|
31
src/clients/jellyfin/jellyfin.search.service.ts
Normal file
31
src/clients/jellyfin/jellyfin.search.service.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
15
src/clients/jellyfin/jellyfin.websocket.service.ts
Normal file
15
src/clients/jellyfin/jellyfin.websocket.service.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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,
|
||||
],
|
||||
|
113
src/commands/search.comands.ts
Normal file
113
src/commands/search.comands.ts
Normal 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,
|
||||
)}`;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user