@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,279 @@
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
+ });
@@ -0,0 +1,142 @@
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
+ });