From 916969f07b0be1528afb3cd9be5c04c39e18bcb4 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Tue, 7 Mar 2023 21:22:19 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20paged=20playlist=20(#106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- src/app.module.ts | 3 + src/clients/discord/discord.voice.service.ts | 6 +- .../jellyfin/jellyfin.search.service.ts | 8 + .../jellyfin/jellyfin.websocket.service.ts | 2 +- src/commands/command.module.ts | 4 +- src/commands/play.comands.ts | 16 +- src/commands/playlist.command.ts | 88 --------- src/commands/playlist/playlist.command.ts | 181 ++++++++++++++++++ .../playlist.interaction-collector.ts | 78 ++++++++ src/main.ts | 18 +- src/models/search/AlbumSearchHint.ts | 4 +- src/models/search/PlaylistSearchHint.ts | 4 +- src/models/search/SearchHint.ts | 13 +- .../{GenericPlaylist.ts => Playlist.ts} | 21 +- .../shared/{GenericTrack.ts => Track.ts} | 11 +- src/playback/playback.service.ts | 8 +- src/utils/arrayUtils.ts | 4 + src/utils/timeUtils.ts | 8 +- yarn.lock | 16 +- 20 files changed, 353 insertions(+), 144 deletions(-) delete mode 100644 src/commands/playlist.command.ts create mode 100644 src/commands/playlist/playlist.command.ts create mode 100644 src/commands/playlist/playlist.interaction-collector.ts rename src/models/shared/{GenericPlaylist.ts => Playlist.ts} (90%) rename src/models/shared/{GenericTrack.ts => Track.ts} (81%) create mode 100644 src/utils/arrayUtils.ts diff --git a/package.json b/package.json index a50a263..3cb5ec9 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@discord-nestjs/common": "^5.2.0", - "@discord-nestjs/core": "^5.3.0", + "@discord-nestjs/common": "^5.2.1", + "@discord-nestjs/core": "^5.3.3", "@discordjs/opus": "^0.9.0", "@discordjs/voice": "^0.14.0", "@jellyfin/sdk": "^0.7.0", diff --git a/src/app.module.ts b/src/app.module.ts index ba19405..7a96634 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,6 +24,9 @@ import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module'; JELLYFIN_AUTHENTICATION_USERNAME: Joi.string().required(), JELLYFIN_AUTHENTICATION_PASSWORD: Joi.string().required(), UPDATER_DISABLE_NOTIFICATIONS: Joi.boolean(), + LOG_LEVEL: Joi.string() + .valid('error', 'warn', 'log', 'debug', 'verbose') + .default('log'), PORT: Joi.number().min(1), }), }), diff --git a/src/clients/discord/discord.voice.service.ts b/src/clients/discord/discord.voice.service.ts index aae3abe..20139b9 100644 --- a/src/clients/discord/discord.voice.service.ts +++ b/src/clients/discord/discord.voice.service.ts @@ -22,7 +22,7 @@ import { JellyfinStreamBuilderService } from '../jellyfin/jellyfin.stream.builde import { JellyfinWebSocketService } from '../jellyfin/jellyfin.websocket.service'; import { GenericTryHandler } from '../../models/generic-try-handler'; import { PlaybackService } from '../../playback/playback.service'; -import { GenericTrack } from '../../models/shared/GenericTrack'; +import { Track } from '../../models/shared/Track'; import { DiscordMessageService } from './discord.message.service'; @@ -41,7 +41,7 @@ export class DiscordVoiceService { ) {} @OnEvent('internal.audio.announce') - handleOnNewTrack(track: GenericTrack) { + handleOnNewTrack(track: Track) { const resource = createAudioResource( track.getStreamUrl(this.jellyfinStreamBuilder), ); @@ -69,7 +69,7 @@ export class DiscordVoiceService { success: false, reply: { embeds: [ - this.discordMessageService.buildErrorMessage({ + this.discordMessageService.buildMessage({ title: 'Unable to join your channel', description: "I am unable to join your channel, because you don't seem to be in a voice channel. Connect to a channel first to use this command", diff --git a/src/clients/jellyfin/jellyfin.search.service.ts b/src/clients/jellyfin/jellyfin.search.service.ts index 7ec3f34..0f9c087 100644 --- a/src/clients/jellyfin/jellyfin.search.service.ts +++ b/src/clients/jellyfin/jellyfin.search.service.ts @@ -134,6 +134,10 @@ export class JellyfinSearchService { const api = this.jellyfinService.getApi(); const remoteImageApi = getRemoteImageApi(api); + this.logger.verbose( + `Searching for remote images of item '${id}' with limit of ${limit}`, + ); + try { const axiosReponse = await remoteImageApi.getRemoteImages({ itemId: id, @@ -151,6 +155,10 @@ export class JellyfinSearchService { TotalRecordCount: 0, }; } + + this.logger.verbose( + `Retrieved ${axiosReponse.data.TotalRecordCount} remote images from Jellyfin`, + ); return axiosReponse.data; } catch (err) { this.logger.error(`Failed to retrieve remote images: ${err}`); diff --git a/src/clients/jellyfin/jellyfin.websocket.service.ts b/src/clients/jellyfin/jellyfin.websocket.service.ts index a102965..21bb8d6 100644 --- a/src/clients/jellyfin/jellyfin.websocket.service.ts +++ b/src/clients/jellyfin/jellyfin.websocket.service.ts @@ -14,7 +14,7 @@ import { PlayNowCommand, SessionApiSendPlaystateCommandRequest, } from '../../types/websocket'; -import { GenericTrack } from '../../models/shared/GenericTrack'; +import { Track } from '../../models/shared/Track'; import { JellyfinSearchService } from './jellyfin.search.service'; import { JellyfinService } from './jellyfin.service'; diff --git a/src/commands/command.module.ts b/src/commands/command.module.ts index bcc2ce9..85dce60 100644 --- a/src/commands/command.module.ts +++ b/src/commands/command.module.ts @@ -4,7 +4,7 @@ import { Module } from '@nestjs/common'; import { DiscordClientModule } from '../clients/discord/discord.module'; import { JellyfinClientModule } from '../clients/jellyfin/jellyfin.module'; import { PlaybackModule } from '../playback/playback.module'; -import { PlaylistCommand } from './playlist.command'; +import { PlaylistCommand } from './playlist/playlist.command'; import { DisconnectCommand } from './disconnect.command'; import { HelpCommand } from './help.command'; import { PausePlaybackCommand } from './pause.command'; @@ -14,6 +14,7 @@ import { SkipTrackCommand } from './next.command'; import { StatusCommand } from './status.command'; import { StopPlaybackCommand } from './stop.command'; import { SummonCommand } from './summon.command'; +import { PlaylistInteractionCollector } from './playlist/playlist.interaction-collector'; @Module({ imports: [ @@ -24,6 +25,7 @@ import { SummonCommand } from './summon.command'; ], controllers: [], providers: [ + PlaylistInteractionCollector, HelpCommand, StatusCommand, PlaylistCommand, diff --git a/src/commands/play.comands.ts b/src/commands/play.comands.ts index e6a8cff..bbc871f 100644 --- a/src/commands/play.comands.ts +++ b/src/commands/play.comands.ts @@ -48,7 +48,7 @@ export class PlayItemCommand { @InteractionEvent(SlashCommandPipe) dto: TrackRequestDto, @IA() interaction: CommandInteraction, ): Promise { - await interaction.deferReply(); + await interaction.deferReply({ ephemeral: true }); const baseItems = TrackRequestDto.getBaseItemKinds(dto.type); @@ -72,6 +72,7 @@ export class PlayItemCommand { description: `- Check for any misspellings\n- Grant me access to your desired libraries\n- Avoid special characters`, }), ], + ephemeral: true, }); return; } @@ -96,20 +97,18 @@ export class PlayItemCommand { (sum, item) => sum + item.duration, 0, ); - const enqueuedCount = this.playbackService - .getPlaylistOrDefault() - .enqueueTracks(tracks); - - console.log(tracks); + this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks); const remoteImage: RemoteImageInfo | undefined = tracks - .map((x) => x.getRemoteImage()) + .flatMap((x) => x.getRemoteImages()) .find((x) => true); await interaction.followUp({ embeds: [ this.discordMessageService.buildMessage({ - title: `Added ${enqueuedCount} tracks to your playlist (${formatMillisecondsAsHumanReadable( + title: `Added ${this.playbackService + .getPlaylistOrDefault() + .getLength()} tracks to your playlist (${formatMillisecondsAsHumanReadable( reducedDuration, )})`, mixin(embedBuilder) { @@ -120,6 +119,7 @@ export class PlayItemCommand { }, }), ], + ephemeral: true, }); } diff --git a/src/commands/playlist.command.ts b/src/commands/playlist.command.ts deleted file mode 100644 index 7908beb..0000000 --- a/src/commands/playlist.command.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Command, Handler, IA } from '@discord-nestjs/core'; - -import { Injectable } from '@nestjs/common'; - -import { CommandInteraction } from 'discord.js'; - -import { PlaybackService } from '../playback/playback.service'; -import { Constants } from '../utils/constants'; -import { formatMillisecondsAsHumanReadable } from '../utils/timeUtils'; -import { DiscordMessageService } from '../clients/discord/discord.message.service'; -import { trimStringToFixedLength } from '../utils/stringUtils/stringUtils'; - -@Injectable() -@Command({ - name: 'playlist', - description: 'Print the current track information', -}) -export class PlaylistCommand { - constructor( - private readonly discordMessageService: DiscordMessageService, - private readonly playbackService: PlaybackService, - ) {} - - @Handler() - async handler(@IA() interaction: CommandInteraction): Promise { - const playlist = this.playbackService.getPlaylistOrDefault(); - - if (playlist.isEmpty()) { - await interaction.reply({ - embeds: [ - this.discordMessageService.buildMessage({ - title: 'Your Playlist', - description: - 'You do not have any tracks in your playlist.\nUse the ``/play`` command to add new tracks to your playlist', - }), - ], - ephemeral: true, - }); - return; - } - - const tracklist = playlist.tracks - .map((track, index) => { - const isCurrent = track === playlist.getActiveTrack(); - - let point = this.getListPoint(isCurrent, index); - point += `**${trimStringToFixedLength(track.name, 30)}**`; - - if (isCurrent) { - point += ' :loud_sound:'; - } - - point += '\n'; - point += Constants.Design.InvisibleSpace.repeat(2); - point += formatMillisecondsAsHumanReadable(track.getDuration()); - - return point; - }) - .slice(0, 10) - .join('\n'); - const remoteImage = undefined; - - await interaction.reply({ - embeds: [ - this.discordMessageService.buildMessage({ - title: 'Your Playlist', - description: `${tracklist}\n\nUse the /skip and /previous command to select a track`, - mixin(embedBuilder) { - if (remoteImage === undefined) { - return embedBuilder; - } - - return embedBuilder.setThumbnail(remoteImage.Url); - }, - }), - ], - ephemeral: true, - }); - } - - private getListPoint(isCurrent: boolean, index: number) { - if (isCurrent) { - return `${index + 1}. `; - } - - return `${index + 1}. `; - } -} diff --git a/src/commands/playlist/playlist.command.ts b/src/commands/playlist/playlist.command.ts new file mode 100644 index 0000000..4b837b3 --- /dev/null +++ b/src/commands/playlist/playlist.command.ts @@ -0,0 +1,181 @@ +import { CollectorInterceptor, SlashCommandPipe } from '@discord-nestjs/common'; +import { + AppliedCollectors, + Command, + Handler, + IA, + InteractionEvent, + Param, + ParamType, + UseCollectors, +} from '@discord-nestjs/core'; + +import { Injectable, Logger, UseInterceptors } from '@nestjs/common'; + +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + CommandInteraction, + EmbedBuilder, + InteractionCollector, + InteractionReplyOptions, + InteractionUpdateOptions, +} from 'discord.js'; + +import { PlaybackService } from '../../playback/playback.service'; +import { chunkArray } from '../../utils/arrayUtils'; +import { Constants } from '../../utils/constants'; +import { formatMillisecondsAsHumanReadable } from '../../utils/timeUtils'; +import { DiscordMessageService } from '../../clients/discord/discord.message.service'; +import { Track } from '../../models/shared/Track'; +import { trimStringToFixedLength } from '../../utils/stringUtils/stringUtils'; + +import { PlaylistInteractionCollector } from './playlist.interaction-collector'; + +class PlaylistCommandDto { + @Param({ + required: false, + description: 'The page', + type: ParamType.INTEGER, + }) + page: number; +} + +@Injectable() +@Command({ + name: 'playlist', + description: 'Print the current track information', +}) +@UseInterceptors(CollectorInterceptor) +@UseCollectors(PlaylistInteractionCollector) +export class PlaylistCommand { + public pageData: Map = new Map(); + private readonly logger = new Logger(PlaylistCommand.name); + + constructor( + private readonly discordMessageService: DiscordMessageService, + private readonly playbackService: PlaybackService, + ) {} + + @Handler() + async handler( + @InteractionEvent(SlashCommandPipe) dto: PlaylistCommandDto, + @IA() interaction: CommandInteraction, + @AppliedCollectors(0) collector: InteractionCollector, + ): Promise { + const page = dto.page ?? 0; + + const response = await interaction.reply( + this.getReplyForPage(page) as InteractionReplyOptions, + ); + + this.pageData.set(response.id, page); + this.logger.debug( + `Added '${interaction.id}' as a message id for page storage`, + ); + } + + private getChunks() { + const playlist = this.playbackService.getPlaylistOrDefault(); + return chunkArray(playlist.tracks, 10); + } + + public getReplyForPage( + page: number, + ): InteractionReplyOptions | InteractionUpdateOptions { + const chunks = this.getChunks(); + + if (page >= chunks.length) { + return { + embeds: [ + this.discordMessageService.buildMessage({ + title: 'Page does not exist', + description: 'Please pass a valid page', + }), + ], + ephemeral: true, + }; + } + + const contentForPage = this.getContentForPage(chunks, page); + + if (!contentForPage) { + return { + embeds: [ + this.discordMessageService.buildMessage({ + title: 'Your Playlist', + description: + 'You do not have any tracks in your playlist.\nUse the ``/play`` command to add new tracks to your playlist', + }), + ], + ephemeral: true, + }; + } + + const hasPrevious = page; + const hasNext = page + 1 < chunks.length; + + const rowBuilder = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setDisabled(!hasPrevious) + .setCustomId('playlist-controls-previous') + .setEmoji('◀️') + .setLabel('Previous') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setDisabled(!hasNext) + .setCustomId('playlist-controls-next') + .setEmoji('▶️') + .setLabel('Next') + .setStyle(ButtonStyle.Secondary), + ); + + return { + embeds: [contentForPage.toJSON()], + ephemeral: true, + components: [rowBuilder], + }; + } + + private getContentForPage( + chunks: Track[][], + page: number, + ): EmbedBuilder | undefined { + const playlist = this.playbackService.getPlaylistOrDefault(); + + if (page >= chunks.length || page < 0) { + return undefined; + } + + const content = chunks[page] + .map((track, index) => { + const isCurrent = track === playlist.getActiveTrack(); + + let point = this.getListPoint(isCurrent, index); + point += `**${trimStringToFixedLength(track.name, 30)}**`; + + if (isCurrent) { + point += ' :loud_sound:'; + } + + point += '\n'; + point += Constants.Design.InvisibleSpace.repeat(2); + point += formatMillisecondsAsHumanReadable(track.getDuration()); + + return point; + }) + .join('\n'); + + return new EmbedBuilder().setTitle('Your playlist').setDescription(content); + } + + private getListPoint(isCurrent: boolean, index: number) { + if (isCurrent) { + return `${index + 1}. `; + } + + return `${index + 1}. `; + } +} diff --git a/src/commands/playlist/playlist.interaction-collector.ts b/src/commands/playlist/playlist.interaction-collector.ts new file mode 100644 index 0000000..3476918 --- /dev/null +++ b/src/commands/playlist/playlist.interaction-collector.ts @@ -0,0 +1,78 @@ +import { + Filter, + InjectCauseEvent, + InteractionEventCollector, + On, +} from '@discord-nestjs/core'; + +import { forwardRef, Inject, Injectable, Scope } from '@nestjs/common'; +import { Logger } from '@nestjs/common/services'; + +import { + ButtonInteraction, + ChatInputCommandInteraction, + InteractionUpdateOptions, +} from 'discord.js'; + +import { PlaylistCommand } from './playlist.command'; + +@Injectable({ scope: Scope.REQUEST }) +@InteractionEventCollector({ time: 15 * 1000 }) +export class PlaylistInteractionCollector { + private readonly logger = new Logger(PlaylistInteractionCollector.name); + + constructor( + @Inject(forwardRef(() => PlaylistCommand)) + private readonly playlistCommand: PlaylistCommand, + @InjectCauseEvent() + private readonly causeInteraction: ChatInputCommandInteraction, + ) {} + + @Filter() + filter(interaction: ButtonInteraction): boolean { + return this.causeInteraction.id === interaction.message.interaction.id; + } + + @On('collect') + async onCollect(interaction: ButtonInteraction): Promise { + const targetPage = this.getInteraction(interaction); + + if (targetPage === undefined) { + await interaction.update({ + content: 'Unknown error', + }); + return; + } + + this.logger.debug( + `Updating current page for interaction ${this.causeInteraction.id} to ${targetPage}`, + ); + this.playlistCommand.pageData.set(this.causeInteraction.id, targetPage); + const reply = this.playlistCommand.getReplyForPage(targetPage); + await interaction.update(reply as InteractionUpdateOptions); + } + + private getInteraction(interaction: ButtonInteraction): number | null { + const current = this.playlistCommand.pageData.get(this.causeInteraction.id); + + this.logger.debug( + `Retrieved current page from command using id '${ + this.causeInteraction.id + }' in list of ${ + Object.keys(this.playlistCommand.pageData).length + }: ${current}`, + ); + + switch (interaction.customId) { + case 'playlist-controls-next': + return current + 1; + case 'playlist-controls-previous': + return current - 1; + default: + this.logger.error( + `Unable to map button interaction from collector to target page`, + ); + return undefined; + } + } +} diff --git a/src/main.ts b/src/main.ts index eab8408..088755b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,11 +4,21 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; function getLoggingLevels(): LogLevel[] { - if (process.env.DEBUG) { - return ['log', 'error', 'warn', 'debug']; + switch (process.env.LOG_LEVEL.toLowerCase()) { + case 'error': + return ['error']; + case 'warn': + return ['error', 'warn']; + case 'log': + return ['error', 'warn', 'log']; + case 'debug': + return ['error', 'warn', 'log', 'debug']; + case 'verbose': + return ['error', 'warn', 'log', 'debug', 'verbose']; + default: + console.log(`failed to process log level ${process.env.LOG_LEVEL}`); + return ['error', 'warn', 'log']; } - - return ['log', 'error', 'warn']; } async function bootstrap() { diff --git a/src/models/search/AlbumSearchHint.ts b/src/models/search/AlbumSearchHint.ts index b1be3a5..4cec06c 100644 --- a/src/models/search/AlbumSearchHint.ts +++ b/src/models/search/AlbumSearchHint.ts @@ -1,6 +1,6 @@ import { SearchHint as JellyfinSearchHint } from '@jellyfin/sdk/lib/generated-client/models'; -import { GenericTrack } from '../shared/GenericTrack'; +import { Track } from '../shared/Track'; import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service'; import { SearchHint } from './SearchHint'; @@ -16,7 +16,7 @@ export class AlbumSearchHint extends SearchHint { override async toTracks( searchService: JellyfinSearchService, - ): Promise { + ): Promise { const albumItems = await searchService.getAlbumItems(this.id); const tracks = albumItems.map(async (x) => (await x.toTracks(searchService)).find((x) => x !== null), diff --git a/src/models/search/PlaylistSearchHint.ts b/src/models/search/PlaylistSearchHint.ts index 7d501f3..dd308c4 100644 --- a/src/models/search/PlaylistSearchHint.ts +++ b/src/models/search/PlaylistSearchHint.ts @@ -1,6 +1,6 @@ import { SearchHint as JellyfinSearchHint } from '@jellyfin/sdk/lib/generated-client/models'; -import { GenericTrack } from '../shared/GenericTrack'; +import { Track } from '../shared/Track'; import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service'; import { SearchHint } from './SearchHint'; @@ -20,7 +20,7 @@ export class PlaylistSearchHint extends SearchHint { override async toTracks( searchService: JellyfinSearchService, - ): Promise { + ): Promise { const playlistItems = await searchService.getPlaylistitems(this.id); const tracks = playlistItems.map(async (x) => (await x.toTracks(searchService)).find((x) => x !== null), diff --git a/src/models/search/SearchHint.ts b/src/models/search/SearchHint.ts index 462deda..c890b25 100644 --- a/src/models/search/SearchHint.ts +++ b/src/models/search/SearchHint.ts @@ -1,6 +1,6 @@ import { SearchHint as JellyfinSearchHint } from '@jellyfin/sdk/lib/generated-client/models'; -import { GenericTrack } from '../shared/GenericTrack'; +import { Track } from '../shared/Track'; import { JellyfinSearchService } from '../../clients/jellyfin/jellyfin.search.service'; export class SearchHint { @@ -14,17 +14,10 @@ export class SearchHint { return `🎵 ${this.name}`; } - async toTracks( - searchService: JellyfinSearchService, - ): Promise { + async toTracks(searchService: JellyfinSearchService): Promise { const remoteImages = await searchService.getRemoteImageById(this.id); return [ - new GenericTrack( - this.id, - this.name, - this.runtimeInMilliseconds, - remoteImages, - ), + new Track(this.id, this.name, this.runtimeInMilliseconds, remoteImages), ]; } diff --git a/src/models/shared/GenericPlaylist.ts b/src/models/shared/Playlist.ts similarity index 90% rename from src/models/shared/GenericPlaylist.ts rename to src/models/shared/Playlist.ts index 13724b1..616e80c 100644 --- a/src/models/shared/GenericPlaylist.ts +++ b/src/models/shared/Playlist.ts @@ -1,9 +1,9 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; -import { GenericTrack } from './GenericTrack'; +import { Track } from './Track'; -export class GenericPlaylist { - tracks: GenericTrack[]; +export class Playlist { + tracks: Track[]; activeTrackIndex?: number; constructor(private readonly eventEmitter: EventEmitter2) { @@ -23,7 +23,7 @@ export class GenericPlaylist { * Checks if the active track is out of bounds * @returns active track or undefined if there's none */ - getActiveTrack(): GenericTrack | undefined { + getActiveTrack(): Track | undefined { if (this.isActiveTrackOutOfSync()) { return undefined; } @@ -40,6 +40,10 @@ export class GenericPlaylist { ); } + getLength() { + return this.tracks.length; + } + /** * Go to the next track in the playlist * @returns if the track has been changed successfully @@ -79,13 +83,18 @@ export class GenericPlaylist { * @param tracks the tracks that should be added * @returns the new lendth of the tracks in the playlist */ - enqueueTracks(tracks: GenericTrack[]) { + enqueueTracks(tracks: Track[]) { this.eventEmitter.emit('controls.playlist.tracks.enqueued', { count: tracks.length, activeTrack: this.activeTrackIndex, }); const length = this.tracks.push(...tracks); - this.announceTrackChange(); + + // emit a track change if there is no item + if (!this.activeTrackIndex) { + this.announceTrackChange(); + } + return length; } diff --git a/src/models/shared/GenericTrack.ts b/src/models/shared/Track.ts similarity index 81% rename from src/models/shared/GenericTrack.ts rename to src/models/shared/Track.ts index 6f0b92c..7ef3b7c 100644 --- a/src/models/shared/GenericTrack.ts +++ b/src/models/shared/Track.ts @@ -1,8 +1,11 @@ -import { RemoteImageInfo, RemoteImageResult } from '@jellyfin/sdk/lib/generated-client/models'; +import { + RemoteImageInfo, + RemoteImageResult, +} from '@jellyfin/sdk/lib/generated-client/models'; import { JellyfinStreamBuilderService } from '../../clients/jellyfin/jellyfin.stream.builder.service'; -export class GenericTrack { +export class Track { /** * The identifier of this track, structured as a UID. * This id can be used to build a stream url and send more API requests to Jellyfin @@ -44,7 +47,7 @@ export class GenericTrack { return streamBuilder.buildStreamUrl(this.id, 96000); } - getRemoteImage(): RemoteImageInfo | undefined { - return this.remoteImages.Images.find((x) => true); + getRemoteImages(): RemoteImageInfo[] { + return this.remoteImages.Images; } } diff --git a/src/playback/playback.service.ts b/src/playback/playback.service.ts index 5a0b282..067ecf9 100644 --- a/src/playback/playback.service.ts +++ b/src/playback/playback.service.ts @@ -1,21 +1,21 @@ import { Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { GenericPlaylist } from '../models/shared/GenericPlaylist'; +import { Playlist } from '../models/shared/Playlist'; @Injectable() export class PlaybackService { private readonly logger = new Logger(PlaybackService.name); - private playlist: GenericPlaylist | undefined = undefined; + private playlist: Playlist | undefined = undefined; constructor(private readonly eventEmitter: EventEmitter2) {} - getPlaylistOrDefault(): GenericPlaylist { + getPlaylistOrDefault(): Playlist { if (this.playlist) { return this.playlist; } - this.playlist = new GenericPlaylist(this.eventEmitter); + this.playlist = new Playlist(this.eventEmitter); return this.playlist; } } diff --git a/src/utils/arrayUtils.ts b/src/utils/arrayUtils.ts new file mode 100644 index 0000000..12d0c7e --- /dev/null +++ b/src/utils/arrayUtils.ts @@ -0,0 +1,4 @@ +export const chunkArray = (a: T[], size): T[][] => + Array.from(new Array(Math.ceil(a.length / size)), (_, i) => + a.slice(i * size, i * size + size), + ); diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts index 8118e6f..c6b5167 100644 --- a/src/utils/timeUtils.ts +++ b/src/utils/timeUtils.ts @@ -1,11 +1,17 @@ import { formatDuration, intervalToDuration } from 'date-fns'; -export const formatMillisecondsAsHumanReadable = (milliseconds: number) => { +export const formatMillisecondsAsHumanReadable = ( + milliseconds: number, + format = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'], +) => { const duration = formatDuration( intervalToDuration({ start: milliseconds, end: 0, }), + { + format: format, + }, ); return duration; }; diff --git a/yarn.lock b/yarn.lock index 0021ef0..773d07c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -359,19 +359,19 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@discord-nestjs/common@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@discord-nestjs/common/-/common-5.2.0.tgz#3bdf25eadf8372d81110e2aeefbb31e707e75554" - integrity sha512-aXp6P7XyDk/Zoz9zpe5DLqGFBfZrz1fu6Vc8oMz2RggVxBm8k8P5bH5iOcIvI0jWsjbZ3pVCVB3SmCpjBFItRA== +"@discord-nestjs/common@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@discord-nestjs/common/-/common-5.2.1.tgz#113a6a67481c9bb5d2e7a0ee76ee61dea555c489" + integrity sha512-6JP53oA6Fysh1Xj3i30zaJTQIZWoPiigqbHjjzPFOMUSjKbaIEX0/75gZm0JBHCPw9oUnVBGq8taV200pyiosg== dependencies: "@nestjs/mapped-types" "1.2.2" class-transformer "0.5.1" class-validator "0.14.0" -"@discord-nestjs/core@^5.3.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.0.tgz#8e93b0310e8cc2c0cde74c6317d949d6e9d28d2d" - integrity sha512-eHVzuPCu3EbQyTln+ZEH0/Jwe0xPG7Z1eZV655jZoCSpq7RmvxWcTG/REf3XMSgtYFN27HaXa9vPCsgUOnp9xQ== +"@discord-nestjs/core@^5.3.3": + version "5.3.3" + resolved "https://registry.yarnpkg.com/@discord-nestjs/core/-/core-5.3.3.tgz#0e0af8cfc7b1c6df0dd9668573a51b44c3940033" + integrity sha512-R3duQIUU9qQiKEIyleG2swdDdGp3FXaXHbgooVieyEJVx8tvIulH5BryE0lnYiUdPEvXbZrBF+w476PATiDWMQ== dependencies: class-transformer "0.5.1"