Report playstate to Jellyfin #14 (#43)

This commit is contained in:
Manuel 2023-01-09 22:12:08 +01:00 committed by GitHub
parent 9ecce22f14
commit 1ec65c93a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 8 deletions

View File

@ -10,11 +10,9 @@ import { DiscordConfigService } from './clients/discord/discord.config.service';
import { DiscordClientModule } from './clients/discord/discord.module';
import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
import { CommandModule } from './commands/command.module';
import { HealthModule } from './health/health.module';
import { PlaybackModule } from './playback/playback.module';
import { UpdatesModule } from './updates/updates.module';
import { HealthController } from './health/health.controller';
import { HealthModule } from './health/health.module';
import { TerminusModule } from '@nestjs/terminus';
@Module({
imports: [

View File

@ -11,7 +11,7 @@ import {
} from '@discordjs/voice';
import { Injectable } from '@nestjs/common';
import { Logger } from '@nestjs/common/services';
import { OnEvent } from '@nestjs/event-emitter';
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
import { GuildMember } from 'discord.js';
import { GenericTryHandler } from '../../models/generic-try-handler';
import { PlaybackService } from '../../playback/playback.service';
@ -29,6 +29,7 @@ export class DiscordVoiceService {
private readonly discordMessageService: DiscordMessageService,
private readonly playbackService: PlaybackService,
private readonly jellyfinWebSocketService: JellyfinWebSocketService,
private readonly eventEmitter: EventEmitter2,
) {}
@OnEvent('playback.newTrack')
@ -95,15 +96,20 @@ export class DiscordVoiceService {
/**
* Pauses the current audio player
*/
@OnEvent('playback.control.pause')
pause() {
this.createAndReturnOrGetAudioPlayer().pause();
this.eventEmitter.emit('playback.state.pause', true);
}
/**
* Stops the audio player
*/
@OnEvent('playback.control.stop')
stop(force: boolean): boolean {
return this.createAndReturnOrGetAudioPlayer().stop(force);
const stopped = this.createAndReturnOrGetAudioPlayer().stop(force);
this.eventEmitter.emit('playback.state.stop');
return stopped;
}
/**
@ -111,6 +117,7 @@ export class DiscordVoiceService {
*/
unpause() {
this.createAndReturnOrGetAudioPlayer().unpause();
this.eventEmitter.emit('playback.state.pause', false);
}
/**
@ -136,6 +143,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('playback.control.togglePause')
togglePaused(): boolean {
if (this.isPaused()) {
this.unpause();

View File

@ -9,12 +9,17 @@ import {
} from '@jellyfin/sdk/lib/generated-client/models';
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-api';
import { OnEvent } from '@nestjs/event-emitter';
import { Track } from '../../types/track';
import { PlaybackService } from '../../playback/playback.service';
@Injectable()
export class JellyinPlaystateService {
private playstateApi: PlaystateApi;
private sessionApi: SessionApi;
constructor(private readonly playbackService: PlaybackService) {}
private readonly logger = new Logger(JellyinPlaystateService.name);
async initializePlayState(api: Api) {
@ -32,7 +37,6 @@ export class JellyinPlaystateService {
playableMediaTypes: [BaseItemKind[BaseItemKind.Audio]],
supportsMediaControl: true,
supportedCommands: [
GeneralCommandType.SetRepeatMode,
GeneralCommandType.Play,
GeneralCommandType.PlayState,
],
@ -40,4 +44,36 @@ export class JellyinPlaystateService {
this.logger.debug('Reported playback capabilities sucessfully');
}
@OnEvent('playback.newTrack')
private async onPlaybackNewTrack(track: Track) {
await this.playstateApi.reportPlaybackStart({
playbackStartInfo: {
ItemId: track.jellyfinId,
},
});
}
@OnEvent('playback.state.pause')
private async onPlaybackPaused(isPaused: boolean) {
const activeTrack = this.playbackService.getActiveTrack();
await this.playstateApi.reportPlaybackProgress({
playbackProgressInfo: {
ItemId: activeTrack.track.jellyfinId,
IsPaused: isPaused,
},
});
}
@OnEvent('playback.state.stop')
private async onPlaybackStopped() {
const activeTrack = this.playbackService.getActiveTrack();
await this.playstateApi.reportPlaybackStopped({
playbackStopInfo: {
ItemId: activeTrack.track.jellyfinId,
},
});
}
}

View File

@ -2,13 +2,17 @@ import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { JellyfinService } from './jellyfin.service';
import { SessionMessageType } from '@jellyfin/sdk/lib/generated-client/models';
import {
PlaystateCommand,
SessionMessageType,
} from '@jellyfin/sdk/lib/generated-client/models';
import { WebSocket } from 'ws';
import { PlaybackService } from '../../playback/playback.service';
import { JellyfinSearchService } from './jellyfin.search.service';
import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service';
import { Track } from '../../types/track';
import { PlayNowCommand } from '../../types/websocket';
import { PlayNowCommand, SessionApiSendPlaystateCommandRequest } from '../../types/websocket';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class JellyfinWebSocketService implements OnModuleDestroy {
@ -21,6 +25,7 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
private readonly jellyfinSearchService: JellyfinSearchService,
private readonly playbackService: PlaybackService,
private readonly jellyfinStreamBuilderService: JellyfinStreamBuilderService,
private readonly eventEmitter: EventEmitter2,
) {}
@Cron('*/30 * * * * *')
@ -130,6 +135,11 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
});
});
break;
case SessionMessageType[SessionMessageType.Playstate]:
const sendPlaystateCommandRequest =
msg.Data as SessionApiSendPlaystateCommandRequest;
this.handleSendPlaystateCommandRequest(sendPlaystateCommandRequest);
break;
default:
this.logger.warn(
`Received a package from the socket of unknown type: ${msg.MessageType}`,
@ -138,6 +148,27 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
}
}
private async handleSendPlaystateCommandRequest(
request: SessionApiSendPlaystateCommandRequest,
) {
switch (request.Command) {
case PlaystateCommand.PlayPause:
this.eventEmitter.emitAsync('playback.control.togglePause');
break;
case PlaystateCommand.Pause:
this.eventEmitter.emitAsync('playback.control.pause');
break;
case PlaystateCommand.Stop:
this.eventEmitter.emitAsync('playback.control.stop');
break;
default:
this.logger.warn(
`Unable to process incoming playstate command request: ${request.Command}`,
);
break;
}
}
private bindWebSocketEvents() {
this.webSocket.on('message', this.messageHandler.bind(this));
}

View File

@ -1,3 +1,5 @@
import { PlaystateCommand } from '@jellyfin/sdk/lib/generated-client/models';
export class PlayNowCommand {
/**
* A list of all items available in the parent element.
@ -35,3 +37,24 @@ export class PlayNowCommand {
return this.ItemIds;
}
}
export interface SessionApiSendPlaystateCommandRequest {
/**
* The MediaBrowser.Model.Session.PlaystateCommand.
* @type {PlaystateCommand}
* @memberof SessionApiSendPlaystateCommand
*/
readonly Command: PlaystateCommand;
/**
* The optional position ticks.
* @type {number}
* @memberof SessionApiSendPlaystateCommand
*/
readonly SeekPositionTicks?: number;
/**
* The optional controlling user id.
* @type {string}
* @memberof SessionApiSendPlaystateCommand
*/
readonly ControllingUserId?: string;
}