@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.
- package/.eslintrc.js +42 -0
- package/README.md +500 -0
- package/__tests__/AudioContentTypeManager.test.ts +592 -0
- package/__tests__/integration.test.ts +279 -0
- package/__tests__/utils.test.ts +142 -0
- package/coverage/AudioContentTypeManager.ts.html +799 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +161 -0
- package/coverage/lcov-report/AudioContentTypeManager.ts.html +799 -0
- package/coverage/lcov-report/base.css +224 -0
- package/coverage/lcov-report/block-navigation.js +87 -0
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +161 -0
- package/coverage/lcov-report/prettify.css +1 -0
- package/coverage/lcov-report/prettify.js +2 -0
- package/coverage/lcov-report/schemas.ts.html +211 -0
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -0
- package/coverage/lcov-report/types.ts.html +331 -0
- package/coverage/lcov-report/utils.ts.html +325 -0
- package/coverage/lcov.info +200 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/schemas.ts.html +211 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/types.ts.html +331 -0
- package/coverage/utils.ts.html +325 -0
- package/jest.config.cjs +32 -0
- package/package.json +50 -0
- package/src/AudioContentTypeManager.ts +238 -0
- package/src/index.ts +31 -0
- package/src/schemas.ts +42 -0
- package/src/types.ts +82 -0
- package/src/utils.ts +80 -0
- package/tsconfig.json +18 -0
package/jest.config.cjs
ADDED
|
@@ -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
|
+
}
|