From 6a40cfa568e066d23e29d998d83369755c89a534 Mon Sep 17 00:00:00 2001 From: Alexander Zinn Date: Sun, 17 Aug 2025 10:36:57 -0400 Subject: [PATCH] basic functionality... --- .npmrc | 4 ++ bun.lock | 32 --------- examples/example-1.js | 11 +++ package.json | 8 +++ src/PCastApi.ts | 52 +++++++------- src/index.ts | 4 +- src/net/http/HttpRequests.ts | 14 ++-- src/pcast/Channels.ts | 31 ++++---- src/pcast/IResponse.ts | 2 +- src/pcast/PCastRequests.ts | 39 +++++------ src/pcast/Reporting.ts | 132 +++++++++++++++++++++++++++++++++++ src/pcast/Stream.ts | 2 +- src/pcast/index.ts | 2 +- 13 files changed, 227 insertions(+), 106 deletions(-) create mode 100644 .npmrc delete mode 100644 bun.lock create mode 100644 examples/example-1.js create mode 100644 src/pcast/Reporting.ts diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..a494ac2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,4 @@ +//registry-node.techniker.me/:_authToken="${NODE_REGISTRY_AUTH_TOKEN}" +@techniker-me:registry=https://registry-node.techniker.me +save-exact=true +package-lock=false \ No newline at end of file diff --git a/bun.lock b/bun.lock deleted file mode 100644 index 2d63298..0000000 --- a/bun.lock +++ /dev/null @@ -1,32 +0,0 @@ -{ - "lockfileVersion": 1, - "workspaces": { - "": { - "name": "@techniker-me/pcast-api", - "devDependencies": { - "@types/bun": "latest", - "prettier": "^3.6.2", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], - - "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], - - "@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="], - - "bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], - - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - - "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], - - "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], - } -} diff --git a/examples/example-1.js b/examples/example-1.js new file mode 100644 index 0000000..6e321b0 --- /dev/null +++ b/examples/example-1.js @@ -0,0 +1,11 @@ +import {PCastApi} from '../dist/node/index.js'; + +const applicationCredentials = { + id: 'phenixrts.com-alex.zinn', + secret: 'AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg' +} + +const pcastUri = 'https://pcast-stg.phenixrts.com'; +const pcastApi = new PCastApi(pcastUri, applicationCredentials); + +pcastApi.channels.list().then(console.log); \ No newline at end of file diff --git a/package.json b/package.json index 68b2829..abfdc67 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,19 @@ "name": "@techniker-me/pcast-api", "module": "src/index.ts", "type": "module", + "scripts": { + "ci-build": "bun run build:node && bun run build:browser", + "build:node": "bun build src/index.ts --outdir dist/node --target node --format esm --minify --production", + "build:browser": "bun build src/index.ts --outdir dist/browser --target browser --format esm --minify --production" + }, "devDependencies": { "@types/bun": "latest", "prettier": "^3.6.2" }, "peerDependencies": { "typescript": "^5" + }, + "publishConfig": { + "registry": "https://registry-node.techniker.me" } } diff --git a/src/PCastApi.ts b/src/PCastApi.ts index 87a6a2f..944dd31 100644 --- a/src/PCastApi.ts +++ b/src/PCastApi.ts @@ -1,34 +1,34 @@ -import { Channels, Streams, type ApplicationCredentials, PCastHttpRequests} from "./pcast"; +import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests} from './pcast'; export class PCastApi { - private readonly _pcastUri: string; - private readonly _applicationCredentials: ApplicationCredentials; - private readonly _pcastHttpRequests: PCastHttpRequests; + private readonly _pcastUri: string; + private readonly _applicationCredentials: ApplicationCredentials; + private readonly _pcastHttpRequests: PCastHttpRequests; - constructor(pcastUri: string, applicationCredentials: ApplicationCredentials) { - if (pcastUri.split('/').at(-1) !== 'pcast') { - this._pcastUri = `${pcastUri}/pcast`; - } else { - this._pcastUri = pcastUri; - } - - this._applicationCredentials = applicationCredentials; - this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials); + constructor(pcastUri: string, applicationCredentials: ApplicationCredentials) { + if (pcastUri.split('/').at(-1) !== 'pcast') { + this._pcastUri = `${pcastUri}/pcast`; + } else { + this._pcastUri = pcastUri; } - get pcastUri(): string { - return this._pcastUri; - } + this._applicationCredentials = applicationCredentials; + this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials); + } - get applicationCredentials(): ApplicationCredentials { - return this._applicationCredentials; - } + get pcastUri(): string { + return this._pcastUri; + } - get channels(): Channels { - return new Channels(this._pcastHttpRequests); - } + get applicationCredentials(): ApplicationCredentials { + return this._applicationCredentials; + } - get streams(): Streams { - return new Streams(this._pcastHttpRequests); - } -} \ No newline at end of file + get channels(): Channels { + return new Channels(this._pcastHttpRequests); + } + + get streams(): Streams { + return new Streams(this._pcastHttpRequests); + } +} diff --git a/src/index.ts b/src/index.ts index dab0fcb..9660d65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import {PCastApi} from "./PCastApi"; +import {PCastApi} from './PCastApi'; export * from './pcast'; export {PCastApi}; -export default {PCastApi}; \ No newline at end of file +export default {PCastApi}; diff --git a/src/net/http/HttpRequests.ts b/src/net/http/HttpRequests.ts index 0a183af..3eab2f0 100644 --- a/src/net/http/HttpRequests.ts +++ b/src/net/http/HttpRequests.ts @@ -5,8 +5,8 @@ const httpMethodsThatMustNotHaveBody = [HttpMethod.GET]; // Head, Options export class HttpRequestError extends Error { constructor( - message: string, - public readonly status?: number, + message: string, + public readonly status?: number, public readonly statusText?: string, public readonly originalError?: unknown ) { @@ -51,11 +51,7 @@ export class HttpRequests { const response = await fetch(requestPath, options); if (!response.ok) { - throw new HttpRequestError( - `HTTP error! status [${response.status}] ${response.statusText}`, - response.status, - response.statusText - ); + throw new HttpRequestError(`HTTP error! status [${response.status}] ${response.statusText}`, response.status, response.statusText); } const responseContentType = response.headers.get('content-type'); @@ -69,14 +65,14 @@ export class HttpRequests { if (e instanceof HttpRequestError) { throw e; } - + if (e instanceof Error) { if (e.name === 'AbortError') { throw new HttpRequestError(`Request timeout after ${timeoutDuration}ms`, undefined, undefined, e); } throw new HttpRequestError(`Request failed: ${e.message}`, undefined, undefined, e); } - + throw new HttpRequestError(`Unknown request error: ${String(e)}`, undefined, undefined, e); } finally { globalThis.clearTimeout(requestTimeoutId); diff --git a/src/pcast/Channels.ts b/src/pcast/Channels.ts index c7aed48..092e09e 100644 --- a/src/pcast/Channels.ts +++ b/src/pcast/Channels.ts @@ -21,7 +21,10 @@ type GetChannelParams = { }; export class ChannelError extends Error { - constructor(message: string, public readonly code: string) { + constructor( + message: string, + public readonly code: string + ) { super(message); this.name = 'ChannelError'; } @@ -50,7 +53,7 @@ export class Channels { for (let channel of channelsList) { this._channelsByAlias.set(channel.alias, channel); } - + this._initialized = true; console.log('[Channels] [initialize] Cache populated successfully'); } catch (error) { @@ -70,14 +73,14 @@ export class Channels { throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS'); } - const createChannelRequestBody = { - channel: { - name: name.trim(), - alias: name.trim(), - description: description.trim(), - options: channelOptions - } - }; + const createChannelRequestBody = { + channel: { + name: name.trim(), + alias: name.trim(), + description: description.trim(), + options: channelOptions + } + }; const response = await this._httpRequests.request(HttpMethod.PUT, '/channel', { body: JSON.stringify(createChannelRequestBody) @@ -117,7 +120,7 @@ export class Channels { } const response = await this._httpRequests.request(HttpMethod.GET, `/channel/${encodeURIComponent(alias)}`); - + if (!response.channel) { throw new ChannelError(`Invalid response format for channel: ${alias}`, 'INVALID_RESPONSE'); } @@ -129,7 +132,7 @@ export class Channels { if (channelId) { const response = await this._httpRequests.request(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}`); - + if (!response.channel) { throw new ChannelError(`Invalid response format for channel ID: ${channelId}`, 'INVALID_RESPONSE'); } @@ -144,7 +147,7 @@ export class Channels { } const response = await this._httpRequests.request(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/members`); - + if (!response.members) { throw new ChannelError(`Invalid response format for channel members: ${channelId}`, 'INVALID_RESPONSE'); } @@ -158,7 +161,7 @@ export class Channels { } const response = await this._httpRequests.request(HttpMethod.DELETE, `/channel/${encodeURIComponent(channelId)}`); - + if (!response.channel) { throw new ChannelError(`Invalid response format for deleted channel: ${channelId}`, 'INVALID_RESPONSE'); } diff --git a/src/pcast/IResponse.ts b/src/pcast/IResponse.ts index de7d719..b45cb57 100644 --- a/src/pcast/IResponse.ts +++ b/src/pcast/IResponse.ts @@ -1,4 +1,4 @@ -import type { Channel } from './Channels'; +import type {Channel} from './Channels'; export default interface IResponse { status: string; diff --git a/src/pcast/PCastRequests.ts b/src/pcast/PCastRequests.ts index 6a663b9..594be2c 100644 --- a/src/pcast/PCastRequests.ts +++ b/src/pcast/PCastRequests.ts @@ -1,26 +1,25 @@ - -import { HttpRequests} from "../net/http/HttpRequests"; +import {HttpRequests} from '../net/http/HttpRequests'; export type ApplicationCredentials = { - id: string; - secret: string; -} + id: string; + secret: string; +}; export class PCastHttpRequests extends HttpRequests { - private readonly _tenancy: string; - - constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) { - const baseHeaders = new Headers({ - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: `Basic ${btoa(`${applicationCredentials.id}:${applicationCredentials.secret}`)}` - }); - super(baseUri, baseHeaders, options); + private readonly _tenancy: string; - this._tenancy = applicationCredentials.id; - } + constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) { + const baseHeaders = new Headers({ + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `Basic ${btoa(`${applicationCredentials.id}:${applicationCredentials.secret}`)}` + }); + super(baseUri, baseHeaders, options); - get tenancy(): string { - return this._tenancy; - } -} \ No newline at end of file + this._tenancy = applicationCredentials.id; + } + + get tenancy(): string { + return this._tenancy; + } +} diff --git a/src/pcast/Reporting.ts b/src/pcast/Reporting.ts new file mode 100644 index 0000000..996cca2 --- /dev/null +++ b/src/pcast/Reporting.ts @@ -0,0 +1,132 @@ +import assertUnreachable from '../lang/assertUnreachable'; +import {HttpMethod} from '../net/http/HttpMethod'; +import type IResponse from './IResponse'; +import type { PCastHttpRequests } from './PCastRequests'; + +export enum ReportKind { + Publishing = 0, + Viewing = 1 +} + +export type ReportKindType = 'Publishing' | 'Viewing'; + +export class ReportKindMapping { + public static convertReportKindTypeToReportKind(reportKindType: ReportKindType): ReportKind { + switch (reportKindType) { + case 'Publishing': + return ReportKind.Publishing; + case 'Viewing': + return ReportKind.Viewing; + + default: + assertUnreachable(reportKindType); + } + } + + public static convertReportKindToReportKindType(reportKind: ReportKind): ReportKindType { + switch (reportKind) { + case ReportKind.Publishing: + return 'Publishing'; + case ReportKind.Viewing: + return 'Viewing'; + + default: + assertUnreachable(reportKind); + } + } +} + +export type PublishingReportOptions = { + applicationIds?: string[]; + streamIds?: string[]; + channelIds?: string[]; + channelAliases?: string[]; + roomIds?: string[]; + roomAliases?:string[]; + tags?: string[]; + start: string; + end: string; +}; + +export enum ViewingReportKind { + RealTime = 0, + HLS = 1, + DASH = 2 +} + +export type ViewingReportKindType = 'RealTime' | 'HLS' | 'DASH'; + +export type ViewingReportOptions = { + kind: ViewingReportKind, + applicationIds?: string[]; + streamIds?: string[]; + sessionIds?: string[]; + originStreamIds?: string[]; + originTags?: string[]; + channelIds?: string[]; + channelAliases?: string[]; + roomIds?: string[]; + roomAliases?:string[]; + tags?: string[]; + start: string; + end: string; + + +}; + + +export class Reporting { + private readonly _httpRequests: PCastHttpRequests; + + constructor(httpRequests: PCastHttpRequests) { + this._httpRequests = httpRequests; + } + + public async generateReport(kind: ReportKind, options: ReportOptions): Promise { + console.log('[Reporting] generateReport [%o]', ReportKindMapping.convertReportKindToReportKindType(kind)); + + if (kind === ReportKind.Publishing) { + return this.requestPublishingReport(options as PublishingReportOptions); + } + + if (kind === ReportKind.Viewing) { + return this.requestViewingReport(options as ViewingReportOptions); + } + + throw new Error(`[Reporting] Unsupported report kind: ${kind}`); + } + + public async requestPublishingReport(options: PublishingReportOptions): Promise { + + if (!(options.start || options.end)) { + throw new Error('[Reporting] [requestPublishingReport] requires a start and end Date'); + } + const publishingReportOptions = { + ...options + }; + + const requestPublishingOptions = { + body: JSON.stringify({publishingReport: publishingReportOptions}) + + }; + const response = await this._httpRequests.request>(HttpMethod.PUT, '/pcast/reporting/publishing', requestPublishingOptions); + + return response; + + } + + private async requestViewingReport(options: ViewingReportOptions): Promise { + const viewingReportOptions = { + ...options + }; + + const requestViewingOptions = { + body: JSON.stringify({viewingReport: viewingReportOptions}) + }; + + const response = await this._httpRequests.request>(HttpMethod.PUT, '/pcast/reporting/viewing', requestViewingOptions); + + return response; + } + +} diff --git a/src/pcast/Stream.ts b/src/pcast/Stream.ts index 7838317..3771dc5 100644 --- a/src/pcast/Stream.ts +++ b/src/pcast/Stream.ts @@ -12,7 +12,7 @@ export class Streams { public async publishUri(mediaUri: string, token: string) { - const mediaType = mediaUri.split('.')?[mediaUri.at(-1)]; + const mediaType = mediaUri.split('.')?.at(-1); if (!mediaType) { throw new Error('Invalid media URI no media type found'); diff --git a/src/pcast/index.ts b/src/pcast/index.ts index b7a33cc..c144c68 100644 --- a/src/pcast/index.ts +++ b/src/pcast/index.ts @@ -1,3 +1,3 @@ export * from './Channels'; export * from './Stream'; -export * from './PCastRequests'; \ No newline at end of file +export * from './PCastRequests';