@bernierllc/content-type-audio 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.eslintrc.js +42 -0
  2. package/README.md +500 -0
  3. package/__tests__/AudioContentTypeManager.test.ts +592 -0
  4. package/__tests__/integration.test.ts +279 -0
  5. package/__tests__/utils.test.ts +142 -0
  6. package/coverage/AudioContentTypeManager.ts.html +799 -0
  7. package/coverage/base.css +224 -0
  8. package/coverage/block-navigation.js +87 -0
  9. package/coverage/favicon.png +0 -0
  10. package/coverage/index.html +161 -0
  11. package/coverage/lcov-report/AudioContentTypeManager.ts.html +799 -0
  12. package/coverage/lcov-report/base.css +224 -0
  13. package/coverage/lcov-report/block-navigation.js +87 -0
  14. package/coverage/lcov-report/favicon.png +0 -0
  15. package/coverage/lcov-report/index.html +161 -0
  16. package/coverage/lcov-report/prettify.css +1 -0
  17. package/coverage/lcov-report/prettify.js +2 -0
  18. package/coverage/lcov-report/schemas.ts.html +211 -0
  19. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  20. package/coverage/lcov-report/sorter.js +210 -0
  21. package/coverage/lcov-report/types.ts.html +331 -0
  22. package/coverage/lcov-report/utils.ts.html +325 -0
  23. package/coverage/lcov.info +200 -0
  24. package/coverage/prettify.css +1 -0
  25. package/coverage/prettify.js +2 -0
  26. package/coverage/schemas.ts.html +211 -0
  27. package/coverage/sort-arrow-sprite.png +0 -0
  28. package/coverage/sorter.js +210 -0
  29. package/coverage/types.ts.html +331 -0
  30. package/coverage/utils.ts.html +325 -0
  31. package/jest.config.cjs +32 -0
  32. package/package.json +50 -0
  33. package/src/AudioContentTypeManager.ts +238 -0
  34. package/src/index.ts +31 -0
  35. package/src/schemas.ts +42 -0
  36. package/src/types.ts +82 -0
  37. package/src/utils.ts +80 -0
  38. package/tsconfig.json +18 -0
