@bernierllc/content-type-audio 1.2.2 → 1.5.0

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.
@@ -1,279 +0,0 @@
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 { AudioContentTypeManager } from '../src/AudioContentTypeManager';
10
- import { AudioFormat } from '../src/types';
11
- import { ContentTypeRegistry } from '@bernierllc/content-type-registry';
12
-
13
- describe('Integration Tests', () => {
14
- describe('AudioContentTypeManager with ContentTypeRegistry', () => {
15
- it('should integrate with content type registry', () => {
16
- const registry = new ContentTypeRegistry();
17
- const manager = new AudioContentTypeManager();
18
-
19
- manager.register(registry);
20
-
21
- const result = registry.get('audio');
22
- expect(result.success).toBe(true);
23
- expect(result.data).toBeDefined();
24
- expect(result.data?.id).toBe('audio');
25
- expect(result.data?.name).toBe('Audio');
26
- });
27
-
28
- it('should find registered audio type by filter', () => {
29
- const registry = new ContentTypeRegistry();
30
- const manager = new AudioContentTypeManager();
31
-
32
- manager.register(registry);
33
-
34
- const result = registry.list({ baseType: 'audio' });
35
- expect(result.success).toBe(true);
36
- expect(result.data).toBeDefined();
37
- expect(result.data).toHaveLength(1);
38
- expect(result.data?.[0].id).toBe('audio');
39
- });
40
-
41
- it('should validate content using registered schema', () => {
42
- const registry = new ContentTypeRegistry();
43
- const manager = new AudioContentTypeManager();
44
-
45
- manager.register(registry);
46
-
47
- const content = manager.create({
48
- filename: 'test.mp3',
49
- mimeType: AudioFormat.MP3,
50
- size: 1024 * 1024,
51
- duration: 60,
52
- bitrate: 128
53
- });
54
-
55
- const validation = manager.validate(content);
56
- expect(validation.valid).toBe(true);
57
- });
58
- });
59
-
60
- describe('End-to-end workflow', () => {
61
- it('should handle complete audio content lifecycle', async () => {
62
- // 1. Create manager
63
- const manager = new AudioContentTypeManager({
64
- maxFileSize: 10 * 1024 * 1024,
65
- allowedFormats: [AudioFormat.MP3, AudioFormat.WAV],
66
- requireMetadata: false
67
- });
68
-
69
- // 2. Register with registry
70
- const registry = new ContentTypeRegistry();
71
- manager.register(registry);
72
-
73
- // 3. Create audio content
74
- const audioContent = manager.create(
75
- {
76
- filename: 'podcast-episode.mp3',
77
- mimeType: AudioFormat.MP3,
78
- size: 5 * 1024 * 1024,
79
- duration: 1800, // 30 minutes
80
- bitrate: 192,
81
- title: 'My Podcast Episode',
82
- artist: 'Podcast Host'
83
- },
84
- {
85
- filePath: '/storage/audio/podcast-episode.mp3',
86
- streamingUrl: 'https://cdn.example.com/podcast-episode.mp3'
87
- }
88
- );
89
-
90
- // 4. Validate content
91
- const validation = manager.validate(audioContent);
92
- expect(validation.valid).toBe(true);
93
-
94
- // 5. Verify content structure
95
- expect(audioContent.id).toBeDefined();
96
- expect(audioContent.type).toBe('audio');
97
- expect(audioContent.metadata.title).toBe('My Podcast Episode');
98
- expect(audioContent.filePath).toBe('/storage/audio/podcast-episode.mp3');
99
- expect(audioContent.streamingUrl).toBe('https://cdn.example.com/podcast-episode.mp3');
100
- });
101
-
102
- it('should handle multiple audio formats', async () => {
103
- const manager = new AudioContentTypeManager();
104
- const registry = new ContentTypeRegistry();
105
- manager.register(registry);
106
-
107
- const formats = [
108
- AudioFormat.MP3,
109
- AudioFormat.WAV,
110
- AudioFormat.AAC,
111
- AudioFormat.OGG,
112
- AudioFormat.FLAC,
113
- AudioFormat.M4A
114
- ];
115
-
116
- for (const format of formats) {
117
- const content = manager.create({
118
- filename: `test.${format.split('/')[1]}`,
119
- mimeType: format,
120
- size: 1024 * 1024,
121
- duration: 60,
122
- bitrate: 128
123
- });
124
-
125
- const validation = manager.validate(content);
126
- expect(validation.valid).toBe(true);
127
- }
128
- });
129
-
130
- it('should calculate accurate bitrate for real file scenarios', () => {
131
- const manager = new AudioContentTypeManager();
132
-
133
- // Scenario 1: MP3 at 128kbps, 3 minutes
134
- const mp3Duration = 180; // 3 minutes
135
- const mp3Size = Math.round((128 * 1000 * mp3Duration) / 8); // bytes
136
- const mp3Bitrate = manager.calculateBitrate(mp3Size, mp3Duration);
137
- expect(mp3Bitrate).toBeCloseTo(128, 1);
138
-
139
- // Scenario 2: High quality WAV, 5 minutes
140
- const wavDuration = 300; // 5 minutes
141
- const wavSize = Math.round((1411 * 1000 * wavDuration) / 8); // CD quality
142
- const wavBitrate = manager.calculateBitrate(wavSize, wavDuration);
143
- expect(wavBitrate).toBeCloseTo(1411, 1);
144
- });
145
-
146
- it('should format and parse durations consistently', () => {
147
- const manager = new AudioContentTypeManager();
148
-
149
- const durations = [30, 90, 180, 1800, 3600, 5025];
150
-
151
- for (const duration of durations) {
152
- const formatted = manager.formatDuration(duration);
153
- const parsed = manager.parseDuration(formatted);
154
- expect(parsed).toBe(duration);
155
- }
156
- });
157
- });
158
-
159
- describe('Error handling', () => {
160
- it('should handle validation errors gracefully', async () => {
161
- const manager = new AudioContentTypeManager({
162
- maxFileSize: 1024 * 1024, // 1MB
163
- requireMetadata: true
164
- });
165
-
166
- const content = manager.create({
167
- filename: 'large.mp3',
168
- mimeType: AudioFormat.MP3,
169
- size: 10 * 1024 * 1024, // 10MB (exceeds limit)
170
- duration: 600,
171
- bitrate: 192
172
- // Missing title (required)
173
- });
174
-
175
- const validation = manager.validate(content);
176
-
177
- expect(validation.valid).toBe(false);
178
- expect(validation.errors).toBeDefined();
179
- expect(validation.errors?.length).toBeGreaterThan(0);
180
- });
181
-
182
- it('should handle multiple validation errors', async () => {
183
- const manager = new AudioContentTypeManager({
184
- maxFileSize: 1024 * 1024,
185
- allowedFormats: [AudioFormat.MP3],
186
- requireMetadata: true
187
- });
188
-
189
- const content = manager.create({
190
- filename: 'test.flac',
191
- mimeType: AudioFormat.FLAC, // Not allowed
192
- size: 10 * 1024 * 1024, // Too large
193
- duration: 600,
194
- bitrate: 192
195
- // Missing title
196
- });
197
-
198
- const validation = manager.validate(content);
199
-
200
- expect(validation.valid).toBe(false);
201
- expect(validation.errors).toBeDefined();
202
- expect(validation.errors?.length).toBeGreaterThanOrEqual(2);
203
- });
204
- });
205
-
206
- describe('Configuration scenarios', () => {
207
- it('should work with minimal configuration', async () => {
208
- const manager = new AudioContentTypeManager();
209
- const registry = new ContentTypeRegistry();
210
-
211
- manager.register(registry);
212
-
213
- const content = manager.create({
214
- filename: 'test.mp3',
215
- mimeType: AudioFormat.MP3,
216
- size: 1024 * 1024,
217
- duration: 60,
218
- bitrate: 128
219
- });
220
-
221
- const validation = manager.validate(content);
222
- expect(validation.valid).toBe(true);
223
- });
224
-
225
- it('should work with restrictive configuration', async () => {
226
- const manager = new AudioContentTypeManager({
227
- maxFileSize: 512 * 1024, // 512KB
228
- allowedFormats: [AudioFormat.MP3],
229
- requireMetadata: true
230
- });
231
-
232
- // Valid content
233
- const validContent = manager.create({
234
- filename: 'small.mp3',
235
- mimeType: AudioFormat.MP3,
236
- size: 256 * 1024,
237
- duration: 30,
238
- bitrate: 64,
239
- title: 'Small Audio'
240
- });
241
-
242
- const validation = manager.validate(validContent);
243
- expect(validation.valid).toBe(true);
244
-
245
- // Invalid content (too large)
246
- const invalidContent = manager.create({
247
- filename: 'large.mp3',
248
- mimeType: AudioFormat.MP3,
249
- size: 1024 * 1024,
250
- duration: 120,
251
- bitrate: 128,
252
- title: 'Large Audio'
253
- });
254
-
255
- const invalidValidation = manager.validate(invalidContent);
256
- expect(invalidValidation.valid).toBe(false);
257
- });
258
-
259
- it('should work with permissive configuration', async () => {
260
- const manager = new AudioContentTypeManager({
261
- maxFileSize: 500 * 1024 * 1024, // 500MB
262
- allowedFormats: Object.values(AudioFormat),
263
- requireMetadata: false
264
- });
265
-
266
- // Very large file
267
- const content = manager.create({
268
- filename: 'huge.flac',
269
- mimeType: AudioFormat.FLAC,
270
- size: 100 * 1024 * 1024,
271
- duration: 3600,
272
- bitrate: 1411
273
- });
274
-
275
- const validation = manager.validate(content);
276
- expect(validation.valid).toBe(true);
277
- });
278
- });
279
- });
@@ -1,142 +0,0 @@
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 {
10
- parseDuration,
11
- formatDuration,
12
- calculateBitrate,
13
- generateAudioId
14
- } from '../src/utils';
15
-
16
- describe('Audio Utilities', () => {
17
- describe('parseDuration', () => {
18
- it('should parse duration from seconds number', () => {
19
- expect(parseDuration(123)).toBe(123);
20
- expect(parseDuration(0)).toBe(0);
21
- expect(parseDuration(3600)).toBe(3600);
22
- });
23
-
24
- it('should parse duration from seconds string', () => {
25
- expect(parseDuration('123')).toBe(123);
26
- expect(parseDuration('0')).toBe(0);
27
- expect(parseDuration('3600')).toBe(3600);
28
- });
29
-
30
- it('should parse duration from mm:ss format', () => {
31
- expect(parseDuration('1:30')).toBe(90); // 1m 30s
32
- expect(parseDuration('23:45')).toBe(1425); // 23m 45s
33
- expect(parseDuration('0:00')).toBe(0);
34
- expect(parseDuration('59:59')).toBe(3599);
35
- });
36
-
37
- it('should parse duration from hh:mm:ss format', () => {
38
- expect(parseDuration('1:23:45')).toBe(5025); // 1h 23m 45s
39
- expect(parseDuration('0:23:45')).toBe(1425); // 23m 45s
40
- expect(parseDuration('2:00:00')).toBe(7200); // 2h
41
- expect(parseDuration('0:00:00')).toBe(0);
42
- });
43
-
44
- it('should handle invalid formats', () => {
45
- expect(parseDuration('')).toBe(0);
46
- expect(parseDuration('invalid')).toBe(0);
47
- });
48
- });
49
-
50
- describe('formatDuration', () => {
51
- it('should format duration under 1 hour as mm:ss', () => {
52
- expect(formatDuration(90)).toBe('1:30');
53
- expect(formatDuration(1425)).toBe('23:45');
54
- expect(formatDuration(0)).toBe('0:00');
55
- expect(formatDuration(59)).toBe('0:59');
56
- expect(formatDuration(3599)).toBe('59:59');
57
- });
58
-
59
- it('should format duration over 1 hour as hh:mm:ss', () => {
60
- expect(formatDuration(5025)).toBe('1:23:45');
61
- expect(formatDuration(3600)).toBe('1:00:00');
62
- expect(formatDuration(7200)).toBe('2:00:00');
63
- expect(formatDuration(86400)).toBe('24:00:00');
64
- });
65
-
66
- it('should handle edge cases', () => {
67
- expect(formatDuration(0)).toBe('0:00');
68
- expect(formatDuration(1)).toBe('0:01');
69
- expect(formatDuration(60)).toBe('1:00');
70
- expect(formatDuration(3599.9)).toBe('59:59'); // Just under 1 hour
71
- expect(formatDuration(3600.1)).toBe('1:00:00'); // Just over 1 hour
72
- });
73
-
74
- it('should handle floating point seconds', () => {
75
- expect(formatDuration(90.5)).toBe('1:30');
76
- expect(formatDuration(59.9)).toBe('0:59');
77
- });
78
-
79
- it('should pad single digit values', () => {
80
- expect(formatDuration(65)).toBe('1:05');
81
- expect(formatDuration(3665)).toBe('1:01:05');
82
- });
83
- });
84
-
85
- describe('calculateBitrate', () => {
86
- it('should calculate bitrate correctly', () => {
87
- const fileSize = 5 * 1024 * 1024; // 5MB
88
- const duration = 180; // 3 minutes
89
-
90
- const bitrate = calculateBitrate(fileSize, duration);
91
-
92
- expect(bitrate).toBeGreaterThan(0);
93
- expect(bitrate).toBeCloseTo(233, 0); // ~233 kbps
94
- });
95
-
96
- it('should handle different file sizes and durations', () => {
97
- expect(calculateBitrate(1024 * 1024, 60)).toBe(140); // 1MB, 1min = ~140kbps
98
- expect(calculateBitrate(10 * 1024 * 1024, 300)).toBe(280); // 10MB, 5min = ~280kbps
99
- });
100
-
101
- it('should return 0 for zero duration', () => {
102
- expect(calculateBitrate(1024 * 1024, 0)).toBe(0);
103
- });
104
-
105
- it('should handle very small files', () => {
106
- expect(calculateBitrate(1024, 60)).toBe(0); // Rounds to 0
107
- });
108
-
109
- it('should handle very large files', () => {
110
- const bitrate = calculateBitrate(100 * 1024 * 1024, 300); // 100MB, 5min
111
- expect(bitrate).toBeGreaterThan(0);
112
- expect(bitrate).toBeCloseTo(2796, 0);
113
- });
114
- });
115
-
116
- describe('generateAudioId', () => {
117
- it('should generate unique IDs', () => {
118
- const id1 = generateAudioId();
119
- const id2 = generateAudioId();
120
-
121
- expect(id1).not.toBe(id2);
122
- });
123
-
124
- it('should start with audio_ prefix', () => {
125
- const id = generateAudioId();
126
- expect(id).toMatch(/^audio_/);
127
- });
128
-
129
- it('should have correct format', () => {
130
- const id = generateAudioId();
131
- expect(id).toMatch(/^audio_\d+_[a-z0-9]+$/);
132
- });
133
-
134
- it('should generate multiple unique IDs', () => {
135
- const ids = new Set();
136
- for (let i = 0; i < 100; i++) {
137
- ids.add(generateAudioId());
138
- }
139
- expect(ids.size).toBe(100);
140
- });
141
- });
142
- });
package/jest.config.cjs DELETED
@@ -1,32 +0,0 @@
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
- // Use fixed environment to handle Node.js v25+ localStorage issue
12
- testEnvironment: '<rootDir>/../../../jest-environment-node-fixed.cjs',
13
- roots: ['<rootDir>/__tests__'],
14
- testMatch: ['**/__tests__/**/*.test.ts'],
15
- collectCoverageFrom: [
16
- 'src/**/*.ts',
17
- '!src/**/*.d.ts',
18
- '!src/index.ts'
19
- ],
20
- coverageThreshold: {
21
- global: {
22
- branches: 83,
23
- functions: 90,
24
- lines: 90,
25
- statements: 90
26
- }
27
- },
28
- moduleFileExtensions: ['ts', 'js', 'json'],
29
- transform: {
30
- '^.+\\.ts$': 'ts-jest'
31
- }
32
- };
@@ -1,238 +0,0 @@
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
- }