Merge pull request #149 from manuel-rw/refactor/strict-typescript

♻️ Strict Typescript
This commit is contained in:
Manuel 2023-04-02 17:41:14 +02:00 committed by GitHub
commit 65d401fdbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 204 additions and 61 deletions

View File

@ -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,

View File

@ -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();

View File

@ -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;
} }
} }
} }

View File

@ -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}'`,
); );

View File

@ -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]:

View File

@ -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;

View File

@ -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) {

View File

@ -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);

View File

@ -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();

View File

@ -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();

View File

@ -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'];

View File

@ -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;
}); });

View File

@ -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);
} }
} }

View File

@ -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,

View File

@ -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
); );
} }
} }

View File

@ -51,6 +51,6 @@ export class Track {
} }
getRemoteImages(): RemoteImageInfo[] { getRemoteImages(): RemoteImageInfo[] {
return this.remoteImages.Images; return this.remoteImages?.Images ?? [];
} }
} }

View File

@ -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]];
} }

View File

@ -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();

View File

@ -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;
}); });
} }
} }

View 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;
};

View File

@ -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,