Update and improve things
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import { defineConfig } from "eslint/config";
|
||||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import {defineConfig} from 'eslint/config';
|
||||
|
||||
export default defineConfig([
|
||||
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: {...globals.browser, ...globals.node} } },
|
||||
tseslint.configs.recommended,
|
||||
{files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], plugins: {js}, extends: ['js/recommended'], languageOptions: {globals: {...globals.browser, ...globals.node}}},
|
||||
tseslint.configs.recommended
|
||||
]);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import PCastApi from "../../src/index";
|
||||
import PCastApi from '../../src/index';
|
||||
|
||||
const pacstUri = "https://pcast-stg.phenixrts.com"
|
||||
const pcastUri = 'https://pcast-stg.phenixrts.com';
|
||||
const application = {
|
||||
id: "phenixrts.com-alex.zinn",
|
||||
secret: "AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg"
|
||||
}
|
||||
const pcastApi = new PCastApi(pacstUri, application);
|
||||
id: 'phenixrts.com-alex.zinn',
|
||||
secret: 'AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg'
|
||||
};
|
||||
const pcastApi = PCastApi.create(pcastUri, application);
|
||||
|
||||
console.log('pcastApi [%o]', pcastApi);
|
||||
console.log();
|
||||
console.log('ChannelsApi [%o]', pcastApi.channels);
|
||||
const channelsList = await pcastApi.channels.list();
|
||||
|
||||
// const start = hrtime.bigint();
|
||||
@@ -19,7 +22,6 @@ const channelsList = await pcastApi.channels.list();
|
||||
// console.log(publishingReportCsv);
|
||||
// console.log(`Time taken: ${Number(endPublishing - start) / 1_000_000_000} seconds`);
|
||||
|
||||
|
||||
// const viewingReportCsv = await pcastApi.reporting.generateReport(ReportKind.Viewing, {
|
||||
// kind: ViewingReportKind.HLS,
|
||||
// start: moment().subtract(1, 'day').toISOString(),
|
||||
@@ -34,4 +36,4 @@ const channelsList = await pcastApi.channels.list();
|
||||
|
||||
const channelMembers = await pcastApi.channels.getMembers(channelsList[1].channelId);
|
||||
console.log('[%o]', channelMembers);
|
||||
console.log('[%o]', channelMembers.streams);
|
||||
console.log('[%o]', channelMembers.streams);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "@techniker-me/pcast-api",
|
||||
"version": "2025.0.10",
|
||||
"version": "2025.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"ci-build": "bun run build",
|
||||
"format": "prettier --write ./",
|
||||
"test": "bun test",
|
||||
"test:watch": "bun test --watch",
|
||||
"test:coverage": "bun test --coverage",
|
||||
"prelint": "bun install",
|
||||
"preformat": "bun install",
|
||||
"format": "prettier --write ./",
|
||||
"prelint": "bun format",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"build:node": "bun build src/index.ts --outdir dist/node --target node --format esm --production",
|
||||
@@ -21,7 +22,6 @@
|
||||
"build:dev": "bun run build:node:dev && bun run build:browser:dev && bun run build:types:dev",
|
||||
"prebuild": "bun run clean",
|
||||
"build": "bun run build:node && bun run build:browser && bun run build:types",
|
||||
"postbuild": "bash scripts/prepare-package-json.sh",
|
||||
"postclean": "bun run lint",
|
||||
"clean": "rm -rf dist",
|
||||
"prepublish": "bash scripts/pre-publish.sh"
|
||||
|
||||
@@ -4,10 +4,7 @@ set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
echo "Preparing package.json for distributions"
|
||||
echo "Preparing package.json for distribution"
|
||||
bash scripts/prepare-package-json.sh
|
||||
|
||||
echo "Copying .npmrc to dist/"
|
||||
cp .npmrc dist/
|
||||
|
||||
echo "Pre-publish done"
|
||||
@@ -1,34 +1,25 @@
|
||||
import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests, Reporting} from './pcast';
|
||||
|
||||
export class PCastApi {
|
||||
private readonly _pcastUri: string;
|
||||
private readonly _applicationCredentials: ApplicationCredentials;
|
||||
private readonly _pcastHttpRequests: PCastHttpRequests;
|
||||
private readonly _channels: Channels;
|
||||
private readonly _streams: Streams;
|
||||
private readonly _reporting: Reporting;
|
||||
|
||||
private constructor(pcastUri: string, applicationCredentials: ApplicationCredentials, channels: Channels) {
|
||||
const normalized = pcastUri.replace(/\/+$/, '');
|
||||
this._pcastUri = normalized.endsWith('/pcast') ? normalized : `${normalized}/pcast`;
|
||||
|
||||
this._applicationCredentials = applicationCredentials;
|
||||
this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials);
|
||||
private constructor(channels: Channels, streams: Streams, reporting: Reporting) {
|
||||
this._channels = channels;
|
||||
this._streams = new Streams(this._pcastHttpRequests);
|
||||
this._reporting = new Reporting(this._pcastHttpRequests);
|
||||
this._streams = streams;
|
||||
this._reporting = reporting;
|
||||
}
|
||||
|
||||
public static async create(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise<PCastApi> {
|
||||
public static create(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise<PCastApi> {
|
||||
const pcastHttpRequests = new PCastHttpRequests(pcastUri.replace(/\/+$/, '').endsWith('/pcast') ? pcastUri : `${pcastUri}/pcast`, applicationCredentials);
|
||||
const channels = await Channels.create(pcastHttpRequests);
|
||||
return new PCastApi(pcastUri, applicationCredentials, channels);
|
||||
const channels = new Channels(pcastHttpRequests);
|
||||
const streams = new Streams(pcastHttpRequests);
|
||||
const reporting = new Reporting(pcastHttpRequests);
|
||||
|
||||
return new PCastApi(channels, streams, reporting);
|
||||
}
|
||||
|
||||
get pcastUri(): string {
|
||||
return this._pcastUri;
|
||||
}
|
||||
|
||||
get channels(): Channels {
|
||||
return this._channels;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export class HttpRequests {
|
||||
|
||||
try {
|
||||
const requestPath = `${this._baseUri}${path}`;
|
||||
|
||||
|
||||
const response = await fetch(requestPath, options);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -20,16 +20,16 @@ export type Member = {
|
||||
screenName: string;
|
||||
role: string;
|
||||
streams: [
|
||||
{
|
||||
type: string;
|
||||
uri: string;
|
||||
audioState: string;
|
||||
videoState: string;
|
||||
}
|
||||
],
|
||||
{
|
||||
type: string;
|
||||
uri: string;
|
||||
audioState: string;
|
||||
videoState: string;
|
||||
}
|
||||
];
|
||||
state: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
};
|
||||
|
||||
type GetChannelParams = {
|
||||
alias?: string;
|
||||
@@ -49,31 +49,26 @@ export class ChannelError extends Error {
|
||||
export class Channels {
|
||||
private readonly _httpRequests: PCastHttpRequests;
|
||||
private readonly _channelsByAlias: Map<ChannelAlias, Channel> = new Map();
|
||||
private _initialized: Promise<void> | boolean = false;
|
||||
|
||||
private constructor(pcastHttpRequests: PCastHttpRequests) {
|
||||
this._httpRequests = pcastHttpRequests;
|
||||
}
|
||||
|
||||
public static async create(pcastHttpRequests: PCastHttpRequests): Promise<Channels> {
|
||||
const instance = new Channels(pcastHttpRequests);
|
||||
await instance.initialize();
|
||||
return instance;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public async createChannel(name: string, description: string, channelOptions: string[] = []): Promise<Channel> {
|
||||
// Input validation
|
||||
if (!name || name.trim().length === 0) {
|
||||
throw new ChannelError('Channel name cannot be empty', 'INVALID_NAME');
|
||||
}
|
||||
|
||||
if (!description || description.trim().length === 0) {
|
||||
throw new ChannelError('Channel description cannot be empty', 'INVALID_DESCRIPTION');
|
||||
}
|
||||
|
||||
if (!Array.isArray(channelOptions)) {
|
||||
throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS');
|
||||
}
|
||||
|
||||
const createChannelRequestBody = {
|
||||
const createChannel = {
|
||||
channel: {
|
||||
name: name.trim(),
|
||||
alias: name.trim(),
|
||||
@@ -82,15 +77,14 @@ export class Channels {
|
||||
}
|
||||
};
|
||||
|
||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.PUT, '/channel', {
|
||||
body: JSON.stringify(createChannelRequestBody)
|
||||
});
|
||||
const route = '/channel';
|
||||
const requestOptions = {body: JSON.stringify(createChannel)};
|
||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.PUT, route, requestOptions);
|
||||
|
||||
if (!response.channel) {
|
||||
throw new ChannelError('Invalid response format - missing channel data', 'INVALID_RESPONSE');
|
||||
}
|
||||
|
||||
// Update cache with new channel
|
||||
this._channelsByAlias.set(response.channel.alias, response.channel);
|
||||
|
||||
return response.channel;
|
||||
@@ -113,7 +107,8 @@ export class Channels {
|
||||
}
|
||||
|
||||
public async refreshCache(): Promise<void> {
|
||||
await this.list(); // This clears and repopulates the cache
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ignored = await this.list();
|
||||
}
|
||||
|
||||
public async getChannelInfoByAlias(alias: string): Promise<Channel | undefined> {
|
||||
@@ -125,41 +120,22 @@ export class Channels {
|
||||
throw new ChannelError('Either alias or channelId must be provided', 'MISSING_PARAMETER');
|
||||
}
|
||||
|
||||
if (this._initialized === false) {
|
||||
await this.initialize();
|
||||
} else if (this._initialized instanceof Promise) {
|
||||
await this._initialized;
|
||||
if (alias && this._channelsByAlias.has(alias)) {
|
||||
return this._channelsByAlias.get(alias);
|
||||
}
|
||||
|
||||
if (alias) {
|
||||
// Check cache first
|
||||
if (this._channelsByAlias.has(alias)) {
|
||||
return this._channelsByAlias.get(alias);
|
||||
}
|
||||
const channelList = await this.list();
|
||||
|
||||
// Instead of fetching full list, try to fetch single channel if possible
|
||||
// Assuming API has /channel/{alias}, but based on code, it doesn't; fallback to list
|
||||
const channelList = await this.list();
|
||||
|
||||
// Update cache
|
||||
return channelList.find(channel => channel.alias === alias);
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
// Similar fallback
|
||||
const channelList = await this.list();
|
||||
|
||||
return channelList.find(channel => channel.channelId === channelId);
|
||||
}
|
||||
return alias ? channelList.find(channel => channel.alias === alias) : channelList.find(channel => channel.channelId === channelId);
|
||||
}
|
||||
|
||||
public async getChannelPublisherCount(channelId: string): Promise<number> {
|
||||
public async getPublisherCount(channelId: string): Promise<number> {
|
||||
const response = await this._httpRequests.request<string>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/publishers/count`);
|
||||
|
||||
return parseInt(response, 10);
|
||||
}
|
||||
|
||||
public async getChannelMembers(channelId: string): Promise<Member[]> {
|
||||
public async getMembers(channelId: string): Promise<Member[]> {
|
||||
if (!channelId || channelId.trim().length === 0) {
|
||||
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
|
||||
}
|
||||
@@ -173,29 +149,32 @@ export class Channels {
|
||||
return response.members;
|
||||
}
|
||||
|
||||
public async getChannelMembersByAlias(alias: string): Promise<Member[]> {
|
||||
public async getMembersByChannelAlias(alias: string): Promise<Member[]> {
|
||||
const channel = await this.get({alias});
|
||||
|
||||
if (!channel) {
|
||||
throw new ChannelError(`Channel not found: ${alias}`, 'CHANNEL_NOT_FOUND');
|
||||
}
|
||||
|
||||
|
||||
return this.getChannelMembers(channel.channelId);
|
||||
}
|
||||
|
||||
public async deleteChannel(channelId: string): Promise<Channel> {
|
||||
return this.delete(channelId);
|
||||
}
|
||||
|
||||
public async delete(channelId: string): Promise<Channel> {
|
||||
if (!channelId || channelId.trim().length === 0) {
|
||||
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
|
||||
public async delete({channelId, alias}: {channelId?: string; alias?: string}): Promise<Channel> {
|
||||
if (!channelId && !alias) {
|
||||
throw new ChannelError('Deleting a channel requires either a channelId or alias', 'INVALID_ARGUMENTS');
|
||||
}
|
||||
|
||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.DELETE, `/channel/${encodeURIComponent(channelId)}`);
|
||||
const channelIdToDelete = alias ? (await this.get({alias}))?.channelId : channelId;
|
||||
|
||||
if (!channelIdToDelete) {
|
||||
throw new ChannelError('Unable to find room to delete', 'NOT_FOUND');
|
||||
}
|
||||
|
||||
const route = `/channel/${encodeURIComponent(channelId)}`;
|
||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.DELETE, route);
|
||||
|
||||
if (!response.channel) {
|
||||
throw new ChannelError(`Invalid response format for deleted channel: ${channelId}`, 'INVALID_RESPONSE');
|
||||
throw new ChannelError(`Invalid response format for deleted channel [${channelId}]`, 'INVALID_RESPONSE');
|
||||
}
|
||||
|
||||
// Remove from cache if it exists
|
||||
@@ -207,14 +186,8 @@ export class Channels {
|
||||
return deletedChannel;
|
||||
}
|
||||
|
||||
// Cache management methods
|
||||
public isInitialized(): boolean {
|
||||
return this._initialized !== false && !(this._initialized instanceof Promise);
|
||||
}
|
||||
|
||||
public clearCache(): void {
|
||||
this._channelsByAlias.clear();
|
||||
this._initialized = false;
|
||||
}
|
||||
|
||||
public getCacheSize(): number {
|
||||
@@ -222,25 +195,18 @@ export class Channels {
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
this._initialized = (async () => {
|
||||
try {
|
||||
const channelsList = await this.list();
|
||||
if (!channelsList) {
|
||||
console.warn('[Channels] Failed to initialize cache - no channels returned');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const channel of channelsList) {
|
||||
this._channelsByAlias.set(channel.alias, channel);
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
} catch (error) {
|
||||
console.error('[Channels] Failed to initialize cache:', error);
|
||||
this._initialized = true; // Mark as initialized even on error to avoid repeated attempts
|
||||
try {
|
||||
const channelsList = await this.list();
|
||||
if (!channelsList) {
|
||||
console.warn('[Channels] Failed to initialize cache - no channels returned');
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
await this._initialized;
|
||||
|
||||
for (const channel of channelsList) {
|
||||
this._channelsByAlias.set(channel.alias, channel);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Channels] Failed to initialize cache:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ export class PCastHttpRequests extends HttpRequests {
|
||||
|
||||
constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) {
|
||||
const credentials = `${applicationCredentials.id}:${applicationCredentials.secret}`;
|
||||
const basic = typeof btoa === 'function'
|
||||
? btoa(credentials)
|
||||
: Buffer.from(credentials, 'utf-8').toString('base64');
|
||||
const basic = typeof btoa === 'function' ? btoa(credentials) : Buffer.from(credentials, 'utf-8').toString('base64');
|
||||
|
||||
const baseHeaders = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -2,7 +2,7 @@ import {HttpMethod} from '../net/http/HttpMethod';
|
||||
import type {PCastHttpRequests} from './PCastRequests';
|
||||
import assertUnreachable from '../lang/assertUnreachable';
|
||||
import {ReportKind} from './ReportKind';
|
||||
import { ViewingReportKind, ViewingReportKindMapping } from './ViewingReportKind';
|
||||
import {ViewingReportKind, ViewingReportKindMapping} from './ViewingReportKind';
|
||||
|
||||
export type PublishingReportOptions = {
|
||||
applicationIds?: string[];
|
||||
@@ -16,7 +16,6 @@ export type PublishingReportOptions = {
|
||||
end: string;
|
||||
};
|
||||
|
||||
|
||||
export type ViewingReportOptions = {
|
||||
kind: ViewingReportKind;
|
||||
applicationIds?: string[];
|
||||
@@ -78,7 +77,7 @@ export class Reporting {
|
||||
};
|
||||
|
||||
const response = await this._httpRequests.request<string>(HttpMethod.PUT, '/reporting/viewing', requestViewingOptions);
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import assertUnreachable from "../lang/assertUnreachable";
|
||||
import assertUnreachable from '../lang/assertUnreachable';
|
||||
|
||||
export enum ViewingReportKind {
|
||||
RealTime = 0,
|
||||
HLS = 1,
|
||||
DASH = 2
|
||||
export enum ViewingReportKind {
|
||||
RealTime = 0,
|
||||
HLS = 1,
|
||||
DASH = 2
|
||||
}
|
||||
|
||||
export type ViewingReportKindType = 'RealTime' | 'HLS' | 'DASH';
|
||||
|
||||
export class ViewingReportKindMapping {
|
||||
public static convertViewingReportKindTypeToViewingReportKind(viewingReportKindType: ViewingReportKindType): ViewingReportKind {
|
||||
switch (viewingReportKindType) {
|
||||
case 'RealTime':
|
||||
return ViewingReportKind.RealTime;
|
||||
case 'HLS':
|
||||
return ViewingReportKind.HLS;
|
||||
case 'DASH':
|
||||
return ViewingReportKind.DASH;
|
||||
default:
|
||||
assertUnreachable(viewingReportKindType);
|
||||
}
|
||||
public static convertViewingReportKindTypeToViewingReportKind(viewingReportKindType: ViewingReportKindType): ViewingReportKind {
|
||||
switch (viewingReportKindType) {
|
||||
case 'RealTime':
|
||||
return ViewingReportKind.RealTime;
|
||||
case 'HLS':
|
||||
return ViewingReportKind.HLS;
|
||||
case 'DASH':
|
||||
return ViewingReportKind.DASH;
|
||||
default:
|
||||
assertUnreachable(viewingReportKindType);
|
||||
}
|
||||
}
|
||||
|
||||
public static convertViewingReportKindToViewingReportKindType(viewingReportKind: ViewingReportKind): ViewingReportKindType {
|
||||
switch (viewingReportKind) {
|
||||
case ViewingReportKind.RealTime:
|
||||
return 'RealTime';
|
||||
case ViewingReportKind.HLS:
|
||||
return 'HLS';
|
||||
case ViewingReportKind.DASH:
|
||||
return 'DASH';
|
||||
default:
|
||||
assertUnreachable(viewingReportKind);
|
||||
}
|
||||
public static convertViewingReportKindToViewingReportKindType(viewingReportKind: ViewingReportKind): ViewingReportKindType {
|
||||
switch (viewingReportKind) {
|
||||
case ViewingReportKind.RealTime:
|
||||
return 'RealTime';
|
||||
case ViewingReportKind.HLS:
|
||||
return 'HLS';
|
||||
case ViewingReportKind.DASH:
|
||||
return 'DASH';
|
||||
default:
|
||||
assertUnreachable(viewingReportKind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ export * from './Reporting';
|
||||
export * from './PCastRequests';
|
||||
export * from './Stream';
|
||||
export * from './ReportKind';
|
||||
export * from './ViewingReportKind';
|
||||
export * from './ViewingReportKind';
|
||||
|
||||
Reference in New Issue
Block a user