diff --git a/src/clients/discord/discord.voice.service.ts b/src/clients/discord/discord.voice.service.ts index ba5775f..fe86177 100644 --- a/src/clients/discord/discord.voice.service.ts +++ b/src/clients/discord/discord.voice.service.ts @@ -116,6 +116,7 @@ export class DiscordVoiceService { /** * Pauses the current audio player */ + @OnEvent('internal.voice.controls.pause') pause() { this.createAndReturnOrGetAudioPlayer().pause(); this.eventEmitter.emit('playback.state.pause', true); @@ -124,6 +125,7 @@ export class DiscordVoiceService { /** * Stops the audio player */ + @OnEvent('internal.voice.controls.stop') stop(force: boolean): boolean { const stopped = this.createAndReturnOrGetAudioPlayer().stop(force); this.eventEmitter.emit('playback.state.stop'); @@ -161,6 +163,7 @@ export class DiscordVoiceService { * Checks if the current state is paused or not and toggles the states to the opposite. * @returns The new paused state - true: paused, false: unpaused */ + @OnEvent('internal.voice.controls.togglePause') togglePaused(): boolean { if (this.isPaused()) { this.unpause(); diff --git a/src/clients/jellyfin/jellyfin.playstate.service.ts b/src/clients/jellyfin/jellyfin.playstate.service.ts index e394926..2d75cd2 100644 --- a/src/clients/jellyfin/jellyfin.playstate.service.ts +++ b/src/clients/jellyfin/jellyfin.playstate.service.ts @@ -71,4 +71,23 @@ export class JellyinPlaystateService { }, }); } + + @OnEvent('playback.state.pause') + private async onPlaybackPause(paused: boolean) { + const track = this.playbackService.getPlaylistOrDefault().getActiveTrack(); + + if (!track) { + this.logger.error( + `Unable to report changed playstate to Jellyfin because no track was active`, + ); + return; + } + + this.playstateApi.reportPlaybackProgress({ + playbackProgressInfo: { + IsPaused: paused, + ItemId: track.id, + }, + }); + } } diff --git a/src/clients/jellyfin/jellyfin.search.service.ts b/src/clients/jellyfin/jellyfin.search.service.ts index a5733a9..ffe755e 100644 --- a/src/clients/jellyfin/jellyfin.search.service.ts +++ b/src/clients/jellyfin/jellyfin.search.service.ts @@ -130,6 +130,27 @@ export class JellyfinSearchService { return this.transformToSearchHint(data.Items[0]); } + async getAllById( + ids: string[], + includeItemTypes: BaseItemKind[] = [BaseItemKind.Audio], + ): Promise | undefined { + const api = this.jellyfinService.getApi(); + + const searchApi = getItemsApi(api); + const { data } = await searchApi.getItems({ + ids: ids, + userId: this.jellyfinService.getUserId(), + includeItemTypes: includeItemTypes, + }); + + if (data.Items.length !== 1) { + this.logger.warn(`Failed to retrieve item via id '${ids}'`); + return null; + } + + return data.Items.map((item) => this.transformToSearchHint(item)); + } + async getRemoteImageById(id: string, limit = 20): Promise { const api = this.jellyfinService.getApi(); const remoteImageApi = getRemoteImageApi(api); diff --git a/src/clients/jellyfin/jellyfin.websocket.service.ts b/src/clients/jellyfin/jellyfin.websocket.service.ts index 21bb8d6..4d22d37 100644 --- a/src/clients/jellyfin/jellyfin.websocket.service.ts +++ b/src/clients/jellyfin/jellyfin.websocket.service.ts @@ -1,11 +1,13 @@ import { PlaystateCommand, SessionMessageType, + UserItemDataDto, } from '@jellyfin/sdk/lib/generated-client/models'; import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Cron } from '@nestjs/schedule'; +import { Session } from 'inspector'; import { WebSocket } from 'ws'; @@ -14,11 +16,9 @@ import { PlayNowCommand, SessionApiSendPlaystateCommandRequest, } from '../../types/websocket'; -import { Track } from '../../models/shared/Track'; import { JellyfinSearchService } from './jellyfin.search.service'; import { JellyfinService } from './jellyfin.service'; -import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service'; @Injectable() export class JellyfinWebSocketService implements OnModuleDestroy { @@ -28,9 +28,8 @@ export class JellyfinWebSocketService implements OnModuleDestroy { constructor( private readonly jellyfinService: JellyfinService, - private readonly jellyfinSearchService: JellyfinSearchService, private readonly playbackService: PlaybackService, - private readonly jellyfinStreamBuilderService: JellyfinStreamBuilderService, + private readonly jellyfinSearchService: JellyfinSearchService, private readonly eventEmitter: EventEmitter2, ) {} @@ -103,14 +102,29 @@ export class JellyfinWebSocketService implements OnModuleDestroy { data.hasSelection = PlayNowCommand.prototype.hasSelection; data.getSelection = PlayNowCommand.prototype.getSelection; const ids = data.getSelection(); + this.logger.log( + `Processing ${ids.length} ids received via websocket and adding them to the queue`, + ); + const searchHints = await this.jellyfinSearchService.getAllById(ids); - // TODO: Implement this again + const tracks = await Promise.all( + searchHints.map(async (x) => + ( + await x.toTracks(this.jellyfinSearchService) + ).find((x) => x !== null), + ), + ); + + this.playbackService.getPlaylistOrDefault().enqueueTracks(tracks); break; case SessionMessageType[SessionMessageType.Playstate]: const sendPlaystateCommandRequest = msg.Data as SessionApiSendPlaystateCommandRequest; this.handleSendPlaystateCommandRequest(sendPlaystateCommandRequest); break; + case SessionMessageType[SessionMessageType.UserDataChanged]: + this.logger.debug(`Received update for user session data`); + break; default: this.logger.warn( `Received a package from the socket of unknown type: ${msg.MessageType}`, @@ -124,13 +138,13 @@ export class JellyfinWebSocketService implements OnModuleDestroy { ) { switch (request.Command) { case PlaystateCommand.PlayPause: - this.eventEmitter.emitAsync('playback.control.togglePause'); + this.eventEmitter.emitAsync('internal.voice.controls.togglePause'); break; case PlaystateCommand.Pause: - this.eventEmitter.emitAsync('playback.control.pause'); + this.eventEmitter.emitAsync('internal.voice.controls.pause'); break; case PlaystateCommand.Stop: - this.eventEmitter.emitAsync('playback.control.stop'); + this.eventEmitter.emitAsync('internal.voice.controls.stop'); break; default: this.logger.warn( diff --git a/src/models/shared/Playlist.ts b/src/models/shared/Playlist.ts index 76cfbab..c16a5b5 100644 --- a/src/models/shared/Playlist.ts +++ b/src/models/shared/Playlist.ts @@ -1,4 +1,4 @@ -import { EventEmitter2 } from '@nestjs/event-emitter'; +import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { Track } from './Track'; diff --git a/src/models/shared/Track.ts b/src/models/shared/Track.ts index 1965890..0e7cb2d 100644 --- a/src/models/shared/Track.ts +++ b/src/models/shared/Track.ts @@ -39,6 +39,7 @@ export class Track { this.name = name; this.duration = duration; this.remoteImages = remoteImages; + this.playing = false; } getDuration() {