Files
streamyfin/providers/SyncPlay/Controller.ts
2026-06-05 20:00:55 +10:00

166 lines
5.0 KiB
TypeScript

/**
* SyncPlay Controller — public playback API exposed to consumers.
*
* Methods are fire-and-forget by design: SyncPlay HTTP responses don't
* carry useful info (the real state arrives via WebSocket broadcast).
* Wrap calls in try/catch so transient network errors don't reach the UI.
*/
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { getSyncPlayApi } from "@jellyfin/sdk/lib/utils/api";
import type { SyncPlayManager } from "./Manager";
import {
getItemsForPlayback,
type TranslateOptions,
translateItemsForPlayback,
} from "./transport/queueTranslation";
export interface PlayOptions extends TranslateOptions {
items?: BaseItemDto[];
ids?: string[];
startIndex?: number;
startPositionTicks?: number;
}
export class Controller {
private manager!: SyncPlayManager;
init(manager: SyncPlayManager): void {
this.manager = manager;
}
/** Toggle play/pause for the whole group. */
playPause(): void {
if (this.manager.isPlaying()) {
this.pause();
} else {
this.unpause();
}
}
/** Resume the group's playback. */
unpause(): void {
this.manager.markPendingPlaybackCommand("Unpause");
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayUnpause();
} catch (error) {
console.error("SyncPlay Controller.unpause failed", error);
}
}
/** Pause the group's playback. */
pause(): void {
this.manager.markPendingPlaybackCommand("Pause");
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayPause();
} catch (error) {
console.error("SyncPlay Controller.pause failed", error);
}
// Pause locally too so the user sees instant feedback.
this.manager.getPlayerWrapper().localPause();
}
/** Seek the group's playback. `positionTicks` is in ticks (1ms = 10000 ticks). */
seek(positionTicks: number): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlaySeek({
seekRequestDto: { PositionTicks: positionTicks },
});
} catch (error) {
console.error("SyncPlay Controller.seek failed", error);
}
}
/**
* Start playback in the group. Expands containers (Series, Season,
* BoxSet, Playlist, single Episode w/ autoplay) into the real
* playable queue before broadcasting.
*
* Resolves once the SetNewQueue request completes; the server then
* broadcasts a PlayQueue update and Play command to every member.
*/
async play(options: PlayOptions): Promise<void> {
const api = this.manager.getApiClient();
const sendPlayRequest = async (items: BaseItemDto[]) => {
const queue = items
.map((item) => item.Id)
.filter((id): id is string => typeof id === "string");
await getSyncPlayApi(api).syncPlaySetNewQueue({
playRequestDto: {
PlayingQueue: queue,
PlayingItemPosition: options.startIndex ?? 0,
StartPositionTicks: options.startPositionTicks ?? 0,
},
});
};
try {
const sourceItems = options.items
? options.items
: await getItemsForPlayback(api, options.ids ?? []);
const items = await translateItemsForPlayback(api, sourceItems, options);
await sendPlayRequest(items);
} catch (error) {
console.error("SyncPlay Controller.play failed", error);
throw error;
}
}
/** Stop the group's playback. */
stop(): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayStop();
} catch (error) {
console.error("SyncPlay Controller.stop failed", error);
}
}
/** Jump to the next item in the group's queue. */
nextItem(): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayNextItem({
nextItemRequestDto: {
PlaylistItemId: this.manager
.getQueueCore()
.getCurrentPlaylistItemId(),
} as unknown as Parameters<
ReturnType<typeof getSyncPlayApi>["syncPlayNextItem"]
>[0]["nextItemRequestDto"],
});
} catch (error) {
console.error("SyncPlay Controller.nextItem failed", error);
}
}
/** Jump to the previous item in the group's queue. */
previousItem(): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlayPreviousItem({
previousItemRequestDto: {
PlaylistItemId: this.manager
.getQueueCore()
.getCurrentPlaylistItemId(),
} as unknown as Parameters<
ReturnType<typeof getSyncPlayApi>["syncPlayPreviousItem"]
>[0]["previousItemRequestDto"],
});
} catch (error) {
console.error("SyncPlay Controller.previousItem failed", error);
}
}
/** Jump to a specific item in the queue by playlist item id. */
setCurrentPlaylistItem(playlistItemId: string): void {
try {
getSyncPlayApi(this.manager.getApiClient()).syncPlaySetPlaylistItem({
setPlaylistItemRequestDto: { PlaylistItemId: playlistItemId },
});
} catch (error) {
console.error("SyncPlay Controller.setCurrentPlaylistItem failed", error);
}
}
}
export default Controller;