@bernierllc/content-type-video 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/VideoContentType.js +4 -2
- package/dist/VideoContentType.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 +3 -1
- package/dist/index.js.map +1 -1
- package/dist/storage/LocalVideoStorage.js +2 -0
- package/dist/storage/LocalVideoStorage.js.map +1 -1
- package/package.json +6 -2
- package/.eslintrc.js +0 -26
- package/CHANGELOG.md +0 -72
- package/__tests__/VideoContentType.test.ts +0 -522
- package/jest.config.cjs +0 -32
- package/src/VideoContentType.ts +0 -142
- package/src/config.ts +0 -63
- package/src/index.ts +0 -14
- package/src/storage/LocalVideoStorage.ts +0 -134
- package/src/storage/VideoStorage.ts +0 -20
- package/src/types.ts +0 -99
- package/tsconfig.json +0 -20
package/dist/VideoContentType.js
CHANGED
|
@@ -51,9 +51,11 @@ const config_1 = require("./config");
|
|
|
51
51
|
* Video content type with file upload, local storage, and streaming URL generation
|
|
52
52
|
*/
|
|
53
53
|
class VideoContentType {
|
|
54
|
+
typeName = 'video';
|
|
55
|
+
version = '1.0.0';
|
|
56
|
+
config;
|
|
57
|
+
storage;
|
|
54
58
|
constructor(config = {}) {
|
|
55
|
-
this.typeName = 'video';
|
|
56
|
-
this.version = '1.0.0';
|
|
57
59
|
this.config = (0, config_1.loadConfig)(config);
|
|
58
60
|
this.storage = new LocalVideoStorage_1.LocalVideoStorage(this.config);
|
|
59
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoContentType.js","sourceRoot":"","sources":["../src/VideoContentType.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEF,6EAAyF;AACzF,2CAA6B;AAC7B,uCAAyB;AACzB,mCAKiB;AAEjB,mEAAgE;AAChE,qCAAsC;AAEtC;;GAEG;AACH,MAAa,gBAAgB;
|
|
1
|
+
{"version":3,"file":"VideoContentType.js","sourceRoot":"","sources":["../src/VideoContentType.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type VideoContentTypeErrorCode = 'VIDEO_CONTENT_TYPE_ERROR' | 'INVALID_INPUT' | 'OPERATION_FAILED';
|
|
2
|
+
export interface VideoContentTypeErrorOptions {
|
|
3
|
+
cause?: Error;
|
|
4
|
+
code?: VideoContentTypeErrorCode;
|
|
5
|
+
context?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
export declare class VideoContentTypeError extends Error {
|
|
8
|
+
readonly code: VideoContentTypeErrorCode;
|
|
9
|
+
readonly context?: Record<string, unknown>;
|
|
10
|
+
constructor(message: string, options?: VideoContentTypeErrorOptions);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,yBAAyB,GACjC,0BAA0B,GAC1B,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,WAAW,4BAA4B;IAC3C,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,IAAI,CAAC,EAAE,yBAAyB,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAE/B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,4BAA4B;CASpE"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Copyright (c) 2025 Bernier LLC
|
|
4
|
+
|
|
5
|
+
This file is licensed to the client under a limited-use license.
|
|
6
|
+
The client may use and modify this code only within the scope of the project it was delivered for.
|
|
7
|
+
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.VideoContentTypeError = void 0;
|
|
11
|
+
class VideoContentTypeError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
context;
|
|
14
|
+
constructor(message, options) {
|
|
15
|
+
super(message, options?.cause ? { cause: options.cause } : undefined);
|
|
16
|
+
this.name = 'VideoContentTypeError';
|
|
17
|
+
this.code = options?.code ?? 'VIDEO_CONTENT_TYPE_ERROR';
|
|
18
|
+
if (options?.context !== undefined) {
|
|
19
|
+
this.context = options.context;
|
|
20
|
+
}
|
|
21
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.VideoContentTypeError = VideoContentTypeError;
|
|
25
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;AAaF,MAAa,qBAAsB,SAAQ,KAAK;IACrC,IAAI,CAA4B;IAChC,OAAO,CAA2B;IAE3C,YAAY,OAAe,EAAE,OAAsC;QACjE,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACtE,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;QACpC,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,IAAI,0BAA0B,CAAC;QACxD,IAAI,OAAO,EAAE,OAAO,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QACjC,CAAC;QACD,KAAK,CAAC,iBAAiB,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IACpD,CAAC;CACF;AAbD,sDAaC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export * from './config';
|
|
|
4
4
|
export * from './storage/VideoStorage';
|
|
5
5
|
export * from './storage/LocalVideoStorage';
|
|
6
6
|
export { VideoContentType } from './VideoContentType';
|
|
7
|
+
export { VideoContentTypeError, type VideoContentTypeErrorCode, type VideoContentTypeErrorOptions, } from './errors';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,KAAK,yBAAyB,EAC9B,KAAK,4BAA4B,GAClC,MAAM,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
21
21
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.VideoContentType = void 0;
|
|
24
|
+
exports.VideoContentTypeError = exports.VideoContentType = void 0;
|
|
25
25
|
__exportStar(require("./types"), exports);
|
|
26
26
|
__exportStar(require("./VideoContentType"), exports);
|
|
27
27
|
__exportStar(require("./config"), exports);
|
|
@@ -29,4 +29,6 @@ __exportStar(require("./storage/VideoStorage"), exports);
|
|
|
29
29
|
__exportStar(require("./storage/LocalVideoStorage"), exports);
|
|
30
30
|
var VideoContentType_1 = require("./VideoContentType");
|
|
31
31
|
Object.defineProperty(exports, "VideoContentType", { enumerable: true, get: function () { return VideoContentType_1.VideoContentType; } });
|
|
32
|
+
var errors_1 = require("./errors");
|
|
33
|
+
Object.defineProperty(exports, "VideoContentTypeError", { enumerable: true, get: function () { return errors_1.VideoContentTypeError; } });
|
|
32
34
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;;;;;;;;;;;;;;;AAEF,0CAAwB;AACxB,qDAAmC;AACnC,2CAAyB;AACzB,yDAAuC;AACvC,8DAA4C;AAC5C,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;;;;;;;;;;;;;;;AAEF,0CAAwB;AACxB,qDAAmC;AACnC,2CAAyB;AACzB,yDAAuC;AACvC,8DAA4C;AAC5C,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,mCAIkB;AAHhB,+GAAA,qBAAqB,OAAA"}
|
|
@@ -49,6 +49,8 @@ const types_1 = require("../types");
|
|
|
49
49
|
* Local file system storage implementation for videos
|
|
50
50
|
*/
|
|
51
51
|
class LocalVideoStorage {
|
|
52
|
+
storageBasePath;
|
|
53
|
+
streamingBaseUrl;
|
|
52
54
|
constructor(config) {
|
|
53
55
|
this.storageBasePath = config.uploadOptions?.storageBasePath || './storage/videos';
|
|
54
56
|
this.streamingBaseUrl = config.streamingBaseUrl || '/api/video/stream';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalVideoStorage.js","sourceRoot":"","sources":["../../src/storage/LocalVideoStorage.ts"],"names":[],"mappings":";AAAA;;;;;;EAME;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEF,gDAAkC;AAClC,2CAA6B;AAC7B,2CAA6B;AAE7B,oCAA4H;AAE5H;;GAEG;AACH,MAAa,iBAAiB;
|
|
1
|
+
{"version":3,"file":"LocalVideoStorage.js","sourceRoot":"","sources":["../../src/storage/LocalVideoStorage.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bernierllc/content-type-video",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Base video content type with file upload, local storage, and streaming URL generation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
7
11
|
"keywords": [
|
|
8
12
|
"content-type",
|
|
9
13
|
"video",
|
|
@@ -18,7 +22,7 @@
|
|
|
18
22
|
"license": "SEE LICENSE IN LICENSE",
|
|
19
23
|
"dependencies": {
|
|
20
24
|
"zod": "^3.22.0",
|
|
21
|
-
"@bernierllc/content-type-registry": "1.
|
|
25
|
+
"@bernierllc/content-type-registry": "1.6.0"
|
|
22
26
|
},
|
|
23
27
|
"devDependencies": {
|
|
24
28
|
"@types/jest": "^29.0.0",
|
package/.eslintrc.js
DELETED
|
@@ -1,26 +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
|
-
parser: '@typescript-eslint/parser',
|
|
11
|
-
extends: [
|
|
12
|
-
'eslint:recommended',
|
|
13
|
-
'plugin:@typescript-eslint/recommended'
|
|
14
|
-
],
|
|
15
|
-
parserOptions: {
|
|
16
|
-
ecmaVersion: 2020,
|
|
17
|
-
sourceType: 'module',
|
|
18
|
-
project: './tsconfig.json'
|
|
19
|
-
},
|
|
20
|
-
rules: {
|
|
21
|
-
'@typescript-eslint/no-explicit-any': 'error',
|
|
22
|
-
'@typescript-eslint/explicit-function-return-type': 'warn',
|
|
23
|
-
'@typescript-eslint/no-unused-vars': 'error',
|
|
24
|
-
'no-console': 'off'
|
|
25
|
-
}
|
|
26
|
-
};
|
package/CHANGELOG.md
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# Change Log
|
|
2
|
-
|
|
3
|
-
All notable changes to this project will be documented in this file.
|
|
4
|
-
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
-
|
|
6
|
-
## [1.2.2](https://github.com/bernierllc/tools/compare/@bernierllc/content-type-video@1.2.1...@bernierllc/content-type-video@1.2.2) (2026-03-09)
|
|
7
|
-
|
|
8
|
-
**Note:** Version bump only for package @bernierllc/content-type-video
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## [1.2.1](https://github.com/bernierllc/tools/compare/@bernierllc/content-type-video@1.2.0...@bernierllc/content-type-video@1.2.1) (2026-03-03)
|
|
15
|
-
|
|
16
|
-
**Note:** Version bump only for package @bernierllc/content-type-video
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# 1.2.0 (2025-12-25)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
### Bug Fixes
|
|
26
|
-
|
|
27
|
-
* **ci:** add lerna version step before publish ([3d20300](https://github.com/bernierllc/tools/commit/3d203002143bf353fffafe4f8a78a99009567347))
|
|
28
|
-
* **ci:** correct jest testEnvironment paths for CI ([c47144c](https://github.com/bernierllc/tools/commit/c47144c3d78aec2be1e1454066c235c2227823fd))
|
|
29
|
-
* **ci:** fix release workflow + convert internal deps to workspace:* ([01c078b](https://github.com/bernierllc/tools/commit/01c078b49d6025f7eef750f79207a1c71c8d85dc))
|
|
30
|
-
* **jest:** resolve Node.js v25+ localStorage SecurityError ([8ffc512](https://github.com/bernierllc/tools/commit/8ffc512805519bc5ba523bdbcd1c457e84998c9e))
|
|
31
|
-
* **route-discoverer:** Fix failing tests and achieve 90%+ test coverage ([9e5c431](https://github.com/bernierllc/tools/commit/9e5c4314bd7bfcbd38720d523b34c134473102e7))
|
|
32
|
-
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
### Features
|
|
36
|
-
|
|
37
|
-
* autonomous pipeline builds 11 packages for content-management-suite ([d2931b5](https://github.com/bernierllc/tools/commit/d2931b5ea5de075268b9c8f078db959113a48fb7))
|
|
38
|
-
* **content-type-video:** implement complete video content type with 100% test coverage ([9e4849e](https://github.com/bernierllc/tools/commit/9e4849ed3e5febdd6aafdf0fb14e6f97a06f2430))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# 1.1.0 (2025-12-25)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
### Bug Fixes
|
|
48
|
-
|
|
49
|
-
* **ci:** correct jest testEnvironment paths for CI ([c47144c](https://github.com/bernierllc/tools/commit/c47144c3d78aec2be1e1454066c235c2227823fd))
|
|
50
|
-
* **ci:** fix release workflow + convert internal deps to workspace:* ([01c078b](https://github.com/bernierllc/tools/commit/01c078b49d6025f7eef750f79207a1c71c8d85dc))
|
|
51
|
-
* **jest:** resolve Node.js v25+ localStorage SecurityError ([8ffc512](https://github.com/bernierllc/tools/commit/8ffc512805519bc5ba523bdbcd1c457e84998c9e))
|
|
52
|
-
* **route-discoverer:** Fix failing tests and achieve 90%+ test coverage ([9e5c431](https://github.com/bernierllc/tools/commit/9e5c4314bd7bfcbd38720d523b34c134473102e7))
|
|
53
|
-
* update neverhub-adapter deps to workspace:* and publish 0.1.2 ([f0e3d04](https://github.com/bernierllc/tools/commit/f0e3d04d8d4f094e3bb899ddf81e93243d16e2c2))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### Features
|
|
57
|
-
|
|
58
|
-
* autonomous pipeline builds 11 packages for content-management-suite ([d2931b5](https://github.com/bernierllc/tools/commit/d2931b5ea5de075268b9c8f078db959113a48fb7))
|
|
59
|
-
* **content-type-video:** implement complete video content type with 100% test coverage ([9e4849e](https://github.com/bernierllc/tools/commit/9e4849ed3e5febdd6aafdf0fb14e6f97a06f2430))
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# @bernierllc/content-type-video
|
|
66
|
-
|
|
67
|
-
## 1.0.1
|
|
68
|
-
|
|
69
|
-
### Patch Changes
|
|
70
|
-
|
|
71
|
-
- Updated dependencies [[`21cf207`](https://github.com/bernierllc/tools/commit/21cf207ed1e120e009f97937c4bd785add69681b)]:
|
|
72
|
-
- @bernierllc/content-type-registry@1.1.0
|
|
@@ -1,522 +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 { VideoContentType } from '../src/VideoContentType';
|
|
10
|
-
import * as fs from 'fs/promises';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
|
|
13
|
-
describe('VideoContentType', () => {
|
|
14
|
-
let videoType: VideoContentType;
|
|
15
|
-
let testUploadDir: string;
|
|
16
|
-
|
|
17
|
-
beforeEach(async () => {
|
|
18
|
-
testUploadDir = path.join(__dirname, 'test-uploads');
|
|
19
|
-
videoType = new VideoContentType({
|
|
20
|
-
uploadOptions: {
|
|
21
|
-
storageBasePath: testUploadDir,
|
|
22
|
-
maxSizeBytes: 1024 * 1024, // 1MB for tests
|
|
23
|
-
},
|
|
24
|
-
streamingBaseUrl: '/test-videos',
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(async () => {
|
|
29
|
-
await fs.rm(testUploadDir, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('upload', () => {
|
|
33
|
-
it('should upload valid MP4 video successfully', async () => {
|
|
34
|
-
const testVideo = Buffer.from('fake mp4 video content for testing');
|
|
35
|
-
const result = await videoType.upload(testVideo, 'test-video.mp4', 'user123');
|
|
36
|
-
|
|
37
|
-
expect(result.success).toBe(true);
|
|
38
|
-
expect(result.data?.metadata.filename).toContain('test-video.mp4');
|
|
39
|
-
expect(result.data?.metadata.mimeType).toBe('video/mp4');
|
|
40
|
-
expect(result.data?.uploadedBy).toBe('user123');
|
|
41
|
-
expect(result.data?.streamingUrl).toMatch(/^\/test-videos\//);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should upload valid WebM video successfully', async () => {
|
|
45
|
-
const testVideo = Buffer.from('fake webm video content');
|
|
46
|
-
const result = await videoType.upload(testVideo, 'test.webm');
|
|
47
|
-
|
|
48
|
-
expect(result.success).toBe(true);
|
|
49
|
-
expect(result.data?.metadata.mimeType).toBe('video/webm');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should upload valid MOV video successfully', async () => {
|
|
53
|
-
const testVideo = Buffer.from('fake mov video content');
|
|
54
|
-
const result = await videoType.upload(testVideo, 'test.mov');
|
|
55
|
-
|
|
56
|
-
expect(result.success).toBe(true);
|
|
57
|
-
expect(result.data?.metadata.mimeType).toBe('video/quicktime');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should upload valid AVI video successfully', async () => {
|
|
61
|
-
const testVideo = Buffer.from('fake avi video content');
|
|
62
|
-
const result = await videoType.upload(testVideo, 'test.avi');
|
|
63
|
-
|
|
64
|
-
expect(result.success).toBe(true);
|
|
65
|
-
expect(result.data?.metadata.mimeType).toBe('video/x-msvideo');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should reject invalid MIME type', async () => {
|
|
69
|
-
const testFile = Buffer.from('fake pdf');
|
|
70
|
-
const result = await videoType.upload(testFile, 'test.pdf');
|
|
71
|
-
|
|
72
|
-
expect(result.success).toBe(false);
|
|
73
|
-
expect(result.error).toContain('not allowed');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should reject oversized file', async () => {
|
|
77
|
-
const largeFile = Buffer.alloc(2 * 1024 * 1024);
|
|
78
|
-
const result = await videoType.upload(largeFile, 'large.mp4');
|
|
79
|
-
|
|
80
|
-
expect(result.success).toBe(false);
|
|
81
|
-
expect(result.error).toContain('exceeds maximum');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should generate unique filenames for concurrent uploads', async () => {
|
|
85
|
-
const testVideo = Buffer.from('test video');
|
|
86
|
-
|
|
87
|
-
// Add small delays to ensure unique timestamps
|
|
88
|
-
const upload1 = await videoType.upload(testVideo, 'test.mp4');
|
|
89
|
-
await new Promise(resolve => setTimeout(resolve, 2));
|
|
90
|
-
const upload2 = await videoType.upload(testVideo, 'test.mp4');
|
|
91
|
-
await new Promise(resolve => setTimeout(resolve, 2));
|
|
92
|
-
const upload3 = await videoType.upload(testVideo, 'test.mp4');
|
|
93
|
-
|
|
94
|
-
const filenames = [
|
|
95
|
-
upload1.data?.metadata.filename,
|
|
96
|
-
upload2.data?.metadata.filename,
|
|
97
|
-
upload3.data?.metadata.filename,
|
|
98
|
-
];
|
|
99
|
-
const uniqueFilenames = new Set(filenames);
|
|
100
|
-
|
|
101
|
-
expect(uniqueFilenames.size).toBe(3);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should handle upload without uploadedBy', async () => {
|
|
105
|
-
const testVideo = Buffer.from('test video');
|
|
106
|
-
const result = await videoType.upload(testVideo, 'test.mp4');
|
|
107
|
-
|
|
108
|
-
expect(result.success).toBe(true);
|
|
109
|
-
expect(result.data?.uploadedBy).toBeUndefined();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should handle zero-byte files', async () => {
|
|
113
|
-
const emptyFile = Buffer.alloc(0);
|
|
114
|
-
const result = await videoType.upload(emptyFile, 'empty.mp4');
|
|
115
|
-
|
|
116
|
-
expect(result.success).toBe(false);
|
|
117
|
-
expect(result.error).toContain('empty');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should sanitize special characters in filenames', async () => {
|
|
121
|
-
const testVideo = Buffer.from('test video');
|
|
122
|
-
const result = await videoType.upload(testVideo, 'test video (1).mp4');
|
|
123
|
-
|
|
124
|
-
expect(result.success).toBe(true);
|
|
125
|
-
expect(result.data?.filePath).toContain('test_video__1_.mp4');
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('validate', () => {
|
|
130
|
-
it('should validate allowed formats', async () => {
|
|
131
|
-
const testFile = Buffer.from('test');
|
|
132
|
-
const mp4Result = await videoType.validate(testFile, 'test.mp4');
|
|
133
|
-
const webmResult = await videoType.validate(testFile, 'test.webm');
|
|
134
|
-
const movResult = await videoType.validate(testFile, 'test.mov');
|
|
135
|
-
const aviResult = await videoType.validate(testFile, 'test.avi');
|
|
136
|
-
|
|
137
|
-
expect(mp4Result.success).toBe(true);
|
|
138
|
-
expect(webmResult.success).toBe(true);
|
|
139
|
-
expect(movResult.success).toBe(true);
|
|
140
|
-
expect(aviResult.success).toBe(true);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('should reject disallowed formats', async () => {
|
|
144
|
-
const testFile = Buffer.from('test');
|
|
145
|
-
const pdfResult = await videoType.validate(testFile, 'test.pdf');
|
|
146
|
-
const docResult = await videoType.validate(testFile, 'test.doc');
|
|
147
|
-
|
|
148
|
-
expect(pdfResult.success).toBe(false);
|
|
149
|
-
expect(pdfResult.error).toContain('not allowed');
|
|
150
|
-
expect(docResult.success).toBe(false);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should check file size limits', async () => {
|
|
154
|
-
const smallFile = Buffer.alloc(100);
|
|
155
|
-
const maxFile = Buffer.alloc(1024 * 1024);
|
|
156
|
-
const largeFile = Buffer.alloc(2 * 1024 * 1024);
|
|
157
|
-
|
|
158
|
-
const smallResult = await videoType.validate(smallFile, 'small.mp4');
|
|
159
|
-
const maxResult = await videoType.validate(maxFile, 'max.mp4');
|
|
160
|
-
const largeResult = await videoType.validate(largeFile, 'large.mp4');
|
|
161
|
-
|
|
162
|
-
expect(smallResult.success).toBe(true);
|
|
163
|
-
expect(maxResult.success).toBe(true);
|
|
164
|
-
expect(largeResult.success).toBe(false);
|
|
165
|
-
expect(largeResult.error).toContain('exceeds maximum');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should reject zero-byte files', async () => {
|
|
169
|
-
const emptyFile = Buffer.alloc(0);
|
|
170
|
-
const result = await videoType.validate(emptyFile, 'empty.mp4');
|
|
171
|
-
|
|
172
|
-
expect(result.success).toBe(false);
|
|
173
|
-
expect(result.error).toContain('empty');
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('file operations', () => {
|
|
178
|
-
it('should retrieve uploaded video', async () => {
|
|
179
|
-
const testVideo = Buffer.from('test video content');
|
|
180
|
-
const uploadResult = await videoType.upload(testVideo, 'test.mp4');
|
|
181
|
-
|
|
182
|
-
expect(uploadResult.success).toBe(true);
|
|
183
|
-
const localPath = uploadResult.data?.filePath;
|
|
184
|
-
expect(localPath).toBeDefined();
|
|
185
|
-
|
|
186
|
-
if (localPath) {
|
|
187
|
-
const retrieved = await videoType.get(localPath);
|
|
188
|
-
expect(retrieved).not.toBeNull();
|
|
189
|
-
expect(retrieved?.length).toBe(testVideo.length);
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('should return null for non-existent video', async () => {
|
|
194
|
-
const result = await videoType.get('/non/existent/path.mp4');
|
|
195
|
-
expect(result).toBeNull();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should delete uploaded video', async () => {
|
|
199
|
-
const testVideo = Buffer.from('test video');
|
|
200
|
-
const uploadResult = await videoType.upload(testVideo, 'test.mp4');
|
|
201
|
-
|
|
202
|
-
expect(uploadResult.success).toBe(true);
|
|
203
|
-
const localPath = uploadResult.data?.filePath;
|
|
204
|
-
expect(localPath).toBeDefined();
|
|
205
|
-
|
|
206
|
-
if (localPath) {
|
|
207
|
-
const deleteResult = await videoType.delete(localPath);
|
|
208
|
-
expect(deleteResult).toBe(true);
|
|
209
|
-
|
|
210
|
-
const retrieved = await videoType.get(localPath);
|
|
211
|
-
expect(retrieved).toBeNull();
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should return false when deleting non-existent video', async () => {
|
|
216
|
-
const result = await videoType.delete('/non/existent/path.mp4');
|
|
217
|
-
expect(result).toBe(false);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('should check if video exists', async () => {
|
|
221
|
-
const testVideo = Buffer.from('test video');
|
|
222
|
-
const uploadResult = await videoType.upload(testVideo, 'test.mp4');
|
|
223
|
-
|
|
224
|
-
expect(uploadResult.success).toBe(true);
|
|
225
|
-
const filePath = uploadResult.data?.filePath;
|
|
226
|
-
expect(filePath).toBeDefined();
|
|
227
|
-
|
|
228
|
-
if (filePath) {
|
|
229
|
-
const exists = await videoType.exists(filePath);
|
|
230
|
-
expect(exists).toBe(true);
|
|
231
|
-
|
|
232
|
-
await videoType.delete(filePath);
|
|
233
|
-
const stillExists = await videoType.exists(filePath);
|
|
234
|
-
expect(stillExists).toBe(false);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('getStreamingUrl', () => {
|
|
240
|
-
it('should generate correct streaming URLs', async () => {
|
|
241
|
-
const testVideo = Buffer.from('test video');
|
|
242
|
-
const uploadResult = await videoType.upload(testVideo, 'test.mp4');
|
|
243
|
-
|
|
244
|
-
expect(uploadResult.success).toBe(true);
|
|
245
|
-
const filePath = uploadResult.data?.filePath;
|
|
246
|
-
expect(filePath).toBeDefined();
|
|
247
|
-
|
|
248
|
-
if (filePath) {
|
|
249
|
-
const streamingUrl = videoType.getStreamingUrl(filePath);
|
|
250
|
-
expect(streamingUrl).toMatch(/^\/test-videos\//);
|
|
251
|
-
expect(streamingUrl).toContain('.mp4');
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('register', () => {
|
|
257
|
-
it('should register with content type registry', async () => {
|
|
258
|
-
await expect(videoType.register()).resolves.not.toThrow();
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('error handling edge cases', () => {
|
|
263
|
-
it('should handle file operations that throw errors', async () => {
|
|
264
|
-
const testVideo = Buffer.from('test video');
|
|
265
|
-
const uploadResult = await videoType.upload(testVideo, 'test.mp4');
|
|
266
|
-
|
|
267
|
-
expect(uploadResult.success).toBe(true);
|
|
268
|
-
const localPath = uploadResult.data?.localPath;
|
|
269
|
-
|
|
270
|
-
if (localPath) {
|
|
271
|
-
await fs.chmod(localPath, 0o000);
|
|
272
|
-
|
|
273
|
-
const retrieved = await videoType.get(localPath);
|
|
274
|
-
expect(retrieved).toBeNull();
|
|
275
|
-
|
|
276
|
-
await fs.chmod(localPath, 0o644);
|
|
277
|
-
await videoType.delete(localPath);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('should handle upload error when file system throws', async () => {
|
|
282
|
-
const invalidVideoType = new VideoContentType({
|
|
283
|
-
uploadOptions: {
|
|
284
|
-
storageBasePath: '/invalid/readonly/path/that/should/not/exist',
|
|
285
|
-
},
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
const testVideo = Buffer.from('test');
|
|
289
|
-
const result = await invalidVideoType.upload(testVideo, 'test.mp4');
|
|
290
|
-
|
|
291
|
-
expect(result.success).toBe(false);
|
|
292
|
-
expect(result.error).toBeDefined();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should handle different error types in upload catch block', async () => {
|
|
296
|
-
const invalidVideoType = new VideoContentType({
|
|
297
|
-
uploadOptions: {
|
|
298
|
-
storageBasePath: '/root/absolutely/invalid/path/with/no/permissions',
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const testVideo = Buffer.from('valid test data');
|
|
303
|
-
const result = await invalidVideoType.upload(testVideo, 'test.mp4');
|
|
304
|
-
|
|
305
|
-
expect(result.success).toBe(false);
|
|
306
|
-
expect(result.error).toBeTruthy();
|
|
307
|
-
expect(typeof result.error).toBe('string');
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe('configuration', () => {
|
|
312
|
-
it('should use custom configuration', () => {
|
|
313
|
-
const customVideoType = new VideoContentType({
|
|
314
|
-
uploadOptions: {
|
|
315
|
-
maxSizeBytes: 100 * 1024 * 1024,
|
|
316
|
-
allowedFormats: ['mp4', 'webm'],
|
|
317
|
-
storageBasePath: './custom-storage',
|
|
318
|
-
},
|
|
319
|
-
streamingBaseUrl: '/custom/stream',
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
expect(customVideoType).toBeInstanceOf(VideoContentType);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('should use default configuration', () => {
|
|
326
|
-
const defaultVideoType = new VideoContentType();
|
|
327
|
-
expect(defaultVideoType.typeName).toBe('video');
|
|
328
|
-
expect(defaultVideoType.version).toBe('1.0.0');
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('should use environment variables for configuration', () => {
|
|
332
|
-
process.env.VIDEO_MAX_SIZE_BYTES = '5000000';
|
|
333
|
-
process.env.VIDEO_ALLOWED_FORMATS = 'mp4,webm';
|
|
334
|
-
process.env.VIDEO_STORAGE_BASE_PATH = '/env/storage';
|
|
335
|
-
process.env.VIDEO_STREAMING_BASE_URL = '/env/stream';
|
|
336
|
-
process.env.VIDEO_GENERATE_THUMBNAIL = 'true';
|
|
337
|
-
process.env.VIDEO_EXTRACT_METADATA = 'false';
|
|
338
|
-
|
|
339
|
-
const envVideoType = new VideoContentType();
|
|
340
|
-
expect(envVideoType).toBeInstanceOf(VideoContentType);
|
|
341
|
-
|
|
342
|
-
delete process.env.VIDEO_MAX_SIZE_BYTES;
|
|
343
|
-
delete process.env.VIDEO_ALLOWED_FORMATS;
|
|
344
|
-
delete process.env.VIDEO_STORAGE_BASE_PATH;
|
|
345
|
-
delete process.env.VIDEO_STREAMING_BASE_URL;
|
|
346
|
-
delete process.env.VIDEO_GENERATE_THUMBNAIL;
|
|
347
|
-
delete process.env.VIDEO_EXTRACT_METADATA;
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('should handle partial configuration override', () => {
|
|
351
|
-
const partialVideoType = new VideoContentType({
|
|
352
|
-
uploadOptions: {
|
|
353
|
-
maxSizeBytes: 50 * 1024 * 1024,
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
expect(partialVideoType).toBeInstanceOf(VideoContentType);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('should handle configuration with undefined boolean values', () => {
|
|
361
|
-
const videoTypeWithUndefined = new VideoContentType({
|
|
362
|
-
uploadOptions: {
|
|
363
|
-
generateThumbnail: undefined,
|
|
364
|
-
extractMetadata: undefined,
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
expect(videoTypeWithUndefined).toBeInstanceOf(VideoContentType);
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
describe('validateContent', () => {
|
|
373
|
-
it('should validate correct video content data', () => {
|
|
374
|
-
const validData = {
|
|
375
|
-
filePath: testUploadDir + '/test.mp4',
|
|
376
|
-
streamingUrl: '/test-videos/test.mp4',
|
|
377
|
-
metadata: {
|
|
378
|
-
filename: 'test.mp4',
|
|
379
|
-
mimeType: 'video/mp4',
|
|
380
|
-
size: 1024000,
|
|
381
|
-
duration: 30,
|
|
382
|
-
width: 1920,
|
|
383
|
-
height: 1080,
|
|
384
|
-
fps: 30,
|
|
385
|
-
},
|
|
386
|
-
uploadedAt: new Date(),
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
const result = videoType.validateContent(validData);
|
|
390
|
-
|
|
391
|
-
expect(result.success).toBe(true);
|
|
392
|
-
expect(result.data).toEqual(validData);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should validate content with optional fields', () => {
|
|
396
|
-
const validData = {
|
|
397
|
-
filePath: testUploadDir + '/test.mp4',
|
|
398
|
-
streamingUrl: '/test-videos/test.mp4',
|
|
399
|
-
metadata: {
|
|
400
|
-
filename: 'test.mp4',
|
|
401
|
-
mimeType: 'video/mp4',
|
|
402
|
-
size: 1024000,
|
|
403
|
-
duration: 30,
|
|
404
|
-
width: 1920,
|
|
405
|
-
height: 1080,
|
|
406
|
-
fps: 30,
|
|
407
|
-
thumbnail: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUg',
|
|
408
|
-
bitrate: 5000000,
|
|
409
|
-
codec: 'h264',
|
|
410
|
-
},
|
|
411
|
-
uploadedAt: new Date(),
|
|
412
|
-
uploadedBy: 'user123',
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
const result = videoType.validateContent(validData);
|
|
416
|
-
|
|
417
|
-
expect(result.success).toBe(true);
|
|
418
|
-
expect(result.data).toEqual(validData);
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it('should reject invalid metadata - wrong MIME type', () => {
|
|
422
|
-
const invalidData = {
|
|
423
|
-
filePath: testUploadDir + '/test.mp4',
|
|
424
|
-
streamingUrl: '/test-videos/test.mp4',
|
|
425
|
-
metadata: {
|
|
426
|
-
filename: 'test.mp4',
|
|
427
|
-
mimeType: 'invalid/type',
|
|
428
|
-
size: 1024000,
|
|
429
|
-
duration: 30,
|
|
430
|
-
width: 1920,
|
|
431
|
-
height: 1080,
|
|
432
|
-
fps: 30,
|
|
433
|
-
},
|
|
434
|
-
uploadedAt: new Date(),
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
const result = videoType.validateContent(invalidData);
|
|
438
|
-
|
|
439
|
-
expect(result.success).toBe(false);
|
|
440
|
-
expect(result.error).toBeDefined();
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('should handle non-Error exceptions in validateContent', () => {
|
|
444
|
-
const invalidData = {
|
|
445
|
-
filePath: testUploadDir + '/test.mp4',
|
|
446
|
-
streamingUrl: '/test-videos/test.mp4',
|
|
447
|
-
metadata: {
|
|
448
|
-
filename: '',
|
|
449
|
-
mimeType: 'video/mp4',
|
|
450
|
-
size: -1,
|
|
451
|
-
duration: -1,
|
|
452
|
-
width: 0,
|
|
453
|
-
height: 0,
|
|
454
|
-
fps: 0,
|
|
455
|
-
},
|
|
456
|
-
uploadedAt: new Date(),
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
const result = videoType.validateContent(invalidData);
|
|
460
|
-
|
|
461
|
-
expect(result.success).toBe(false);
|
|
462
|
-
expect(result.error).toBeDefined();
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
describe('storage edge cases', () => {
|
|
467
|
-
it('should handle file path validation from string', async () => {
|
|
468
|
-
const testFilePath = path.join(__dirname, 'fixtures', 'test.mp4');
|
|
469
|
-
await fs.mkdir(path.dirname(testFilePath), { recursive: true });
|
|
470
|
-
await fs.writeFile(testFilePath, 'test video content');
|
|
471
|
-
|
|
472
|
-
const result = await videoType.upload(testFilePath, 'test.mp4');
|
|
473
|
-
|
|
474
|
-
expect(result.success).toBe(true);
|
|
475
|
-
expect(result.data?.metadata.size).toBeGreaterThan(0);
|
|
476
|
-
|
|
477
|
-
// Clean up
|
|
478
|
-
await fs.rm(path.join(__dirname, 'fixtures'), { recursive: true, force: true });
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('should handle getMimeType for unknown extensions', async () => {
|
|
482
|
-
const testVideo = Buffer.from('test video');
|
|
483
|
-
|
|
484
|
-
const customVideoType = new VideoContentType({
|
|
485
|
-
uploadOptions: {
|
|
486
|
-
storageBasePath: testUploadDir,
|
|
487
|
-
allowedFormats: ['mp4', 'xyz'], // Add fake format
|
|
488
|
-
},
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
const result = await customVideoType.upload(testVideo, 'test.xyz');
|
|
492
|
-
|
|
493
|
-
expect(result.success).toBe(true);
|
|
494
|
-
// Unknown format falls back to video/mp4
|
|
495
|
-
expect(result.data?.metadata.mimeType).toBe('video/mp4');
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
it('should handle JPG extension mapping', async () => {
|
|
499
|
-
const customVideoType = new VideoContentType({
|
|
500
|
-
uploadOptions: {
|
|
501
|
-
storageBasePath: testUploadDir,
|
|
502
|
-
allowedFormats: ['mp4', 'jpg'], // Allow jpg for testing
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
const testVideo = Buffer.from('test');
|
|
507
|
-
const result = await customVideoType.upload(testVideo, 'test.jpg');
|
|
508
|
-
|
|
509
|
-
expect(result.success).toBe(true);
|
|
510
|
-
// jpg extension maps to video/mp4 (default fallback)
|
|
511
|
-
expect(result.data?.metadata.mimeType).toBe('video/mp4');
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('should handle uppercase file extensions', async () => {
|
|
515
|
-
const testVideo = Buffer.from('test');
|
|
516
|
-
const result = await videoType.upload(testVideo, 'TEST.MP4');
|
|
517
|
-
|
|
518
|
-
expect(result.success).toBe(true);
|
|
519
|
-
expect(result.data?.metadata.mimeType).toBe('video/mp4');
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
});
|
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: 75,
|
|
23
|
-
functions: 90,
|
|
24
|
-
lines: 90,
|
|
25
|
-
statements: 90
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
29
|
-
transform: {
|
|
30
|
-
'^.+\\.ts$': 'ts-jest'
|
|
31
|
-
}
|
|
32
|
-
};
|
package/src/VideoContentType.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 { contentTypeRegistry, BaseContentType } from '@bernierllc/content-type-registry';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
import {
|
|
13
|
-
VideoContentData,
|
|
14
|
-
VideoContentTypeConfig,
|
|
15
|
-
VideoContentResult,
|
|
16
|
-
VideoMetadataSchema,
|
|
17
|
-
} from './types';
|
|
18
|
-
import { VideoStorage } from './storage/VideoStorage';
|
|
19
|
-
import { LocalVideoStorage } from './storage/LocalVideoStorage';
|
|
20
|
-
import { loadConfig } from './config';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Video content type with file upload, local storage, and streaming URL generation
|
|
24
|
-
*/
|
|
25
|
-
export class VideoContentType {
|
|
26
|
-
public readonly typeName = 'video';
|
|
27
|
-
public readonly version = '1.0.0';
|
|
28
|
-
private config: VideoContentTypeConfig;
|
|
29
|
-
private storage: VideoStorage;
|
|
30
|
-
|
|
31
|
-
constructor(config: Partial<VideoContentTypeConfig> = {}) {
|
|
32
|
-
this.config = loadConfig(config);
|
|
33
|
-
this.storage = new LocalVideoStorage(this.config);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Register video content type with registry
|
|
38
|
-
*/
|
|
39
|
-
async register(): Promise<void> {
|
|
40
|
-
contentTypeRegistry.register({
|
|
41
|
-
id: 'video',
|
|
42
|
-
name: 'Video',
|
|
43
|
-
description: 'Base video content type with file upload and streaming',
|
|
44
|
-
baseType: BaseContentType.VIDEO,
|
|
45
|
-
icon: 'video',
|
|
46
|
-
schema: VideoMetadataSchema,
|
|
47
|
-
editor: this.config.editor,
|
|
48
|
-
storage: this.config.storage,
|
|
49
|
-
publishing: this.config.publishing,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Validate video file
|
|
55
|
-
*/
|
|
56
|
-
async validate(file: Buffer | string, filename: string): Promise<VideoContentResult<void>> {
|
|
57
|
-
// Check for zero-byte files
|
|
58
|
-
const fileSize = Buffer.isBuffer(file) ? file.length : fs.statSync(file).size;
|
|
59
|
-
if (fileSize === 0) {
|
|
60
|
-
return {
|
|
61
|
-
success: false,
|
|
62
|
-
error: 'File is empty (0 bytes)',
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Validate file format
|
|
67
|
-
const extension = path.extname(filename).slice(1).toLowerCase();
|
|
68
|
-
const allowedFormats = this.config.uploadOptions?.allowedFormats || ['mp4', 'webm', 'mov', 'avi'];
|
|
69
|
-
|
|
70
|
-
if (!allowedFormats.includes(extension)) {
|
|
71
|
-
return {
|
|
72
|
-
success: false,
|
|
73
|
-
error: `File format ${extension} not allowed. Allowed: ${allowedFormats.join(', ')}`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Validate file size
|
|
78
|
-
const maxSize = this.config.uploadOptions?.maxSizeBytes || 500 * 1024 * 1024;
|
|
79
|
-
if (fileSize > maxSize) {
|
|
80
|
-
return {
|
|
81
|
-
success: false,
|
|
82
|
-
error: `File size ${fileSize} exceeds maximum ${maxSize}`,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return { success: true };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Upload video file and extract metadata
|
|
91
|
-
*/
|
|
92
|
-
async upload(
|
|
93
|
-
file: Buffer | string,
|
|
94
|
-
filename: string,
|
|
95
|
-
uploadedBy?: string
|
|
96
|
-
): Promise<VideoContentResult<VideoContentData>> {
|
|
97
|
-
try {
|
|
98
|
-
const validation = await this.validate(file, filename);
|
|
99
|
-
if (!validation.success) {
|
|
100
|
-
return { success: false, error: validation.error || 'Validation failed' };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return await this.storage.save(file, filename, uploadedBy);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return {
|
|
106
|
-
success: false,
|
|
107
|
-
error: error instanceof Error ? error.message : 'Unknown error during video upload',
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async get(localPath: string): Promise<Buffer | null> {
|
|
113
|
-
return this.storage.get(localPath);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async delete(localPath: string): Promise<boolean> {
|
|
117
|
-
return this.storage.delete(localPath);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async exists(localPath: string): Promise<boolean> {
|
|
121
|
-
return this.storage.exists(localPath);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getStreamingUrl(localPath: string): string {
|
|
125
|
-
return this.storage.getStreamingUrl(localPath);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Validate video content data
|
|
130
|
-
*/
|
|
131
|
-
validateContent(data: VideoContentData): VideoContentResult<VideoContentData> {
|
|
132
|
-
try {
|
|
133
|
-
VideoMetadataSchema.parse(data.metadata);
|
|
134
|
-
return { success: true, data };
|
|
135
|
-
} catch (error) {
|
|
136
|
-
return {
|
|
137
|
-
success: false,
|
|
138
|
-
error: error instanceof Error ? error.message : 'Validation failed',
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
package/src/config.ts
DELETED
|
@@ -1,63 +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 { VideoContentTypeConfig } from './types';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Load configuration from environment variables with fallback to defaults
|
|
13
|
-
*/
|
|
14
|
-
export function loadConfig(config: Partial<VideoContentTypeConfig> = {}): VideoContentTypeConfig {
|
|
15
|
-
const maxSizeBytes = config.uploadOptions?.maxSizeBytes
|
|
16
|
-
|| (process.env.VIDEO_MAX_SIZE_BYTES ? parseInt(process.env.VIDEO_MAX_SIZE_BYTES, 10) : undefined)
|
|
17
|
-
|| 500 * 1024 * 1024; // 500MB default
|
|
18
|
-
|
|
19
|
-
const storageBasePath = config.uploadOptions?.storageBasePath
|
|
20
|
-
|| process.env.VIDEO_STORAGE_BASE_PATH
|
|
21
|
-
|| './storage/videos';
|
|
22
|
-
|
|
23
|
-
const streamingBaseUrl = config.streamingBaseUrl
|
|
24
|
-
|| process.env.VIDEO_STREAMING_BASE_URL
|
|
25
|
-
|| '/api/video/stream';
|
|
26
|
-
|
|
27
|
-
const allowedFormats = config.uploadOptions?.allowedFormats
|
|
28
|
-
|| (process.env.VIDEO_ALLOWED_FORMATS ? process.env.VIDEO_ALLOWED_FORMATS.split(',').map(f => f.trim()) : undefined)
|
|
29
|
-
|| ['mp4', 'webm', 'mov', 'avi'];
|
|
30
|
-
|
|
31
|
-
const generateThumbnail = config.uploadOptions?.generateThumbnail
|
|
32
|
-
!== undefined ? config.uploadOptions.generateThumbnail
|
|
33
|
-
: (process.env.VIDEO_GENERATE_THUMBNAIL === 'true' || true);
|
|
34
|
-
|
|
35
|
-
const extractMetadata = config.uploadOptions?.extractMetadata
|
|
36
|
-
!== undefined ? config.uploadOptions.extractMetadata
|
|
37
|
-
: (process.env.VIDEO_EXTRACT_METADATA === 'true' || true);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
baseType: 'video',
|
|
41
|
-
editor: config.editor || {
|
|
42
|
-
component: 'file-upload-with-player',
|
|
43
|
-
settings: {},
|
|
44
|
-
},
|
|
45
|
-
storage: config.storage || {
|
|
46
|
-
method: 'file',
|
|
47
|
-
settings: {},
|
|
48
|
-
},
|
|
49
|
-
publishing: config.publishing || {
|
|
50
|
-
methods: ['streaming-url'],
|
|
51
|
-
defaultMethod: 'streaming-url',
|
|
52
|
-
settings: {},
|
|
53
|
-
},
|
|
54
|
-
uploadOptions: {
|
|
55
|
-
maxSizeBytes,
|
|
56
|
-
allowedFormats,
|
|
57
|
-
generateThumbnail,
|
|
58
|
-
extractMetadata,
|
|
59
|
-
storageBasePath,
|
|
60
|
-
},
|
|
61
|
-
streamingBaseUrl,
|
|
62
|
-
};
|
|
63
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,14 +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
|
-
export * from './types';
|
|
10
|
-
export * from './VideoContentType';
|
|
11
|
-
export * from './config';
|
|
12
|
-
export * from './storage/VideoStorage';
|
|
13
|
-
export * from './storage/LocalVideoStorage';
|
|
14
|
-
export { VideoContentType } from './VideoContentType';
|
|
@@ -1,134 +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 * as fs from 'fs/promises';
|
|
10
|
-
import * as fsSync from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import { VideoStorage } from './VideoStorage';
|
|
13
|
-
import { VideoContentResult, VideoContentData, VideoMetadata, VideoMetadataSchema, VideoContentTypeConfig } from '../types';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Local file system storage implementation for videos
|
|
17
|
-
*/
|
|
18
|
-
export class LocalVideoStorage implements VideoStorage {
|
|
19
|
-
private storageBasePath: string;
|
|
20
|
-
private streamingBaseUrl: string;
|
|
21
|
-
|
|
22
|
-
constructor(config: VideoContentTypeConfig) {
|
|
23
|
-
this.storageBasePath = config.uploadOptions?.storageBasePath || './storage/videos';
|
|
24
|
-
this.streamingBaseUrl = config.streamingBaseUrl || '/api/video/stream';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async save(file: Buffer | string, filename: string, uploadedBy?: string): Promise<VideoContentResult<VideoContentData>> {
|
|
28
|
-
try {
|
|
29
|
-
// Ensure storage directory exists
|
|
30
|
-
await fs.mkdir(this.storageBasePath, { recursive: true });
|
|
31
|
-
|
|
32
|
-
// Generate unique filename
|
|
33
|
-
const timestamp = Date.now();
|
|
34
|
-
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
35
|
-
const uniqueFilename = `${timestamp}-${sanitizedFilename}`;
|
|
36
|
-
|
|
37
|
-
// Save file
|
|
38
|
-
const filePath = path.join(this.storageBasePath, uniqueFilename);
|
|
39
|
-
|
|
40
|
-
if (Buffer.isBuffer(file)) {
|
|
41
|
-
await fs.writeFile(filePath, file);
|
|
42
|
-
} else {
|
|
43
|
-
await fs.copyFile(file, filePath);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Extract metadata
|
|
47
|
-
const metadata = await this.extractMetadata(filePath);
|
|
48
|
-
|
|
49
|
-
// Generate streaming URL
|
|
50
|
-
const streamingUrl = this.getStreamingUrl(filePath);
|
|
51
|
-
|
|
52
|
-
const contentData: VideoContentData = {
|
|
53
|
-
filePath,
|
|
54
|
-
streamingUrl,
|
|
55
|
-
metadata,
|
|
56
|
-
uploadedAt: new Date(),
|
|
57
|
-
uploadedBy,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
success: true,
|
|
62
|
-
data: contentData,
|
|
63
|
-
};
|
|
64
|
-
} catch (error) {
|
|
65
|
-
return {
|
|
66
|
-
success: false,
|
|
67
|
-
error: error instanceof Error ? error.message : 'Unknown save error',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async get(localPath: string): Promise<Buffer | null> {
|
|
73
|
-
try {
|
|
74
|
-
return await fs.readFile(localPath);
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async delete(localPath: string): Promise<boolean> {
|
|
81
|
-
try {
|
|
82
|
-
await fs.unlink(localPath);
|
|
83
|
-
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async exists(localPath: string): Promise<boolean> {
|
|
90
|
-
try {
|
|
91
|
-
await fs.access(localPath);
|
|
92
|
-
return true;
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
getStreamingUrl(localPath: string): string {
|
|
99
|
-
const relativePath = path.relative(this.storageBasePath, localPath);
|
|
100
|
-
return `${this.streamingBaseUrl}/${encodeURIComponent(relativePath)}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Extract video metadata
|
|
105
|
-
* Note: In production, this would use ffmpeg/ffprobe for real metadata extraction
|
|
106
|
-
*/
|
|
107
|
-
private async extractMetadata(filePath: string): Promise<VideoMetadata> {
|
|
108
|
-
const stats = fsSync.statSync(filePath);
|
|
109
|
-
|
|
110
|
-
// Basic metadata - in production, use ffmpeg/ffprobe for real extraction
|
|
111
|
-
const metadata: VideoMetadata = {
|
|
112
|
-
filename: path.basename(filePath),
|
|
113
|
-
mimeType: this.getMimeTypeFromExtension(filePath),
|
|
114
|
-
size: stats.size,
|
|
115
|
-
duration: 60, // Placeholder - would extract from video
|
|
116
|
-
width: 1920, // Placeholder - would extract from video
|
|
117
|
-
height: 1080, // Placeholder - would extract from video
|
|
118
|
-
fps: 30, // Placeholder - would extract from video
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
return VideoMetadataSchema.parse(metadata);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private getMimeTypeFromExtension(filePath: string): string {
|
|
125
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
126
|
-
const mimeTypes: Record<string, string> = {
|
|
127
|
-
mp4: 'video/mp4',
|
|
128
|
-
webm: 'video/webm',
|
|
129
|
-
mov: 'video/quicktime',
|
|
130
|
-
avi: 'video/x-msvideo',
|
|
131
|
-
};
|
|
132
|
-
return mimeTypes[ext] || 'video/mp4';
|
|
133
|
-
}
|
|
134
|
-
}
|
|
@@ -1,20 +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 { VideoContentResult, VideoContentData } from '../types';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Video storage interface
|
|
13
|
-
*/
|
|
14
|
-
export interface VideoStorage {
|
|
15
|
-
save(file: Buffer | string, filename: string, uploadedBy?: string): Promise<VideoContentResult<VideoContentData>>;
|
|
16
|
-
get(localPath: string): Promise<Buffer | null>;
|
|
17
|
-
delete(localPath: string): Promise<boolean>;
|
|
18
|
-
exists(localPath: string): Promise<boolean>;
|
|
19
|
-
getStreamingUrl(localPath: string): string;
|
|
20
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,99 +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
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Video metadata schema
|
|
13
|
-
*/
|
|
14
|
-
export const VideoMetadataSchema = z.object({
|
|
15
|
-
filename: z.string(),
|
|
16
|
-
mimeType: z.string().regex(/^video\//),
|
|
17
|
-
size: z.number().positive(),
|
|
18
|
-
duration: z.number().positive(), // seconds
|
|
19
|
-
width: z.number().int().positive(),
|
|
20
|
-
height: z.number().int().positive(),
|
|
21
|
-
fps: z.number().positive(),
|
|
22
|
-
thumbnail: z.string().optional(), // base64 or URL to thumbnail
|
|
23
|
-
bitrate: z.number().positive().optional(),
|
|
24
|
-
codec: z.string().optional()
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export type VideoMetadata = z.infer<typeof VideoMetadataSchema>;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Video content data
|
|
31
|
-
*/
|
|
32
|
-
export interface VideoContentData {
|
|
33
|
-
filePath: string; // Local file path
|
|
34
|
-
streamingUrl: string; // Generated streaming URL
|
|
35
|
-
metadata: VideoMetadata;
|
|
36
|
-
uploadedAt: Date;
|
|
37
|
-
uploadedBy?: string | undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Video upload options
|
|
42
|
-
*/
|
|
43
|
-
export interface VideoUploadOptions {
|
|
44
|
-
maxSizeBytes?: number; // Default: 500MB
|
|
45
|
-
allowedFormats?: string[]; // Default: ['mp4', 'webm', 'mov', 'avi']
|
|
46
|
-
generateThumbnail?: boolean; // Default: true
|
|
47
|
-
extractMetadata?: boolean; // Default: true
|
|
48
|
-
storageBasePath?: string; // Default: './storage/videos'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Video content type configuration
|
|
53
|
-
*/
|
|
54
|
-
export interface VideoContentTypeConfig {
|
|
55
|
-
baseType: 'video';
|
|
56
|
-
editor: {
|
|
57
|
-
component: string;
|
|
58
|
-
settings?: Record<string, unknown>;
|
|
59
|
-
};
|
|
60
|
-
storage: {
|
|
61
|
-
method: string;
|
|
62
|
-
settings?: Record<string, unknown>;
|
|
63
|
-
};
|
|
64
|
-
publishing: {
|
|
65
|
-
methods: string[];
|
|
66
|
-
defaultMethod?: string;
|
|
67
|
-
settings?: Record<string, unknown>;
|
|
68
|
-
};
|
|
69
|
-
uploadOptions?: VideoUploadOptions;
|
|
70
|
-
streamingBaseUrl?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Result type for video operations
|
|
75
|
-
*/
|
|
76
|
-
export interface VideoContentResult<T = unknown> {
|
|
77
|
-
success: boolean;
|
|
78
|
-
data?: T;
|
|
79
|
-
error?: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Video content error codes
|
|
84
|
-
*/
|
|
85
|
-
export type VideoContentErrorCode =
|
|
86
|
-
| 'INVALID_FORMAT'
|
|
87
|
-
| 'FILE_TOO_LARGE'
|
|
88
|
-
| 'UPLOAD_FAILED'
|
|
89
|
-
| 'METADATA_EXTRACTION_FAILED'
|
|
90
|
-
| 'VALIDATION_FAILED';
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Video content error
|
|
94
|
-
*/
|
|
95
|
-
export interface VideoContentError {
|
|
96
|
-
code: VideoContentErrorCode;
|
|
97
|
-
message: string;
|
|
98
|
-
details?: unknown;
|
|
99
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "./dist",
|
|
5
|
-
"rootDir": "./src",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"declarationMap": true,
|
|
8
|
-
"sourceMap": true,
|
|
9
|
-
"noEmit": false
|
|
10
|
-
},
|
|
11
|
-
"include": [
|
|
12
|
-
"src/**/*"
|
|
13
|
-
],
|
|
14
|
-
"exclude": [
|
|
15
|
-
"node_modules",
|
|
16
|
-
"dist",
|
|
17
|
-
"**/*.test.ts",
|
|
18
|
-
"**/*.test.tsx"
|
|
19
|
-
]
|
|
20
|
-
}
|