@@ -0,0 +1,32 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ module.exports = {
10
+ preset: 'ts-jest',
11
+ testEnvironment: 'node',
12
+ roots: ['<rootDir>/__tests__'],
13
+ testMatch: ['**/__tests__/**/*.test.ts'],
14
+ collectCoverageFrom: [
15
+ 'src/**/*.ts',
16
+ '!src/**/*.d.ts',
17
+ '!src/index.ts'
18
+ ],
19
+ coverageThreshold: {
20
+ global: {
21
+ branches: 83,
22
+ functions: 90,
23
+ lines: 90,
24
+ statements: 90
25
+ }
26
+ },
27
+ coverageReporters: ['text', 'lcov', 'html'],
28
+ moduleFileExtensions: ['ts', 'js', 'json'],
29
+ transform: {
30
+ '^.+\\.ts$': 'ts-jest'
31
+ }
32
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@bernierllc/content-type-audio",
3
+ "version": "1.0.1",
4
+ "description": "Base audio content type with file upload, local storage, and streaming URL capabilities",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [
8
+ "audio",
9
+ "content-type",
10
+ "media",
11
+ "streaming",
12
+ "file-upload",
13
+ "mp3",
14
+ "wav",
15
+ "content-management"
16
+ ],
17
+ "author": "Bernier LLC",
18
+ "license": "SEE LICENSE IN LICENSE",
19
+ "dependencies": {
20
+ "@bernierllc/content-type-registry": "^1.0.3",
21
+ "zod": "^3.22.4"
22
+ },
23
+ "devDependencies": {
24
+ "@types/jest": "^29.5.0",
25
+ "@types/node": "^20.0.0",
26
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
27
+ "@typescript-eslint/parser": "^6.0.0",
28
+ "eslint": "^8.0.0",
29
+ "jest": "^29.0.0",
30
+ "ts-jest": "^29.0.0",
31
+ "typescript": "^5.0.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "bernierllc": {
37
+ "integration": {
38
+ "neverhub": "optional",
39
+ "logger": "planned"
40
+ }
41
+ },
42
+ "scripts": {
43
+ "build": "tsc",
44
+ "test": "jest",
45
+ "test:run": "jest --runInBand",
46
+ "test:coverage": "jest --coverage",
47
+ "lint": "eslint src __tests__ --ext .ts",
48
+ "clean": "rm -rf dist"
49
+ }
50
+ }
@@ -0,0 +1,238 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { z } from 'zod';
10
+ import {
11
+ ContentTypeRegistry,
12
+ BaseContentType,
13
+ type ContentTypeDefinition
14
+ } from '@bernierllc/content-type-registry';
15
+ import { AudioContentSchema } from './schemas';
16
+ import {
17
+ AudioFormat,
18
+ AudioContentTypeConfig,
19
+ AudioMetadata,
20
+ AudioContentType,
21
+ AudioValidationResult,
22
+ PackageResult
23
+ } from './types';
24
+ import {
25
+ parseDuration,
26
+ formatDuration,
27
+ calculateBitrate,
28
+ generateAudioId
29
+ } from './utils';
30
+
31
+ /**
32
+ * Audio content type manager
33
+ */
34
+ export class AudioContentTypeManager {
35
+ private config: AudioContentTypeConfig;
36
+
37
+ constructor(config?: Partial<AudioContentTypeConfig>) {
38
+ this.config = {
39
+ baseType: 'audio',
40
+ editor: 'file-upload-with-player',
41
+ storage: 'file',
42
+ publishing: 'streaming-url',
43
+ maxFileSize: 100 * 1024 * 1024, // 100MB
44
+ allowedFormats: Object.values(AudioFormat),
45
+ requireMetadata: false,
46
+ ...config
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Register audio content type with registry
52
+ */
53
+ register(registry: ContentTypeRegistry): void {
54
+ const definition: ContentTypeDefinition = {
55
+ id: 'audio',
56
+ name: 'Audio',
57
+ description: 'Base audio content type with file upload and streaming',
58
+ baseType: BaseContentType.AUDIO,
59
+ icon: '🎵',
60
+ schema: this.getSchema(),
61
+ editor: {
62
+ component: this.config.editor,
63
+ settings: {
64
+ maxFileSize: this.config.maxFileSize,
65
+ allowedFormats: this.config.allowedFormats,
66
+ requireMetadata: this.config.requireMetadata
67
+ }
68
+ },
69
+ storage: {
70
+ method: this.config.storage
71
+ },
72
+ publishing: {
73
+ methods: [this.config.publishing],
74
+ defaultMethod: this.config.publishing
75
+ },
76
+ metadata: {
77
+ version: '1.0.0',
78
+ author: 'Bernier LLC'
79
+ }
80
+ };
81
+
82
+ registry.register(definition);
83
+ }
84
+
85
+ /**
86
+ * Get Zod schema for audio content type
87
+ */
88
+ getSchema(): z.ZodSchema {
89
+ return AudioContentSchema;
90
+ }
91
+
92
+ /**
93
+ * Validate audio content
94
+ */
95
+ validate(content: unknown): AudioValidationResult {
96
+ try {
97
+ const schema = this.getSchema();
98
+ const validated = schema.parse(content) as AudioContentType;
99
+
100
+ // Additional validation
101
+ const errors: string[] = [];
102
+
103
+ // Check file size
104
+ if (this.config.maxFileSize && validated.metadata.size > this.config.maxFileSize) {
105
+ errors.push(
106
+ `File size ${validated.metadata.size} exceeds maximum ${this.config.maxFileSize}`
107
+ );
108
+ }
109
+
110
+ // Check allowed formats
111
+ if (
112
+ this.config.allowedFormats &&
113
+ !this.config.allowedFormats.includes(validated.metadata.mimeType)
114
+ ) {
115
+ errors.push(`Audio format ${validated.metadata.mimeType} not allowed`);
116
+ }
117
+
118
+ // Check metadata requirements
119
+ if (this.config.requireMetadata && !validated.metadata.title) {
120
+ errors.push('Title is required');
121
+ }
122
+
123
+ if (errors.length > 0) {
124
+ return {
125
+ valid: false,
126
+ errors,
127
+ metadata: validated.metadata
128
+ };
129
+ }
130
+
131
+ return {
132
+ valid: true,
133
+ metadata: validated.metadata
134
+ };
135
+ } catch (error) {
136
+ if (error instanceof z.ZodError) {
137
+ return {
138
+ valid: false,
139
+ errors: error.errors.map((e) => `${e.path.join('.')}: ${e.message}`)
140
+ };
141
+ }
142
+
143
+ return {
144
+ valid: false,
145
+ errors: [error instanceof Error ? error.message : 'Unknown validation error']
146
+ };
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Create audio content type instance
152
+ */
153
+ create(
154
+ metadata: AudioMetadata,
155
+ options?: {
156
+ filePath?: string;
157
+ fileUrl?: string;
158
+ streamingUrl?: string;
159
+ }
160
+ ): AudioContentType {
161
+ const content: AudioContentType = {
162
+ id: generateAudioId(),
163
+ type: 'audio',
164
+ metadata,
165
+ createdAt: new Date(),
166
+ updatedAt: new Date()
167
+ };
168
+
169
+ if (options?.filePath) {
170
+ content.filePath = options.filePath;
171
+ }
172
+ if (options?.fileUrl) {
173
+ content.fileUrl = options.fileUrl;
174
+ }
175
+ if (options?.streamingUrl) {
176
+ content.streamingUrl = options.streamingUrl;
177
+ }
178
+
179
+ return content;
180
+ }
181
+
182
+ /**
183
+ * Parse audio duration from various formats
184
+ */
185
+ parseDuration(duration: string | number): number {
186
+ return parseDuration(duration);
187
+ }
188
+
189
+ /**
190
+ * Format duration to human-readable string
191
+ */
192
+ formatDuration(seconds: number): string {
193
+ return formatDuration(seconds);
194
+ }
195
+
196
+ /**
197
+ * Calculate bitrate from file size and duration
198
+ */
199
+ calculateBitrate(fileSize: number, duration: number): number {
200
+ return calculateBitrate(fileSize, duration);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Wrapper for validation with error handling
206
+ */
207
+ export function validateAudio(
208
+ manager: AudioContentTypeManager,
209
+ content: unknown
210
+ ): PackageResult<AudioValidationResult> {
211
+ try {
212
+ const result = manager.validate(content);
213
+ if (result.valid) {
214
+ return {
215
+ success: true,
216
+ data: result
217
+ };
218
+ } else {
219
+ const errorMessage = result.errors?.join(', ');
220
+ if (errorMessage) {
221
+ return {
222
+ success: false,
223
+ data: result,
224
+ error: errorMessage
225
+ };
226
+ }
227
+ return {
228
+ success: false,
229
+ data: result
230
+ };
231
+ }
232
+ } catch (error) {
233
+ return {
234
+ success: false,
235
+ error: error instanceof Error ? error.message : 'Unknown error'
236
+ };
237
+ }
238
+ }
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ // Export types
10
+ export {
11
+ AudioFormat,
12
+ type AudioMetadata,
13
+ type AudioContentTypeConfig,
14
+ type AudioContentType,
15
+ type AudioValidationResult,
16
+ type PackageResult
17
+ } from './types';
18
+
19
+ // Export schemas
20
+ export { AudioContentSchema, type AudioContent } from './schemas';
21
+
22
+ // Export utilities
23
+ export {
24
+ parseDuration,
25
+ formatDuration,
26
+ calculateBitrate,
27
+ generateAudioId
28
+ } from './utils';
29
+
30
+ // Export manager
31
+ export { AudioContentTypeManager, validateAudio } from './AudioContentTypeManager';
package/src/schemas.ts ADDED
@@ -0,0 +1,42 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { z } from 'zod';
10
+ import { AudioFormat } from './types';
11
+
12
+ /**
13
+ * Zod schema for audio content type validation
14
+ */
15
+ export const AudioContentSchema = z.object({
16
+ id: z.string(),
17
+ type: z.literal('audio'),
18
+ metadata: z.object({
19
+ filename: z.string(),
20
+ mimeType: z.nativeEnum(AudioFormat),
21
+ size: z.number().positive(),
22
+ duration: z.number().nonnegative(),
23
+ bitrate: z.number().positive(),
24
+ sampleRate: z.number().positive().optional(),
25
+ channels: z.number().int().min(1).max(8).optional(),
26
+ title: z.string().optional(),
27
+ artist: z.string().optional(),
28
+ album: z.string().optional(),
29
+ year: z.number().int().min(1900).max(2100).optional(),
30
+ coverArt: z.string().optional()
31
+ }),
32
+ filePath: z.string().optional(),
33
+ fileUrl: z.string().url().optional(),
34
+ streamingUrl: z.string().url().optional(),
35
+ createdAt: z.date(),
36
+ updatedAt: z.date()
37
+ });
38
+
39
+ /**
40
+ * Type inferred from schema
41
+ */
42
+ export type AudioContent = z.infer<typeof AudioContentSchema>;
package/src/types.ts ADDED
@@ -0,0 +1,82 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ /**
10
+ * Audio format enumeration
11
+ */
12
+ export enum AudioFormat {
13
+ MP3 = 'audio/mpeg',
14
+ WAV = 'audio/wav',
15
+ AAC = 'audio/aac',
16
+ OGG = 'audio/ogg',
17
+ FLAC = 'audio/flac',
18
+ M4A = 'audio/mp4'
19
+ }
20
+
21
+ /**
22
+ * Audio metadata interface
23
+ */
24
+ export interface AudioMetadata {
25
+ filename: string;
26
+ mimeType: AudioFormat;
27
+ size: number; // bytes
28
+ duration: number; // seconds
29
+ bitrate: number; // kbps
30
+ sampleRate?: number; // Hz
31
+ channels?: number; // 1 = mono, 2 = stereo
32
+ title?: string;
33
+ artist?: string;
34
+ album?: string;
35
+ year?: number;
36
+ coverArt?: string; // URL or base64
37
+ }
38
+
39
+ /**
40
+ * Audio content type configuration
41
+ */
42
+ export interface AudioContentTypeConfig {
43
+ baseType: 'audio';
44
+ editor: 'file-upload-with-player';
45
+ storage: 'file' | 'cloud' | 'custom';
46
+ publishing: 'streaming-url' | 'download' | 'custom';
47
+ maxFileSize?: number; // bytes, default: 100MB
48
+ allowedFormats?: AudioFormat[]; // default: all formats
49
+ requireMetadata?: boolean; // default: false
50
+ }
51
+
52
+ /**
53
+ * Audio content type interface
54
+ */
55
+ export interface AudioContentType {
56
+ id: string;
57
+ type: 'audio';
58
+ metadata: AudioMetadata;
59
+ filePath?: string; // for local storage
60
+ fileUrl?: string; // for cloud storage
61
+ streamingUrl?: string; // for publishing
62
+ createdAt: Date;
63
+ updatedAt: Date;
64
+ }
65
+
66
+ /**
67
+ * Audio validation result
68
+ */
69
+ export interface AudioValidationResult {
70
+ valid: boolean;
71
+ errors?: string[];
72
+ metadata?: Partial<AudioMetadata>;
73
+ }
74
+
75
+ /**
76
+ * Package result interface for error handling
77
+ */
78
+ export interface PackageResult<T = unknown> {
79
+ success: boolean;
80
+ data?: T;
81
+ error?: string;
82
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,80 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ /**
10
+ * Parse audio duration from various formats
11
+ * @param duration - Duration as string ("1:23:45", "23:45", "123") or number
12
+ * @returns Duration in seconds
13
+ */
14
+ export function parseDuration(duration: string | number): number {
15
+ if (typeof duration === 'number') {
16
+ return duration;
17
+ }
18
+
19
+ // Parse formats: "1:23:45", "23:45", "123"
20
+ const parts = duration.split(':').map(Number);
21
+
22
+ // Check if all parts are valid numbers
23
+ if (parts.some(part => isNaN(part))) {
24
+ return 0;
25
+ }
26
+
27
+ if (parts.length === 1) {
28
+ const part = parts[0];
29
+ return part !== undefined ? part : 0; // seconds
30
+ } else if (parts.length === 2) {
31
+ const part0 = parts[0];
32
+ const part1 = parts[1];
33
+ return (part0 !== undefined && part1 !== undefined) ? part0 * 60 + part1 : 0; // mm:ss
34
+ } else if (parts.length === 3) {
35
+ const part0 = parts[0];
36
+ const part1 = parts[1];
37
+ const part2 = parts[2];
38
+ return (part0 !== undefined && part1 !== undefined && part2 !== undefined)
39
+ ? part0 * 3600 + part1 * 60 + part2
40
+ : 0; // hh:mm:ss
41
+ }
42
+
43
+ return 0;
44
+ }
45
+
46
+ /**
47
+ * Format duration to human-readable string
48
+ * @param seconds - Duration in seconds
49
+ * @returns Formatted string (e.g., "1:23:45" or "23:45")
50
+ */
51
+ export function formatDuration(seconds: number): string {
52
+ const hours = Math.floor(seconds / 3600);
53
+ const minutes = Math.floor((seconds % 3600) / 60);
54
+ const secs = Math.floor(seconds % 60);
55
+
56
+ if (hours > 0) {
57
+ return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
58
+ }
59
+ return `${minutes}:${String(secs).padStart(2, '0')}`;
60
+ }
61
+
62
+ /**
63
+ * Calculate bitrate from file size and duration
64
+ * @param fileSize - File size in bytes
65
+ * @param duration - Duration in seconds
66
+ * @returns Bitrate in kbps
67
+ */
68
+ export function calculateBitrate(fileSize: number, duration: number): number {
69
+ if (duration === 0) return 0;
70
+ // (fileSize in bytes * 8 bits/byte) / (duration in seconds) / 1000 = kbps
71
+ return Math.round((fileSize * 8) / duration / 1000);
72
+ }
73
+
74
+ /**
75
+ * Generate unique ID for audio content
76
+ * @returns Unique identifier
77
+ */
78
+ export function generateAudioId(): string {
79
+ return `audio_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
80
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true
9
+ },
10
+ "include": [
11
+ "src/**/*"
12
+ ],
13
+ "exclude": [
14
+ "node_modules",
15
+ "dist",
16
+ "**/*.test.ts"
17
+ ]
18
+ }