stremio/packages/protocol/src/index.ts
Jos Vooges | STH 607386b94c Keep release tags in imported filenames and add OpenSubtitles NL pick.
Downloads confirm flow can select a Dutch subtitle that is written beside the movie on import.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 18:35:05 +02:00

177 lines
4.2 KiB
TypeScript

import { randomUUID } from "crypto";
import type {
CreatePlaybackSessionPayload,
LibraryEvent,
NodeHeartbeat,
} from "@media-cluster/shared-types";
import { PROTOCOL_VERSION } from "@media-cluster/shared-types";
export { PROTOCOL_VERSION };
export type ControlMessageType =
| "HELLO"
| "HELLO_ACK"
| "HEARTBEAT"
| "HEARTBEAT_ACK"
| "LIBRARY_EVENT"
| "LIBRARY_EVENT_ACK"
| "FULL_LIBRARY_SYNC"
| "FULL_LIBRARY_SYNC_ACK"
| "CREATE_PLAYBACK_SESSION"
| "CREATE_PLAYBACK_SESSION_ACK"
| "REVOKE_PLAYBACK_SESSION"
| "RESCAN"
| "CONFIG_UPDATE"
| "RESTART"
| "IMPORT_MEDIA"
| "IMPORT_MEDIA_ACK"
| "PLAYBACK_SESSION_ENDED"
| "PLAYBACK_SESSION_PROGRESS"
| "ERROR";
export interface ControlMessage<T = unknown> {
protocolVersion: number;
type: ControlMessageType;
messageId: string;
timestamp: string;
payload: T;
}
export interface HelloPayload {
nodeId: string;
apiKey: string;
softwareVersion: string;
architecture: string;
hostname: string;
publicStreamUrl: string;
/** Current scan roots on the node (seed master if empty) */
moviesPaths?: string[];
seriesPaths?: string[];
}
export interface HelloAckPayload {
accepted: boolean;
reason?: string;
heartbeatIntervalSeconds: number;
/** @deprecated prefer fullScanInterval + fullScanAt */
fullScanIntervalSeconds: number;
/** "off" or Go duration e.g. "6h", "24h" */
fullScanInterval: string;
/** Nightly catch-up HH:MM, or "off" */
fullScanAt: string;
moviesPaths?: string[];
seriesPaths?: string[];
scanRoots?: Array<{
path: string;
shelfId: string;
kind: "movie" | "series";
}>;
}
export interface HeartbeatPayload extends NodeHeartbeat {}
export interface LibraryEventPayload {
events: LibraryEvent[];
}
export interface FullLibrarySyncPayload {
nodeRevision: number;
files: LibraryEvent["file"][];
/** Present when sync is split across multiple WebSocket messages */
syncId?: string;
batchIndex?: number;
batchCount?: number;
/** True on the final batch (or when sending a single unbatched sync) */
isLast?: boolean;
}
export interface FullLibrarySyncAckPayload {
accepted: boolean;
syncId?: string;
batchIndex?: number;
}
export interface RescanPayload {
full: boolean;
}
export interface ConfigUpdatePayload {
fullScanInterval?: string;
fullScanAt?: string;
moviesPaths?: string[];
seriesPaths?: string[];
/** Preferred: path + shelf binding for Stremio catalogs */
scanRoots?: Array<{
path: string;
shelfId: string;
kind: "movie" | "series";
}>;
}
export interface RestartPayload {
reason?: string;
}
/** Master → node: move finished Download Station output into 4K library folder. */
export interface ImportMediaPayload {
jobId: string;
/** Absolute host path of the finished download folder/file */
sourcePath: string;
/** Absolute host path for destination movie folder (Title (Year)) */
destDir: string;
movieTitle: string;
movieYear?: number | null;
/** Release tags kept from torrent/file name (UHD BluRay Atmos …) */
releaseTags?: string;
/** Fallback if releaseTags empty */
qualityHint?: string;
/** Optional sidecar subtitle written next to the video */
subtitle?: {
language: string;
extension?: string;
contentBase64: string;
};
}
export interface ImportMediaAckPayload {
jobId: string;
ok: boolean;
importedPath?: string;
error?: string;
}
export interface PlaybackSessionEndedPayload {
sessionId: string;
reason: string;
}
export interface PlaybackSessionProgressPayload {
sessionId: string;
lastActivity: string;
bytesSent: number;
lastByteOffset: number;
fileSizeBytes: number;
}
export interface CreatePlaybackSessionAckPayload {
sessionId: string;
ok: boolean;
error?: string;
}
export function createMessage<T>(
type: ControlMessageType,
payload: T,
messageId?: string
): ControlMessage<T> {
return {
protocolVersion: PROTOCOL_VERSION,
type,
messageId: messageId ?? randomUUID(),
timestamp: new Date().toISOString(),
payload,
};
}
export type CreatePlaybackSessionMessage = ControlMessage<CreatePlaybackSessionPayload>;
export type RevokePlaybackSessionMessage = ControlMessage<{ sessionId: string }>;