Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
34
packages/connector-mock-video/src/contract.test.ts
Normal file
34
packages/connector-mock-video/src/contract.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { createMockVideoConnector, createMockVideoState } from "./index.js";
|
||||
import { connectorContractTests, videoConnectorContractTests } from "@tactical-media-broadcast-hub/connector-sdk/testing";
|
||||
import type { ConnectorConnection } from "@tactical-media-broadcast-hub/core";
|
||||
|
||||
const connection: ConnectorConnection = {
|
||||
id: "conn-video",
|
||||
workspaceId: "ws-1",
|
||||
provider: "mock-video",
|
||||
channelId: "ch-1",
|
||||
credentialId: { kind: "credential-ref", id: "cred-1" },
|
||||
fields: {},
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
connectorContractTests(createMockVideoConnector(createMockVideoState()), { connection });
|
||||
videoConnectorContractTests(createMockVideoConnector(createMockVideoState()), { connection });
|
||||
|
||||
describe("mock-video processing machine", () => {
|
||||
it("reaches ready after enough status polls", async () => {
|
||||
const connector = createMockVideoConnector(createMockVideoState());
|
||||
const upload = await connector.uploadVideo?.(connection, {
|
||||
videoAssetId: "asset-1",
|
||||
visibility: "provider_default",
|
||||
formatIntent: "long_form",
|
||||
providerMetadata: {},
|
||||
});
|
||||
expect(upload?.remoteId).toBeTruthy();
|
||||
let status = await connector.getVideoStatus?.(connection, upload!.remoteId);
|
||||
expect(status?.state).toBe("processing");
|
||||
status = await connector.getVideoStatus?.(connection, upload!.remoteId);
|
||||
expect(status?.state).toBe("ready");
|
||||
});
|
||||
});
|
||||
116
packages/connector-mock-video/src/index.ts
Normal file
116
packages/connector-mock-video/src/index.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* Deterministic video mock: upload → processing → ready state machine,
|
||||
* library pagination, thumbnails/captions capability.
|
||||
*/
|
||||
import type {
|
||||
ConnectorCapabilities,
|
||||
ConnectorConnection,
|
||||
NormalizedIdentity,
|
||||
NormalizedVideoPublication,
|
||||
Page,
|
||||
VideoProcessingStatus,
|
||||
} from "@tactical-media-broadcast-hub/core";
|
||||
import {
|
||||
assertValidManifest,
|
||||
pageOf,
|
||||
type ConnectorManifest,
|
||||
type PlatformConnector,
|
||||
} from "@tactical-media-broadcast-hub/connector-sdk";
|
||||
|
||||
const CAPABILITIES: ConnectorCapabilities = {
|
||||
publishVideo: true,
|
||||
longFormVideo: true,
|
||||
videoThumbnails: true,
|
||||
videoCaptions: true,
|
||||
playlists: true,
|
||||
videoAnalytics: true,
|
||||
};
|
||||
|
||||
let seq = 0;
|
||||
const nextId = (prefix: string): string => `${prefix}-${(seq += 1)}`;
|
||||
|
||||
interface MockVideo {
|
||||
remoteId: string;
|
||||
title?: string;
|
||||
createdAt: Date;
|
||||
processingAdvance: number;
|
||||
}
|
||||
|
||||
export interface MockVideoState {
|
||||
videos: MockVideo[];
|
||||
}
|
||||
|
||||
export function createMockVideoState(): MockVideoState {
|
||||
return { videos: [] };
|
||||
}
|
||||
|
||||
export function createMockVideoConnector(
|
||||
state: MockVideoState = createMockVideoState(),
|
||||
): PlatformConnector {
|
||||
const manifest: ConnectorManifest = {
|
||||
id: "mock-video",
|
||||
displayName: "Mock Video",
|
||||
capabilities: CAPABILITIES,
|
||||
connectionFields: [{ name: "apiToken", label: "API token", required: true, secret: true }],
|
||||
};
|
||||
|
||||
return {
|
||||
manifest: () => {
|
||||
assertValidManifest(manifest);
|
||||
return manifest;
|
||||
},
|
||||
|
||||
async authorize() {
|
||||
throw new Error("mock-video uses token auth in tests only");
|
||||
},
|
||||
|
||||
async getIdentity(connection: ConnectorConnection): Promise<NormalizedIdentity> {
|
||||
return {
|
||||
provider: manifest.id,
|
||||
remoteId: connection.channelId,
|
||||
handle: "mock@video",
|
||||
displayName: "Mock Video Channel",
|
||||
};
|
||||
},
|
||||
|
||||
async uploadVideo(
|
||||
_connection: ConnectorConnection,
|
||||
video: NormalizedVideoPublication,
|
||||
): Promise<{ remoteId: string }> {
|
||||
const remoteId = nextId("vid");
|
||||
state.videos.push({
|
||||
remoteId,
|
||||
title: video.title,
|
||||
createdAt: new Date(),
|
||||
processingAdvance: 0,
|
||||
});
|
||||
return { remoteId };
|
||||
},
|
||||
|
||||
async getVideoStatus(
|
||||
_connection: ConnectorConnection,
|
||||
remoteId: string,
|
||||
): Promise<VideoProcessingStatus> {
|
||||
const video = state.videos.find((v) => v.remoteId === remoteId);
|
||||
if (video === undefined) {
|
||||
return { remoteId, state: "failed", error: "unknown remote id" };
|
||||
}
|
||||
video.processingAdvance = Math.min(100, video.processingAdvance + 50);
|
||||
const state_: VideoProcessingStatus["state"] =
|
||||
video.processingAdvance >= 100 ? "ready" : "processing";
|
||||
return { remoteId, state: state_, progressPercent: video.processingAdvance };
|
||||
},
|
||||
|
||||
async getVideos(
|
||||
_connection: ConnectorConnection,
|
||||
request: { cursor?: string; limit: number },
|
||||
): Promise<Page<{ remoteId: string; title?: string; createdAt?: Date }>> {
|
||||
const start = request.cursor ? Number(request.cursor) : 0;
|
||||
const items = state.videos
|
||||
.slice(start, start + request.limit)
|
||||
.map((v) => ({ remoteId: v.remoteId, title: v.title, createdAt: v.createdAt }));
|
||||
const next = start + request.limit;
|
||||
return next < state.videos.length ? pageOf(items, String(next)) : pageOf(items);
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue