@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.
- package/dist/AudioContentTypeManager.js +1 -0
- package/dist/AudioContentTypeManager.js.map +1 -1
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/.eslintrc.js +0 -42
- package/CHANGELOG.md +0 -72
- package/__tests__/AudioContentTypeManager.test.ts +0 -592
- package/__tests__/integration.test.ts +0 -279
- package/__tests__/utils.test.ts +0 -142
- package/jest.config.cjs +0 -32
- package/src/AudioContentTypeManager.ts +0 -238
- package/src/index.ts +0 -31
- package/src/schemas.ts +0 -42
- package/src/types.ts +0 -82
- package/src/utils.ts +0 -80
- package/tsconfig.json +0 -19
|
@@ -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
|
-
});
|
package/__tests__/utils.test.ts
DELETED
|
@@ -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
|
-
}
|