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 { DiscordClientModule } from './clients/discord/discord.module';
import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module'; import { JellyfinClientModule } from './clients/jellyfin/jellyfin.module';
import { CommandModule } from './commands/command.module'; import { CommandModule } from './commands/command.module';
import { HealthModule } from './health/health.module';
import { PlaybackModule } from './playback/playback.module'; import { PlaybackModule } from './playback/playback.module';
import { UpdatesModule } from './updates/updates.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({ @Module({
imports: [ imports: [

View File

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

View File

@ -9,12 +9,17 @@ import {
} from '@jellyfin/sdk/lib/generated-client/models'; } from '@jellyfin/sdk/lib/generated-client/models';
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api'; import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api';
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api/session-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() @Injectable()
export class JellyinPlaystateService { export class JellyinPlaystateService {
private playstateApi: PlaystateApi; private playstateApi: PlaystateApi;
private sessionApi: SessionApi; private sessionApi: SessionApi;
constructor(private readonly playbackService: PlaybackService) {}
private readonly logger = new Logger(JellyinPlaystateService.name); private readonly logger = new Logger(JellyinPlaystateService.name);
async initializePlayState(api: Api) { async initializePlayState(api: Api) {
@ -32,7 +37,6 @@ export class JellyinPlaystateService {
playableMediaTypes: [BaseItemKind[BaseItemKind.Audio]], playableMediaTypes: [BaseItemKind[BaseItemKind.Audio]],
supportsMediaControl: true, supportsMediaControl: true,
supportedCommands: [ supportedCommands: [
GeneralCommandType.SetRepeatMode,
GeneralCommandType.Play, GeneralCommandType.Play,
GeneralCommandType.PlayState, GeneralCommandType.PlayState,
], ],
@ -40,4 +44,36 @@ export class JellyinPlaystateService {
this.logger.debug('Reported playback capabilities sucessfully'); 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 { Cron } from '@nestjs/schedule';
import { JellyfinService } from './jellyfin.service'; 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 { WebSocket } from 'ws';
import { PlaybackService } from '../../playback/playback.service'; import { PlaybackService } from '../../playback/playback.service';
import { JellyfinSearchService } from './jellyfin.search.service'; import { JellyfinSearchService } from './jellyfin.search.service';
import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service'; import { JellyfinStreamBuilderService } from './jellyfin.stream.builder.service';
import { Track } from '../../types/track'; import { Track } from '../../types/track';
import { PlayNowCommand } from '../../types/websocket'; import { PlayNowCommand, SessionApiSendPlaystateCommandRequest } from '../../types/websocket';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable() @Injectable()
export class JellyfinWebSocketService implements OnModuleDestroy { export class JellyfinWebSocketService implements OnModuleDestroy {
@ -21,6 +25,7 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
private readonly jellyfinSearchService: JellyfinSearchService, private readonly jellyfinSearchService: JellyfinSearchService,
private readonly playbackService: PlaybackService, private readonly playbackService: PlaybackService,
private readonly jellyfinStreamBuilderService: JellyfinStreamBuilderService, private readonly jellyfinStreamBuilderService: JellyfinStreamBuilderService,
private readonly eventEmitter: EventEmitter2,
) {} ) {}
@Cron('*/30 * * * * *') @Cron('*/30 * * * * *')
@ -130,6 +135,11 @@ export class JellyfinWebSocketService implements OnModuleDestroy {
}); });
}); });
break; break;
case SessionMessageType[SessionMessageType.Playstate]:
const sendPlaystateCommandRequest =
msg.Data as SessionApiSendPlaystateCommandRequest;
this.handleSendPlaystateCommandRequest(sendPlaystateCommandRequest);
break;
default: default:
this.logger.warn( this.logger.warn(
`Received a package from the socket of unknown type: ${msg.MessageType}`, `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() { private bindWebSocketEvents() {
this.webSocket.on('message', this.messageHandler.bind(this)); 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 { export class PlayNowCommand {
/** /**
* A list of all items available in the parent element. * A list of all items available in the parent element.
@ -35,3 +37,24 @@ export class PlayNowCommand {
return this.ItemIds; 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;
}