From d0f3af72f6618c953a3a078e1459954caa819f12 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Mon, 1 Sep 2025 21:21:45 -0400 Subject: [PATCH] update legacy apis --- .prettierrc | 2 +- examples/src/index.ts | 39 +++---------- package.json | 4 +- src/PCastApi.ts | 57 ++++++++++++++++++- src/apis/Channels.ts | 40 +++++++++++-- src/apis/{ => Reporting}/ReportKind.ts | 11 +++- src/apis/{ => Reporting}/Reporting.ts | 51 ++++++++++++++--- src/apis/{ => Reporting}/ViewingReportKind.ts | 2 +- src/apis/Reporting/index.ts | 3 + src/apis/index.ts | 3 +- src/index.ts | 9 +-- tsconfig.json | 2 +- 12 files changed, 160 insertions(+), 63 deletions(-) rename src/apis/{ => Reporting}/ReportKind.ts (71%) rename src/apis/{ => Reporting}/Reporting.ts (58%) rename src/apis/{ => Reporting}/ViewingReportKind.ts (94%) create mode 100644 src/apis/Reporting/index.ts diff --git a/.prettierrc b/.prettierrc index caa814d..5e87b44 100644 --- a/.prettierrc +++ b/.prettierrc @@ -2,7 +2,7 @@ "arrowParens": "avoid", "bracketSameLine": true, "bracketSpacing": false, - "printWidth": 160, + "printWidth": 180, "semi": true, "singleAttributePerLine": false, "singleQuote": true, diff --git a/examples/src/index.ts b/examples/src/index.ts index 5ddd990..6da4d8c 100644 --- a/examples/src/index.ts +++ b/examples/src/index.ts @@ -1,4 +1,4 @@ -import PCastApi from '../../src/index'; +import PCastApi, {ReportKind} from '../../src/index'; const pcastUri = 'https://pcast-stg.phenixrts.com'; const application = { @@ -6,34 +6,11 @@ const application = { secret: 'AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg' }; const pcastApi = PCastApi.create(pcastUri, application); +const viewingReportRealTime = await pcastApi.generateViewingReport('RealTime', new Date('2025-08-02'), new Date('2025-09-01'), {}); +const publishingReport = await pcastApi.reporting.generateReport(ReportKind.Publishing, { + start: '2025-08-02', + end: '2025-09-01' +}); -console.log('pcastApi [%o]', pcastApi); -console.log(); -console.log('ChannelsApi [%o]', pcastApi.channels); -const channelsList = await pcastApi.channels.list(); - -// const start = hrtime.bigint(); -// const publishingReportCsv = await pcastApi.reporting.generateReport(ReportKind.Publishing, { -// start: moment().subtract(1, 'day').toISOString(), -// end: moment().toISOString(), -// applicationIds: [application.id] -// }); -// const endPublishing = hrtime.bigint(); -// 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(), -// end: moment().toISOString(), -// applicationIds: [application.id] -// }); -// const endViewing = hrtime.bigint(); -// console.log(`Time taken: ${Number(endViewing - endPublishing) / 1_000_000_000} seconds`); -// // const viewingReport = await CsvParser.parse(viewingReportCsv); - -// console.log(viewingReportCsv); - -const channelMembers = await pcastApi.channels.getMembers(channelsList[1].channelId); -console.log('[%o]', channelMembers); -console.log('[%o]', channelMembers.streams); +// console.log(viewingReportRealTime); +console.log(publishingReport); diff --git a/package.json b/package.json index 48040df..f2f3b9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@techniker-me/pcast-api", - "version": "2025.1.4", + "version": "2025.1.5", "type": "module", "scripts": { "ci-build": "bun run build", @@ -9,7 +9,7 @@ "test:coverage": "bun test --coverage", "preformat": "bun install", "format": "prettier --write ./", - "prelint": "bun format", + "prelint:fix": "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", diff --git a/src/PCastApi.ts b/src/PCastApi.ts index b8a4cd3..a743b68 100644 --- a/src/PCastApi.ts +++ b/src/PCastApi.ts @@ -1,4 +1,5 @@ -import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests, Reporting} from './apis'; +import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests, Reporting, Channel, Member} from './apis'; +import {ReportKind, ViewingReportKindMapping, type ViewingReportKindType} from './apis/Reporting'; export class PCastApi { private readonly _channels: Channels; @@ -12,7 +13,8 @@ export class PCastApi { } public static create(pcastUri: string, applicationCredentials: ApplicationCredentials): PCastApi { - const pcastHttpRequests = new PCastHttpRequests(pcastUri.replace(/\/+$/, '').endsWith('/pcast') ? pcastUri : `${pcastUri}/pcast`, applicationCredentials); + const pcastUriWithSuffix = pcastUri.replace(/\/+$/, '').endsWith('/pcast') ? pcastUri : `${pcastUri}/pcast`; + const pcastHttpRequests = new PCastHttpRequests(pcastUriWithSuffix, applicationCredentials); const channels = new Channels(pcastHttpRequests); const streams = new Streams(pcastHttpRequests); const reporting = new Reporting(pcastHttpRequests); @@ -31,4 +33,55 @@ export class PCastApi { get reporting(): Reporting { return this._reporting; } + + // ====== Legacy API ======= + // @deprecated - use pcastApi.channels.create instead + public async createChannel(channelName: string, channelDescription: string, channelOptions: string[] = []): Promise { + return this._channels.create(channelName, channelDescription, channelOptions); + } + + // @deprecated - use pcastApi.channels.get instead + public async getChannelInfoByAlias(alias: string): Promise { + return this._channels.get({alias}); + } + + // @deprecated - use pcastApi.channels.get instead + public async getChannelInfoByChannelId(channelId: string): Promise { + return this._channels.get({channelId}); + } + + // @deprecated - use pcastApi.channels.getPublisherCount instead + public async getChannelPublisherCount(channelId: string): Promise { + return this._channels.getPublisherCount(channelId); + } + + // @deprecated - use pcastApi.channels.getMembers instead + public async getChannelMembers(channelId: string): Promise { + return this._channels.getMembers(channelId); + } + + // @deprecated - use pcastApi.channels.getMembersByChannelAlias instead + public async getChannelMembersByChannelAlias(alias: string): Promise { + return this._channels.getMembersByChannelAlias(alias); + } + + // @deprecated - use pcastApi.channels.delete instead + public async deleteChannel(channelId: string): Promise { + return this._channels.delete({channelId}); + } + + // @deprecated - use pcastApi.reporting.generateReport instead + public async generateViewingReport( + kind: ViewingReportKindType, + reportStartTimestamp: Date, + reportEndTimestamp: Date, + options: Record = {} + ): Promise { + return this._reporting.generateReport(ReportKind.Viewing, { + kind: ViewingReportKindMapping.convertViewingReportKindTypeToViewingReportKind(kind), + start: reportStartTimestamp.toISOString(), + end: reportEndTimestamp.toISOString(), + ...options + }); + } } diff --git a/src/apis/Channels.ts b/src/apis/Channels.ts index 712dcda..8acf55b 100644 --- a/src/apis/Channels.ts +++ b/src/apis/Channels.ts @@ -55,7 +55,7 @@ export class Channels { this.initialize(); } - public async createChannel(name: string, description: string, channelOptions: string[] = []): Promise { + public async create(name: string, description: string, channelOptions: string[] = []): Promise { if (!name || name.trim().length === 0) { throw new ChannelError('Channel name cannot be empty', 'INVALID_NAME'); } @@ -186,13 +186,41 @@ export class Channels { return deletedChannel; } - public clearCache(): void { - this._channelsByAlias.clear(); + async getPublishSourceStreamId(channelId: string, retryCount: number = 3): Promise { + const retryCountRemaining = retryCount || 3; + const channelMembers = await this.getMembers(channelId); + + if (channelMembers.length === 0) { + if (retryCountRemaining > 0) { + return this.getPublishSourceStreamId(channelId, retryCountRemaining - 1); + } + + return null; + } + + const presenter = channelMembers.find(member => member.role === 'Presenter'); + + if (!presenter) { + if (retryCountRemaining > 0) { + return this.getPublishSourceStreamId(channelId, retryCountRemaining - 1); + } + + return null; + } + + const publishSourceStreamIdRegExp = /pcast:\/\/.*\/([^?]*)/; + + return presenter.streams[0].uri.match(publishSourceStreamIdRegExp)?.[1] ?? null; } - public getCacheSize(): number { - return this._channelsByAlias.size; - } + // TODO(AZ): Implement this + // public async fork(channelId: string): Promise + + // TODO(AZ): Implement this + // public async killChannel(channelId: string): Promise + + // TODO(AZ): Implement this + // public async publishViaUrl(mediaUriToPublish: string, token: string): Promise private async initialize(): Promise { try { diff --git a/src/apis/ReportKind.ts b/src/apis/Reporting/ReportKind.ts similarity index 71% rename from src/apis/ReportKind.ts rename to src/apis/Reporting/ReportKind.ts index 5157855..e1c5b5a 100644 --- a/src/apis/ReportKind.ts +++ b/src/apis/Reporting/ReportKind.ts @@ -1,11 +1,12 @@ -import assertUnreachable from '../lang/assertUnreachable'; +import assertUnreachable from '../../lang/assertUnreachable'; export enum ReportKind { Publishing = 0, - Viewing = 1 + Viewing = 1, + Ingest = 2 } -export type ReportKindType = 'Publishing' | 'Viewing'; +export type ReportKindType = 'Publishing' | 'Viewing' | 'IngestReport'; export class ReportKindMapping { public static convertReportKindTypeToReportKind(reportKindType: ReportKindType): ReportKind { @@ -14,6 +15,8 @@ export class ReportKindMapping { return ReportKind.Publishing; case 'Viewing': return ReportKind.Viewing; + case 'IngestReport': + return ReportKind.Ingest; default: assertUnreachable(reportKindType); @@ -26,6 +29,8 @@ export class ReportKindMapping { return 'Publishing'; case ReportKind.Viewing: return 'Viewing'; + case ReportKind.Ingest: + return 'IngestReport'; default: assertUnreachable(reportKind); diff --git a/src/apis/Reporting.ts b/src/apis/Reporting/Reporting.ts similarity index 58% rename from src/apis/Reporting.ts rename to src/apis/Reporting/Reporting.ts index 93e128a..4336c9c 100644 --- a/src/apis/Reporting.ts +++ b/src/apis/Reporting/Reporting.ts @@ -1,6 +1,6 @@ -import {HttpMethod} from '../net/http/HttpMethod'; -import type {PCastHttpRequests} from './PCastRequests'; -import assertUnreachable from '../lang/assertUnreachable'; +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'; @@ -32,6 +32,17 @@ export type ViewingReportOptions = { end: string; }; +export type IngestBufferUnderrunOptions = { + kind: 'BufferUnderrun'; // Are there more kinds? https://phenixrts.com/docs/sdk_ref/rest-api/reporting/#generate-an-ingest-report + applicationIds?: string[]; + streamIds?: string[]; + channelIds?: string[]; + channelAliases?: string[]; + ingestIds?: string[]; + start: string; + end: string; +}; + export class Reporting { private readonly _httpRequests: PCastHttpRequests; @@ -39,12 +50,17 @@ export class Reporting { this._httpRequests = httpRequests; } - public async generateReport(kind: ReportKind, options: ReportOptions): Promise { + public async generateReport( + kind: ReportKind, + options: ReportOptions + ): Promise { switch (kind) { case ReportKind.Publishing: return this.requestPublishingReport(options as PublishingReportOptions); case ReportKind.Viewing: return this.requestViewingReport(options as ViewingReportOptions); + case ReportKind.Ingest: + return this.requestIngestBufferUnderrun(options as IngestBufferUnderrunOptions); default: assertUnreachable(kind); } @@ -54,12 +70,12 @@ export class Reporting { if (!options.start || !options.end) { throw new Error('[Reporting] [requestPublishingReport] requires a start and end Date'); } - const publishingReportOptions = { + const publishingReport = { ...options }; const requestPublishingOptions = { - body: JSON.stringify({publishingReport: publishingReportOptions}) + body: JSON.stringify({publishingReport: publishingReport}) }; const response = await this._httpRequests.request(HttpMethod.PUT, '/reporting/publishing', requestPublishingOptions); @@ -67,17 +83,36 @@ export class Reporting { } private async requestViewingReport(options: ViewingReportOptions): Promise { - const viewingReportOptions = { + const viewingReport = { ...options, + tags: options.tags ?? [], + originTags: options.originTags ?? [], kind: ViewingReportKindMapping.convertViewingReportKindToViewingReportKindType(options.kind) }; + console.log(viewingReport); + const requestViewingOptions = { - body: JSON.stringify({viewingReport: viewingReportOptions}) + body: JSON.stringify({viewingReport: viewingReport}) }; const response = await this._httpRequests.request(HttpMethod.PUT, '/reporting/viewing', requestViewingOptions); return response; } + + private async requestIngestBufferUnderrun(options: IngestBufferUnderrunOptions): Promise { + const ingestReport = { + ...options, + kind: 'BufferUnderrun' + }; + + const requestViewingOptions = { + body: JSON.stringify({ingestReport}) + }; + + const response = await this._httpRequests.request(HttpMethod.PUT, '/reporting/ingest', requestViewingOptions); + + return response; + } } diff --git a/src/apis/ViewingReportKind.ts b/src/apis/Reporting/ViewingReportKind.ts similarity index 94% rename from src/apis/ViewingReportKind.ts rename to src/apis/Reporting/ViewingReportKind.ts index a8d6951..2f79da8 100644 --- a/src/apis/ViewingReportKind.ts +++ b/src/apis/Reporting/ViewingReportKind.ts @@ -1,4 +1,4 @@ -import assertUnreachable from '../lang/assertUnreachable'; +import assertUnreachable from '../../lang/assertUnreachable'; export enum ViewingReportKind { RealTime = 0, diff --git a/src/apis/Reporting/index.ts b/src/apis/Reporting/index.ts new file mode 100644 index 0000000..0040a34 --- /dev/null +++ b/src/apis/Reporting/index.ts @@ -0,0 +1,3 @@ +export * from './Reporting'; +export * from './ReportKind'; +export * from './ViewingReportKind'; diff --git a/src/apis/index.ts b/src/apis/index.ts index 7c8f7f3..cac9dc5 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -2,5 +2,4 @@ export * from './Channels'; export * from './Reporting'; export * from './PCastRequests'; export * from './Stream'; -export * from './ReportKind'; -export * from './ViewingReportKind'; +export * from './Reporting'; diff --git a/src/index.ts b/src/index.ts index e209c43..39de592 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,13 @@ import {PCastApi} from './PCastApi'; -import type {Channels, Streams, Reporting, ReportKind, ViewingReportKind} from './apis'; +import {Channels, Streams, Reporting, PublishingReportOptions, ViewingReportOptions, ReportKindType, ViewingReportKindType, ViewingReportKind, ReportKind} from './apis'; import type {ChannelId, Channel, ChannelAlias, Member, ChannelError} from './apis/Channels'; import type {HttpMethod} from './net/http/HttpMethod'; import type {HttpRequestError} from './net/http/HttpRequests'; import type {ChannelResponse, ChannelsResponse, MembersResponse} from './apis/IResponse'; import type {ApplicationCredentials} from './apis/PCastRequests'; -import type {PublishingReportOptions, ViewingReportOptions} from './apis/Reporting'; -import type {ReportKindType} from './apis/ReportKind'; -import type {ViewingReportKindType} from './apis/ViewingReportKind'; -export type {Channels, Streams, Reporting, ReportKind, ViewingReportKind}; +export type {Channels, Streams, Reporting, ViewingReportKind}; export type {ChannelId, Channel, ChannelAlias, Member, ChannelError}; export type {HttpMethod, HttpRequestError}; export type {ChannelResponse, ChannelsResponse, MembersResponse}; @@ -18,5 +15,5 @@ export type {ApplicationCredentials}; export type {PublishingReportOptions, ViewingReportOptions}; export type {ReportKindType, ViewingReportKindType}; -export {PCastApi}; +export {PCastApi, ReportKind}; export default PCastApi; diff --git a/tsconfig.json b/tsconfig.json index 563bea4..20e1818 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, + "verbatimModuleSyntax": false, "emitDeclarationOnly": true, "declaration": true, "declarationDir": "./dist/types",