Pass embedded track language/title through play ACK into clients so Android/iOS can label tracks instead of generic Ondertitel N when ExoPlayer/VLC miss tags.
263 lines
6.1 KiB
TypeScript
263 lines
6.1 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"
|
|
| "UPGRADE"
|
|
| "UPGRADE_ACK"
|
|
| "IMPORT_MEDIA"
|
|
| "IMPORT_MEDIA_ACK"
|
|
| "WRITE_SUBTITLE"
|
|
| "WRITE_SUBTITLE_ACK"
|
|
| "FIND_SIDECAR_SUBTITLES"
|
|
| "FIND_SIDECAR_SUBTITLES_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;
|
|
}
|
|
|
|
export interface UpgradePayload {
|
|
upgradeId: string;
|
|
url: string;
|
|
}
|
|
|
|
export interface UpgradeAckPayload {
|
|
upgradeId: string;
|
|
ok: boolean;
|
|
version?: string;
|
|
error?: string;
|
|
}
|
|
|
|
/** Master → node: move finished Download Station output into library folder. */
|
|
export interface ImportMediaPayload {
|
|
jobId: string;
|
|
/** Absolute host path of the finished download folder/file */
|
|
sourcePath: string;
|
|
/**
|
|
* Movie: destination movie folder (Title (Year)).
|
|
* Series: destination show folder (Show (Year)); Season XX is created underneath.
|
|
*/
|
|
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;
|
|
/** Default movie when omitted (backwards compatible). */
|
|
kind?: "movie" | "series";
|
|
seasonNumber?: number;
|
|
/** Expected episodes (required for series). */
|
|
episodes?: Array<{
|
|
episodeNumber: number;
|
|
episodeTitle?: string;
|
|
}>;
|
|
/** Optional sidecar subtitle written next to the video (single-episode series or movie). */
|
|
subtitle?: {
|
|
language: string;
|
|
extension?: string;
|
|
contentBase64: string;
|
|
};
|
|
}
|
|
|
|
export interface ImportMediaEpisodeResult {
|
|
episodeNumber: number;
|
|
importedPath: string;
|
|
subtitlePath?: string;
|
|
}
|
|
|
|
export interface ImportMediaAckPayload {
|
|
jobId: string;
|
|
ok: boolean;
|
|
importedPath?: string;
|
|
/** Absolute path of written sidecar, if any (movie / single episode) */
|
|
subtitlePath?: string;
|
|
/** Per-episode results for series imports */
|
|
episodes?: ImportMediaEpisodeResult[];
|
|
error?: string;
|
|
}
|
|
|
|
/** Master → node: write sidecar next to an already-imported video */
|
|
export interface WriteSubtitlePayload {
|
|
jobId: string;
|
|
videoPath: string;
|
|
subtitle: {
|
|
language: string;
|
|
extension?: string;
|
|
contentBase64: string;
|
|
};
|
|
}
|
|
|
|
export interface WriteSubtitleAckPayload {
|
|
jobId: string;
|
|
ok: boolean;
|
|
subtitlePath?: string;
|
|
error?: string;
|
|
}
|
|
|
|
/** Master → node: list/read sidecar subtitle files next to a video. */
|
|
export interface FindSidecarSubtitlesPayload {
|
|
requestId: string;
|
|
localFileId: string;
|
|
}
|
|
|
|
export interface SidecarSubtitleFile {
|
|
fileName: string;
|
|
language: string;
|
|
format: string;
|
|
contentBase64: string;
|
|
}
|
|
|
|
export interface FindSidecarSubtitlesAckPayload {
|
|
requestId: string;
|
|
ok: boolean;
|
|
subtitles?: SidecarSubtitleFile[];
|
|
error?: string;
|
|
}
|
|
|
|
export interface PlaybackSessionEndedPayload {
|
|
sessionId: string;
|
|
reason: string;
|
|
}
|
|
|
|
export interface PlaybackSessionProgressPayload {
|
|
sessionId: string;
|
|
lastActivity: string;
|
|
bytesSent: number;
|
|
lastByteOffset: number;
|
|
fileSizeBytes: number;
|
|
}
|
|
|
|
export interface MediaTrackInfo {
|
|
type: string;
|
|
number?: number;
|
|
language?: string;
|
|
title?: string;
|
|
codec?: string;
|
|
}
|
|
|
|
export interface CreatePlaybackSessionAckPayload {
|
|
sessionId: string;
|
|
ok: boolean;
|
|
error?: string;
|
|
tracks?: MediaTrackInfo[];
|
|
}
|
|
|
|
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 }>;
|