mirror of
https://github.com/informaticker/discord-jellyfin-bot.git
synced 2024-11-25 02:51:57 +01:00
Merge pull request #149 from manuel-rw/refactor/strict-typescript
♻️ Strict Typescript
This commit is contained in:
commit
65d401fdbb
@ -9,7 +9,7 @@ import { GatewayIntentBits } from 'discord.js';
|
|||||||
export class DiscordConfigService implements DiscordOptionsFactory {
|
export class DiscordConfigService implements DiscordOptionsFactory {
|
||||||
createDiscordOptions(): DiscordModuleOption {
|
createDiscordOptions(): DiscordModuleOption {
|
||||||
return {
|
return {
|
||||||
token: process.env.DISCORD_CLIENT_TOKEN,
|
token: process.env.DISCORD_CLIENT_TOKEN ?? '',
|
||||||
discordClientOptions: {
|
discordClientOptions: {
|
||||||
intents: [
|
intents: [
|
||||||
GatewayIntentBits.Guilds,
|
GatewayIntentBits.Guilds,
|
||||||
|
@ -103,6 +103,12 @@ export class DiscordVoiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeVolume(volume: number) {
|
changeVolume(volume: number) {
|
||||||
|
if (!this.audioResource || !this.audioResource.volume) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to change audio volume, AudioResource or volume was undefined`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.audioResource.volume.setVolume(volume);
|
this.audioResource.volume.setVolume(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +241,20 @@ export class DiscordVoiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private attachEventListenersToAudioPlayer() {
|
private attachEventListenersToAudioPlayer() {
|
||||||
|
if (!this.voiceConnection) {
|
||||||
|
this.logger.error(
|
||||||
|
`Unable to attach listener events, because the VoiceConnection was undefined`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.audioPlayer) {
|
||||||
|
this.logger.error(
|
||||||
|
`Unable to attach listener events, because the AudioPlayer was undefined`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.voiceConnection.on('debug', (message) => {
|
this.voiceConnection.on('debug', (message) => {
|
||||||
if (process.env.DEBUG?.toLowerCase() !== 'true') {
|
if (process.env.DEBUG?.toLowerCase() !== 'true') {
|
||||||
return;
|
return;
|
||||||
@ -252,6 +272,13 @@ export class DiscordVoiceService {
|
|||||||
this.logger.error(message);
|
this.logger.error(message);
|
||||||
});
|
});
|
||||||
this.audioPlayer.on('stateChange', (previousState) => {
|
this.audioPlayer.on('stateChange', (previousState) => {
|
||||||
|
if (!this.audioPlayer) {
|
||||||
|
this.logger.error(
|
||||||
|
`Unable to process state change from audio player, because the current audio player in the callback was undefined`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Audio player changed state from ${previousState.status} to ${this.audioPlayer.state.status}`,
|
`Audio player changed state from ${previousState.status} to ${this.audioPlayer.state.status}`,
|
||||||
);
|
);
|
||||||
@ -268,9 +295,11 @@ export class DiscordVoiceService {
|
|||||||
|
|
||||||
const playlist = this.playbackService.getPlaylistOrDefault();
|
const playlist = this.playbackService.getPlaylistOrDefault();
|
||||||
const finishedTrack = playlist.getActiveTrack();
|
const finishedTrack = playlist.getActiveTrack();
|
||||||
finishedTrack.playing = false;
|
|
||||||
|
|
||||||
|
if (finishedTrack) {
|
||||||
|
finishedTrack.playing = false;
|
||||||
this.eventEmitter.emit('internal.audio.track.finish', finishedTrack);
|
this.eventEmitter.emit('internal.audio.track.finish', finishedTrack);
|
||||||
|
}
|
||||||
|
|
||||||
const hasNextTrack = playlist.hasNextTrackInPlaylist();
|
const hasNextTrack = playlist.hasNextTrackInPlaylist();
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
BaseItemDto,
|
||||||
BaseItemKind,
|
BaseItemKind,
|
||||||
RemoteImageResult,
|
RemoteImageResult,
|
||||||
SearchHint as JellyfinSearchHint,
|
SearchHint as JellyfinSearchHint,
|
||||||
@ -57,9 +58,13 @@ export class JellyfinSearchService {
|
|||||||
|
|
||||||
const { SearchHints } = data;
|
const { SearchHints } = data;
|
||||||
|
|
||||||
return SearchHints.map((hint) => this.transformToSearchHint(hint)).filter(
|
if (!SearchHints) {
|
||||||
(x) => x !== null,
|
throw new Error('SearchHints were undefined');
|
||||||
);
|
}
|
||||||
|
|
||||||
|
return SearchHints.map((hint) =>
|
||||||
|
this.transformToSearchHintFromHint(hint),
|
||||||
|
).filter((x) => x !== null) as SearchHint[];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(`Failed to search on Jellyfin: ${err}`);
|
this.logger.error(`Failed to search on Jellyfin: ${err}`);
|
||||||
return [];
|
return [];
|
||||||
@ -82,8 +87,15 @@ export class JellyfinSearchService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!axiosResponse.data.Items) {
|
||||||
|
this.logger.error(
|
||||||
|
`Jellyfin search returned no items: ${axiosResponse.data}`,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return axiosResponse.data.Items.map((hint) =>
|
return axiosResponse.data.Items.map((hint) =>
|
||||||
SearchHint.constructFromHint(hint),
|
SearchHint.constructFromBaseItem(hint),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +116,13 @@ export class JellyfinSearchService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!axiosResponse.data.SearchHints) {
|
||||||
|
this.logger.error(
|
||||||
|
`Received an unexpected empty list but expected a list of tracks of the album`,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return [...axiosResponse.data.SearchHints]
|
return [...axiosResponse.data.SearchHints]
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((hint) => SearchHint.constructFromHint(hint));
|
.map((hint) => SearchHint.constructFromHint(hint));
|
||||||
@ -112,7 +131,7 @@ export class JellyfinSearchService {
|
|||||||
async getById(
|
async getById(
|
||||||
id: string,
|
id: string,
|
||||||
includeItemTypes: BaseItemKind[],
|
includeItemTypes: BaseItemKind[],
|
||||||
): Promise<SearchHint> | undefined {
|
): Promise<SearchHint | undefined> {
|
||||||
const api = this.jellyfinService.getApi();
|
const api = this.jellyfinService.getApi();
|
||||||
|
|
||||||
const searchApi = getItemsApi(api);
|
const searchApi = getItemsApi(api);
|
||||||
@ -122,18 +141,18 @@ export class JellyfinSearchService {
|
|||||||
includeItemTypes: includeItemTypes,
|
includeItemTypes: includeItemTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.Items.length !== 1) {
|
if (!data.Items || data.Items.length !== 1) {
|
||||||
this.logger.warn(`Failed to retrieve item via id '${id}'`);
|
this.logger.warn(`Failed to retrieve item via id '${id}'`);
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.transformToSearchHint(data.Items[0]);
|
return this.transformToSearchHintFromBaseItemDto(data.Items[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllById(
|
async getAllById(
|
||||||
ids: string[],
|
ids: string[],
|
||||||
includeItemTypes: BaseItemKind[] = [BaseItemKind.Audio],
|
includeItemTypes: BaseItemKind[] = [BaseItemKind.Audio],
|
||||||
): Promise<SearchHint[]> | undefined {
|
): Promise<SearchHint[]> {
|
||||||
const api = this.jellyfinService.getApi();
|
const api = this.jellyfinService.getApi();
|
||||||
|
|
||||||
const searchApi = getItemsApi(api);
|
const searchApi = getItemsApi(api);
|
||||||
@ -143,12 +162,14 @@ export class JellyfinSearchService {
|
|||||||
includeItemTypes: includeItemTypes,
|
includeItemTypes: includeItemTypes,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.Items.length !== 1) {
|
if (!data.Items || data.Items.length !== 1) {
|
||||||
this.logger.warn(`Failed to retrieve item via id '${ids}'`);
|
this.logger.warn(`Failed to retrieve item via id '${ids}'`);
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.Items.map((item) => this.transformToSearchHint(item));
|
return data.Items.map((item) =>
|
||||||
|
this.transformToSearchHintFromBaseItemDto(item),
|
||||||
|
).filter((searchHint) => searchHint !== undefined) as SearchHint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRemoteImageById(id: string, limit = 20): Promise<RemoteImageResult> {
|
async getRemoteImageById(id: string, limit = 20): Promise<RemoteImageResult> {
|
||||||
@ -204,18 +225,25 @@ export class JellyfinSearchService {
|
|||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!response.data.Items) {
|
||||||
|
this.logger.error(
|
||||||
|
`Received empty list of items but expected a random list of tracks`,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.Items.map((item) => {
|
return response.data.Items.map((item) => {
|
||||||
return SearchHint.constructFromBaseItem(item);
|
return SearchHint.constructFromBaseItem(item);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Unabele to retrieve random items from Jellyfin: ${err}`,
|
`Unable to retrieve random items from Jellyfin: ${err}`,
|
||||||
);
|
);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private transformToSearchHint(jellyifnHint: JellyfinSearchHint) {
|
private transformToSearchHintFromHint(jellyifnHint: JellyfinSearchHint) {
|
||||||
switch (jellyifnHint.Type) {
|
switch (jellyifnHint.Type) {
|
||||||
case BaseItemKind[BaseItemKind.Audio]:
|
case BaseItemKind[BaseItemKind.Audio]:
|
||||||
return SearchHint.constructFromHint(jellyifnHint);
|
return SearchHint.constructFromHint(jellyifnHint);
|
||||||
@ -227,7 +255,23 @@ export class JellyfinSearchService {
|
|||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Received unexpected item type from Jellyfin search: ${jellyifnHint.Type}`,
|
`Received unexpected item type from Jellyfin search: ${jellyifnHint.Type}`,
|
||||||
);
|
);
|
||||||
return null;
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private transformToSearchHintFromBaseItemDto(baseItemDto: BaseItemDto) {
|
||||||
|
switch (baseItemDto.Type) {
|
||||||
|
case BaseItemKind[BaseItemKind.Audio]:
|
||||||
|
return SearchHint.constructFromBaseItem(baseItemDto);
|
||||||
|
case BaseItemKind[BaseItemKind.MusicAlbum]:
|
||||||
|
return AlbumSearchHint.constructFromBaseItem(baseItemDto);
|
||||||
|
case BaseItemKind[BaseItemKind.Playlist]:
|
||||||
|
return PlaylistSearchHint.constructFromBaseItem(baseItemDto);
|
||||||
|
default:
|
||||||
|
this.logger.warn(
|
||||||
|
`Received unexpected item type from Jellyfin search: ${baseItemDto.Type}`,
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,18 +33,20 @@ export class JellyfinService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.api = this.jellyfin.createApi(process.env.JELLYFIN_SERVER_ADDRESS);
|
this.api = this.jellyfin.createApi(
|
||||||
|
process.env.JELLYFIN_SERVER_ADDRESS ?? '',
|
||||||
|
);
|
||||||
this.logger.debug('Created Jellyfin Client and Api');
|
this.logger.debug('Created Jellyfin Client and Api');
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticate() {
|
authenticate() {
|
||||||
this.api
|
this.api
|
||||||
.authenticateUserByName(
|
.authenticateUserByName(
|
||||||
process.env.JELLYFIN_AUTHENTICATION_USERNAME,
|
process.env.JELLYFIN_AUTHENTICATION_USERNAME ?? '',
|
||||||
process.env.JELLYFIN_AUTHENTICATION_PASSWORD,
|
process.env.JELLYFIN_AUTHENTICATION_PASSWORD,
|
||||||
)
|
)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (response.data.SessionInfo === undefined) {
|
if (response.data.SessionInfo?.UserId === undefined) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to authenticate with response code ${response.status}: '${response.data}'`,
|
`Failed to authenticate with response code ${response.status}: '${response.data}'`,
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { Cron } from '@nestjs/schedule';
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
import { convertToTracks } from 'src/utils/trackConverter';
|
||||||
|
|
||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
|
|
||||||
@ -104,15 +105,7 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
|
|||||||
`Processing ${ids.length} ids received via websocket and adding them to the queue`,
|
`Processing ${ids.length} ids received via websocket and adding them to the queue`,
|
||||||
);
|
);
|
||||||
const searchHints = await this.jellyfinSearchService.getAllById(ids);
|
const searchHints = await this.jellyfinSearchService.getAllById(ids);
|
||||||
|
const tracks = convertToTracks(searchHints, this.jellyfinSearchService);
|
||||||
const tracks = await Promise.all(
|
|
||||||
searchHints.map(async (x) =>
|
|
||||||
(
|
|
||||||
await x.toTracks(this.jellyfinSearchService)
|
|
||||||
).find((x) => x !== null),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks);
|
this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks);
|
||||||
break;
|
break;
|
||||||
case SessionMessageType[SessionMessageType.Playstate]:
|
case SessionMessageType[SessionMessageType.Playstate]:
|
||||||
|
@ -48,12 +48,12 @@ export class PlayItemCommand {
|
|||||||
async handler(
|
async handler(
|
||||||
@InteractionEvent(SlashCommandPipe) dto: PlayCommandParams,
|
@InteractionEvent(SlashCommandPipe) dto: PlayCommandParams,
|
||||||
@IA() interaction: CommandInteraction,
|
@IA() interaction: CommandInteraction,
|
||||||
): Promise<InteractionReplyOptions | string> {
|
) {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
const baseItems = PlayCommandParams.getBaseItemKinds(dto.type);
|
const baseItems = PlayCommandParams.getBaseItemKinds(dto.type);
|
||||||
|
|
||||||
let item: SearchHint;
|
let item: SearchHint | undefined;
|
||||||
if (dto.name.startsWith('native-')) {
|
if (dto.name.startsWith('native-')) {
|
||||||
item = await this.jellyfinSearchService.getById(
|
item = await this.jellyfinSearchService.getById(
|
||||||
dto.name.replace('native-', ''),
|
dto.name.replace('native-', ''),
|
||||||
@ -104,9 +104,9 @@ export class PlayItemCommand {
|
|||||||
);
|
);
|
||||||
this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks);
|
this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks);
|
||||||
|
|
||||||
const remoteImage: RemoteImageInfo | undefined = tracks
|
const remoteImages = tracks.flatMap((track) => track.getRemoteImages());
|
||||||
.flatMap((track) => track.getRemoteImages())
|
const remoteImage: RemoteImageInfo | undefined =
|
||||||
.find(() => true);
|
remoteImages.length > 0 ? remoteImages[0] : undefined;
|
||||||
|
|
||||||
await interaction.followUp({
|
await interaction.followUp({
|
||||||
embeds: [
|
embeds: [
|
||||||
@ -117,7 +117,7 @@ export class PlayItemCommand {
|
|||||||
reducedDuration,
|
reducedDuration,
|
||||||
)})`,
|
)})`,
|
||||||
mixin(embedBuilder) {
|
mixin(embedBuilder) {
|
||||||
if (!remoteImage) {
|
if (!remoteImage?.Url) {
|
||||||
return embedBuilder;
|
return embedBuilder;
|
||||||
}
|
}
|
||||||
return embedBuilder.setThumbnail(remoteImage.Url);
|
return embedBuilder.setThumbnail(remoteImage.Url);
|
||||||
@ -135,7 +135,15 @@ export class PlayItemCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const focusedAutoCompleteAction = interaction.options.getFocused(true);
|
const focusedAutoCompleteAction = interaction.options.getFocused(true);
|
||||||
const typeIndex: number | null = interaction.options.getInteger('type');
|
const typeIndex = interaction.options.getInteger('type');
|
||||||
|
|
||||||
|
if (typeIndex === null) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to get type integer from play command interaction autocomplete`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const type = Object.values(SearchType)[typeIndex] as SearchType;
|
const type = Object.values(SearchType)[typeIndex] as SearchType;
|
||||||
const searchQuery = focusedAutoCompleteAction.value;
|
const searchQuery = focusedAutoCompleteAction.value;
|
||||||
|
|
||||||
|
@ -30,7 +30,10 @@ export class PlaylistInteractionCollector {
|
|||||||
|
|
||||||
@Filter()
|
@Filter()
|
||||||
filter(interaction: ButtonInteraction): boolean {
|
filter(interaction: ButtonInteraction): boolean {
|
||||||
return this.causeInteraction.id === interaction.message.interaction.id;
|
return (
|
||||||
|
interaction.message.interaction !== null &&
|
||||||
|
this.causeInteraction.id === interaction.message.interaction.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@On('collect')
|
@On('collect')
|
||||||
@ -55,7 +58,7 @@ export class PlaylistInteractionCollector {
|
|||||||
await interaction.update(reply as InteractionUpdateOptions);
|
await interaction.update(reply as InteractionUpdateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInteraction(interaction: ButtonInteraction): number | null {
|
private getInteraction(interaction: ButtonInteraction): number | undefined {
|
||||||
const current = this.playlistCommand.pageData.get(this.causeInteraction.id);
|
const current = this.playlistCommand.pageData.get(this.causeInteraction.id);
|
||||||
|
|
||||||
if (current === undefined) {
|
if (current === undefined) {
|
||||||
|
@ -39,7 +39,7 @@ export class StatusCommand {
|
|||||||
const status = Status[this.client.ws.status];
|
const status = Status[this.client.ws.status];
|
||||||
|
|
||||||
const interval = intervalToDuration({
|
const interval = intervalToDuration({
|
||||||
start: this.client.uptime,
|
start: this.client.uptime ?? 0,
|
||||||
end: 0,
|
end: 0,
|
||||||
});
|
});
|
||||||
const formattedDuration = formatDuration(interval);
|
const formattedDuration = formatDuration(interval);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { InjectionToken } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
HealthCheckResult,
|
HealthCheckResult,
|
||||||
HealthCheckService,
|
HealthCheckService,
|
||||||
@ -42,10 +43,14 @@ describe('HealthController', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (token === HealthCheckService) {
|
if (token === HealthCheckService) {
|
||||||
return new HealthCheckService(new HealthCheckExecutor(), null, null);
|
return new HealthCheckService(
|
||||||
|
new HealthCheckExecutor(),
|
||||||
|
{ getErrorMessage: jest.fn() },
|
||||||
|
{ log: jest.fn(), error: jest.fn(), warn: jest.fn() },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return useDefaultMockerToken(token);
|
return useDefaultMockerToken(token as InjectionToken);
|
||||||
})
|
})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { InjectionToken } from '@nestjs/common';
|
||||||
import { HealthIndicatorResult } from '@nestjs/terminus';
|
import { HealthIndicatorResult } from '@nestjs/terminus';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { JellyfinService } from '../../clients/jellyfin/jellyfin.service';
|
import { JellyfinService } from '../../clients/jellyfin/jellyfin.service';
|
||||||
@ -16,7 +17,7 @@ describe('JellyfinHealthIndicator', () => {
|
|||||||
if (token === JellyfinService) {
|
if (token === JellyfinService) {
|
||||||
return { isConnected: jest.fn() };
|
return { isConnected: jest.fn() };
|
||||||
}
|
}
|
||||||
return useDefaultMockerToken(token);
|
return useDefaultMockerToken(token as InjectionToken);
|
||||||
})
|
})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
@ -4,6 +4,10 @@ import { NestFactory } from '@nestjs/core';
|
|||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
function getLoggingLevels(): LogLevel[] {
|
function getLoggingLevels(): LogLevel[] {
|
||||||
|
if (!process.env.LOG_LEVEL) {
|
||||||
|
return ['error', 'warn', 'log'];
|
||||||
|
}
|
||||||
|
|
||||||
switch (process.env.LOG_LEVEL.toLowerCase()) {
|
switch (process.env.LOG_LEVEL.toLowerCase()) {
|
||||||
case 'error':
|
case 'error':
|
||||||
return ['error'];
|
return ['error'];
|
||||||
|
@ -11,6 +11,12 @@ export class AlbumSearchHint extends SearchHint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static constructFromHint(hint: JellyfinSearchHint) {
|
static constructFromHint(hint: JellyfinSearchHint) {
|
||||||
|
if (hint.Id === undefined || !hint.Name || !hint.RunTimeTicks) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to construct playlist search hint, required properties were undefined',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new AlbumSearchHint(hint.Id, hint.Name, hint.RunTimeTicks / 10000);
|
return new AlbumSearchHint(hint.Id, hint.Name, hint.RunTimeTicks / 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +30,7 @@ export class AlbumSearchHint extends SearchHint {
|
|||||||
(await x.toTracks(searchService)).find((x) => x !== null),
|
(await x.toTracks(searchService)).find((x) => x !== null),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return tracks.map((track): Track => {
|
return tracks.map((track: Track): Track => {
|
||||||
track.remoteImages = remoteImages;
|
track.remoteImages = remoteImages;
|
||||||
return track;
|
return track;
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ import { Track } from '../shared/Track';
|
|||||||
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service';
|
||||||
|
|
||||||
import { SearchHint } from './SearchHint';
|
import { SearchHint } from './SearchHint';
|
||||||
|
import { convertToTracks } from 'src/utils/trackConverter';
|
||||||
|
|
||||||
export class PlaylistSearchHint extends SearchHint {
|
export class PlaylistSearchHint extends SearchHint {
|
||||||
override toString(): string {
|
override toString(): string {
|
||||||
@ -11,6 +12,12 @@ export class PlaylistSearchHint extends SearchHint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static constructFromHint(hint: JellyfinSearchHint) {
|
static constructFromHint(hint: JellyfinSearchHint) {
|
||||||
|
if (hint.Id === undefined || !hint.Name || !hint.RunTimeTicks) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to construct playlist search hint, required properties were undefined',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new PlaylistSearchHint(
|
return new PlaylistSearchHint(
|
||||||
hint.Id,
|
hint.Id,
|
||||||
hint.Name,
|
hint.Name,
|
||||||
@ -22,9 +29,6 @@ export class PlaylistSearchHint extends SearchHint {
|
|||||||
searchService: JellyfinSearchService,
|
searchService: JellyfinSearchService,
|
||||||
): Promise<Track[]> {
|
): Promise<Track[]> {
|
||||||
const playlistItems = await searchService.getPlaylistitems(this.id);
|
const playlistItems = await searchService.getPlaylistitems(this.id);
|
||||||
const tracks = playlistItems.map(async (x) =>
|
return convertToTracks(playlistItems, searchService);
|
||||||
(await x.toTracks(searchService)).find((x) => x !== null),
|
|
||||||
);
|
|
||||||
return await Promise.all(tracks);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,20 @@ export class SearchHint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static constructFromHint(hint: JellyfinSearchHint) {
|
static constructFromHint(hint: JellyfinSearchHint) {
|
||||||
|
if (hint.Id === undefined || !hint.Name || !hint.RunTimeTicks) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to construct search hint, required properties were undefined',
|
||||||
|
);
|
||||||
|
}
|
||||||
return new SearchHint(hint.Id, hint.Name, hint.RunTimeTicks / 10000);
|
return new SearchHint(hint.Id, hint.Name, hint.RunTimeTicks / 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
static constructFromBaseItem(baseItem: BaseItemDto) {
|
static constructFromBaseItem(baseItem: BaseItemDto) {
|
||||||
|
if (baseItem.Id === undefined || !baseItem.Name || !baseItem.RunTimeTicks) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to construct search hint from base item, required properties were undefined',
|
||||||
|
);
|
||||||
|
}
|
||||||
return new SearchHint(
|
return new SearchHint(
|
||||||
baseItem.Id,
|
baseItem.Id,
|
||||||
baseItem.Name,
|
baseItem.Name,
|
||||||
|
@ -24,7 +24,7 @@ export class Playlist {
|
|||||||
* @returns active track or undefined if there's none
|
* @returns active track or undefined if there's none
|
||||||
*/
|
*/
|
||||||
getActiveTrack(): Track | undefined {
|
getActiveTrack(): Track | undefined {
|
||||||
if (this.isActiveTrackOutOfSync()) {
|
if (this.isActiveTrackOutOfSync() || this.activeTrackIndex === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return this.tracks[this.activeTrackIndex];
|
return this.tracks[this.activeTrackIndex];
|
||||||
@ -51,7 +51,10 @@ export class Playlist {
|
|||||||
setNextTrackAsActiveTrack(): boolean {
|
setNextTrackAsActiveTrack(): boolean {
|
||||||
this.announceTrackFinishIfSet();
|
this.announceTrackFinishIfSet();
|
||||||
|
|
||||||
if (this.activeTrackIndex >= this.tracks.length) {
|
if (
|
||||||
|
this.activeTrackIndex === undefined ||
|
||||||
|
this.activeTrackIndex >= this.tracks.length
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +73,7 @@ export class Playlist {
|
|||||||
setPreviousTrackAsActiveTrack(): boolean {
|
setPreviousTrackAsActiveTrack(): boolean {
|
||||||
this.announceTrackFinishIfSet();
|
this.announceTrackFinishIfSet();
|
||||||
|
|
||||||
if (this.activeTrackIndex <= 0) {
|
if (this.activeTrackIndex === undefined || this.activeTrackIndex <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +123,7 @@ export class Playlist {
|
|||||||
* @returns if there is a track next in the playlist
|
* @returns if there is a track next in the playlist
|
||||||
*/
|
*/
|
||||||
hasNextTrackInPlaylist() {
|
hasNextTrackInPlaylist() {
|
||||||
return this.activeTrackIndex + 1 < this.tracks.length;
|
return (this.activeTrackIndex ?? 0) + 1 < this.tracks.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,7 +131,7 @@ export class Playlist {
|
|||||||
* @returns if there is a previous track in the playlist
|
* @returns if there is a previous track in the playlist
|
||||||
*/
|
*/
|
||||||
hasPreviousTrackInPlaylist() {
|
hasPreviousTrackInPlaylist() {
|
||||||
return this.activeTrackIndex > 0;
|
return this.activeTrackIndex !== undefined && this.activeTrackIndex > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@ -156,13 +159,20 @@ export class Playlist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeTrack = this.getActiveTrack();
|
const activeTrack = this.getActiveTrack();
|
||||||
|
|
||||||
|
if (!activeTrack) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
activeTrack.playing = true;
|
activeTrack.playing = true;
|
||||||
this.eventEmitter.emit('internal.audio.track.announce', activeTrack);
|
this.eventEmitter.emit('internal.audio.track.announce', activeTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isActiveTrackOutOfSync(): boolean {
|
private isActiveTrackOutOfSync(): boolean {
|
||||||
return (
|
return (
|
||||||
this.activeTrackIndex < 0 || this.activeTrackIndex >= this.tracks.length
|
this.activeTrackIndex === undefined ||
|
||||||
|
this.activeTrackIndex < 0 ||
|
||||||
|
this.activeTrackIndex >= this.tracks.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,6 @@ export class Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRemoteImages(): RemoteImageInfo[] {
|
getRemoteImages(): RemoteImageInfo[] {
|
||||||
return this.remoteImages.Images;
|
return this.remoteImages?.Images ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export class PlayNowCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSelection(): string[] {
|
getSelection(): string[] {
|
||||||
if (this.hasSelection()) {
|
if (this.hasSelection() && this.StartIndex !== undefined) {
|
||||||
return [this.ItemIds[this.StartIndex]];
|
return [this.ItemIds[this.StartIndex]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { DiscordMessageService } from '../clients/discord/discord.message.servic
|
|||||||
import { GithubRelease } from '../models/github-release';
|
import { GithubRelease } from '../models/github-release';
|
||||||
import { useDefaultMockerToken } from '../utils/tests/defaultMockerToken';
|
import { useDefaultMockerToken } from '../utils/tests/defaultMockerToken';
|
||||||
import { UpdatesService } from './updates.service';
|
import { UpdatesService } from './updates.service';
|
||||||
|
import { InjectionToken } from '@nestjs/common';
|
||||||
|
|
||||||
// mock axios: https://stackoverflow.com/questions/51275434/type-of-axios-mock-using-jest-typescript/55351900#55351900
|
// mock axios: https://stackoverflow.com/questions/51275434/type-of-axios-mock-using-jest-typescript/55351900#55351900
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
@ -49,7 +50,7 @@ describe('UpdatesService', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return useDefaultMockerToken(token);
|
return useDefaultMockerToken(token as InjectionToken);
|
||||||
})
|
})
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
@ -30,6 +30,14 @@ export class UpdatesService {
|
|||||||
this.logger.debug('Checking for available updates...');
|
this.logger.debug('Checking for available updates...');
|
||||||
|
|
||||||
const latestGitHubRelease = await this.fetchLatestGithubRelease();
|
const latestGitHubRelease = await this.fetchLatestGithubRelease();
|
||||||
|
|
||||||
|
if (!latestGitHubRelease) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Aborting update check because api request failed. Please check your internet connection or disable the check`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentVersion = Constants.Metadata.Version.All();
|
const currentVersion = Constants.Metadata.Version.All();
|
||||||
|
|
||||||
if (latestGitHubRelease.tag_name <= currentVersion) {
|
if (latestGitHubRelease.tag_name <= currentVersion) {
|
||||||
@ -95,21 +103,21 @@ export class UpdatesService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchLatestGithubRelease(): Promise<null | GithubRelease> {
|
private async fetchLatestGithubRelease(): Promise<GithubRelease | undefined> {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: Constants.Links.Api.GetLatestRelease,
|
url: Constants.Links.Api.GetLatestRelease,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data as GithubRelease;
|
return response.data as GithubRelease;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.logger.error('Error while checking for updates', err);
|
this.logger.error('Error while checking for updates', err);
|
||||||
return null;
|
return undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
src/utils/trackConverter.ts
Normal file
15
src/utils/trackConverter.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { JellyfinSearchService } from 'src/clients/jellyfin/jellyfin.search.service';
|
||||||
|
import { SearchHint } from 'src/models/search/SearchHint';
|
||||||
|
import { Track } from 'src/models/shared/Track';
|
||||||
|
|
||||||
|
export const convertToTracks = (
|
||||||
|
hints: SearchHint[],
|
||||||
|
jellyfinSearchService: JellyfinSearchService,
|
||||||
|
): Track[] => {
|
||||||
|
let tracks: Track[] = [];
|
||||||
|
hints.forEach(async (hint) => {
|
||||||
|
const searchedTracks = await hint.toTracks(jellyfinSearchService);
|
||||||
|
tracks = [...tracks, ...searchedTracks];
|
||||||
|
});
|
||||||
|
return tracks;
|
||||||
|
};
|
@ -12,7 +12,7 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
Loading…
Reference in New Issue
Block a user