Files
SubMiner/src/main/runtime/jellyfin-remote-commands.test.ts

172 lines
5.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
createHandleJellyfinRemoteGeneralCommand,
createHandleJellyfinRemotePlay,
createHandleJellyfinRemotePlaystate,
getConfiguredJellyfinSession,
type ActiveJellyfinRemotePlaybackState,
} from './jellyfin-remote-commands';
test('getConfiguredJellyfinSession returns null for incomplete config', () => {
assert.equal(
getConfiguredJellyfinSession({
serverUrl: '',
accessToken: 'token',
userId: 'user',
username: 'name',
}),
null,
);
});
test('createHandleJellyfinRemotePlay forwards parsed payload to play runtime', async () => {
const calls: Array<{ itemId: string; audio?: number; subtitle?: number; start?: number }> = [];
const handlePlay = createHandleJellyfinRemotePlay({
getConfiguredSession: () => ({
serverUrl: 'https://jellyfin.local',
accessToken: 'token',
userId: 'user',
username: 'name',
}),
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
getJellyfinConfig: () => ({ enabled: true }),
playJellyfinItem: async (params) => {
calls.push({
itemId: params.itemId,
audio: params.audioStreamIndex,
subtitle: params.subtitleStreamIndex,
start: params.startTimeTicksOverride,
});
},
logWarn: () => {},
});
await handlePlay({
ItemIds: ['item-1'],
AudioStreamIndex: 3,
SubtitleStreamIndex: 7,
StartPositionTicks: 1000,
});
assert.deepEqual(calls, [{ itemId: 'item-1', audio: 3, subtitle: 7, start: 1000 }]);
});
test('createHandleJellyfinRemotePlay parses string StartPositionTicks', async () => {
const calls: Array<{ itemId: string; start?: number }> = [];
const handlePlay = createHandleJellyfinRemotePlay({
getConfiguredSession: () => ({
serverUrl: 'https://jellyfin.local',
accessToken: 'token',
userId: 'user',
username: 'name',
}),
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
getJellyfinConfig: () => ({ enabled: true }),
playJellyfinItem: async (params) => {
calls.push({
itemId: params.itemId,
start: params.startTimeTicksOverride,
});
},
logWarn: () => {},
});
await handlePlay({
ItemIds: ['item-2'],
StartPositionTicks: '12345',
});
assert.deepEqual(calls, [{ itemId: 'item-2', start: 12345 }]);
});
test('createHandleJellyfinRemotePlay logs and skips payload without item id', async () => {
const warnings: string[] = [];
const handlePlay = createHandleJellyfinRemotePlay({
getConfiguredSession: () => ({
serverUrl: 'https://jellyfin.local',
accessToken: 'token',
userId: 'user',
username: 'name',
}),
getClientInfo: () => ({ clientName: 'SubMiner', clientVersion: '1.0', deviceId: 'abc' }),
getJellyfinConfig: () => ({}),
playJellyfinItem: async () => {
throw new Error('should not be called');
},
logWarn: (message) => warnings.push(message),
});
await handlePlay({ ItemIds: [] });
assert.deepEqual(warnings, ['Ignoring Jellyfin remote Play event without ItemIds.']);
});
test('createHandleJellyfinRemotePlaystate dispatches pause/seek/stop flows', async () => {
const mpvClient = {};
const commands: Array<(string | number)[]> = [];
const calls: string[] = [];
const handlePlaystate = createHandleJellyfinRemotePlaystate({
getMpvClient: () => mpvClient,
sendMpvCommand: (_client, command) => commands.push(command),
reportJellyfinRemoteProgress: async (force) => {
calls.push(`progress:${force}`);
},
reportJellyfinRemoteStopped: async () => {
calls.push('stopped');
},
jellyfinTicksToSeconds: (ticks) => ticks / 10,
});
await handlePlaystate({ Command: 'Pause' });
await handlePlaystate({ Command: 'Seek', SeekPositionTicks: 50 });
await handlePlaystate({ Command: 'Stop' });
assert.deepEqual(commands, [
['set_property', 'pause', 'yes'],
['seek', 5, 'absolute+exact'],
['stop'],
]);
assert.deepEqual(calls, ['progress:true', 'progress:true', 'stopped']);
});
test('createHandleJellyfinRemoteGeneralCommand mutates active playback indices', async () => {
const mpvClient = {};
const commands: Array<(string | number)[]> = [];
const playback: ActiveJellyfinRemotePlaybackState = {
itemId: 'item-1',
playMethod: 'DirectPlay',
audioStreamIndex: null,
subtitleStreamIndex: null,
};
const calls: string[] = [];
const handleGeneral = createHandleJellyfinRemoteGeneralCommand({
getMpvClient: () => mpvClient,
sendMpvCommand: (_client, command) => commands.push(command),
getActivePlayback: () => playback,
reportJellyfinRemoteProgress: async (force) => {
calls.push(`progress:${force}`);
},
logDebug: (message) => {
calls.push(`debug:${message}`);
},
});
await handleGeneral({ Name: 'SetAudioStreamIndex', Arguments: { Index: 2 } });
await handleGeneral({ Name: 'SetSubtitleStreamIndex', Arguments: { Index: -1 } });
await handleGeneral({ Name: 'UnsupportedCommand', Arguments: {} });
assert.deepEqual(commands, [
['set_property', 'aid', 2],
['set_property', 'sid', 'no'],
]);
assert.equal(playback.audioStreamIndex, 2);
assert.equal(playback.subtitleStreamIndex, null);
assert.ok(calls.includes('progress:true'));
assert.ok(
calls.some((entry) =>
entry.includes('Ignoring unsupported Jellyfin GeneralCommand: UnsupportedCommand'),
),
);
});