@sproux/media-sdk 0.1.0 → 0.1.2
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/README.md +123 -115
- package/dist/index.cjs +146 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -97
- package/dist/index.d.ts +91 -97
- package/dist/index.js +138 -118
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @sproux/media-sdk
|
|
2
2
|
|
|
3
|
-
Media URL builder library for Sproux — build image variant URLs, video HLS playlist URLs, and thumbnails from
|
|
3
|
+
Media URL builder library for Sproux — build image variant URLs, video HLS playlist URLs, and thumbnails from object keys using a singleton pattern.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -18,138 +18,161 @@ yarn add @sproux/media-sdk
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
20
|
```typescript
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
import { SprouxMedia, IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
|
|
22
|
+
|
|
23
|
+
// Initialize once (e.g. at app startup)
|
|
24
|
+
const media = SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
|
|
25
|
+
|
|
26
|
+
// Build an image variant URL with all options
|
|
27
|
+
const imageUrl = media.getImageUrl('avatar/image/usr-1/abc.jpg', {
|
|
28
|
+
extension: IMAGE_FORMAT.WEBP,
|
|
29
|
+
width: 200,
|
|
30
|
+
height: 200,
|
|
31
|
+
resizeType: IMAGE_RESIZE_TYPE.FIT,
|
|
32
|
+
quality: 80,
|
|
33
|
+
});
|
|
34
|
+
// → "https://cdn.example.com/avatar/image/usr-1/abc-200x200-fit-q80.webp"
|
|
27
35
|
|
|
28
|
-
//
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
{ width: 200, height: 200, resizeType: 'fit', quality: 80 }
|
|
32
|
-
);
|
|
33
|
-
// → "https://cdn.example.com/avatar/image/usr-1/profile-200x200-fit-q80.webp"
|
|
36
|
+
// Or with just the object key (no variant transformation)
|
|
37
|
+
const originalUrl = media.getImageUrl('avatar/image/usr-1/abc.jpg');
|
|
38
|
+
// → "https://cdn.example.com/avatar/image/usr-1/abc.jpg"
|
|
34
39
|
|
|
35
40
|
// Build a video HLS playlist URL
|
|
36
|
-
const hlsUrl =
|
|
37
|
-
'https://cdn.example.com/gallery/video/usr-1/intro.mp4'
|
|
38
|
-
);
|
|
41
|
+
const hlsUrl = media.getVideoHlsUrl('gallery/video/usr-1/intro');
|
|
39
42
|
// → "https://cdn.example.com/gallery/video/usr-1/intro/playlist.m3u8"
|
|
40
43
|
|
|
41
44
|
// Build a video thumbnail URL
|
|
42
|
-
const
|
|
43
|
-
'https://cdn.example.com/gallery/video/usr-1/intro.mp4'
|
|
44
|
-
);
|
|
45
|
+
const thumbUrl = media.getVideoThumbnailUrl('gallery/video/usr-1/intro');
|
|
45
46
|
// → "https://cdn.example.com/gallery/video/usr-1/intro/thumbnail.webp"
|
|
46
47
|
```
|
|
47
48
|
|
|
48
|
-
##
|
|
49
|
+
## Singleton Usage
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
```typescript
|
|
52
|
+
import { SprouxMedia, IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
// Initialize once at startup
|
|
55
|
+
SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
// Access from anywhere via getInstance()
|
|
58
|
+
const media = SprouxMedia.getInstance();
|
|
59
|
+
// With variant options
|
|
60
|
+
const url = media.getImageUrl('avatar/image/usr-1/photo.jpg', {
|
|
61
|
+
extension: IMAGE_FORMAT.WEBP,
|
|
62
|
+
width: 400,
|
|
63
|
+
height: 300,
|
|
64
|
+
resizeType: IMAGE_RESIZE_TYPE.FILL,
|
|
65
|
+
});
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
// Without options — returns the original URL
|
|
68
|
+
const original = media.getImageUrl('avatar/image/usr-1/photo.jpg');
|
|
69
|
+
```
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// }
|
|
71
|
+
> Calling `getInstance()` before `init()` will throw an error.
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### `SprouxMedia.init(config: SprouxMediaConfig): SprouxMedia`
|
|
76
|
+
|
|
77
|
+
Initialize the singleton with CDN configuration. Returns the singleton instance.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const media = SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
|
|
70
81
|
```
|
|
71
82
|
|
|
72
|
-
### `
|
|
83
|
+
### `SprouxMedia.getInstance(): SprouxMedia`
|
|
84
|
+
|
|
85
|
+
Returns the existing singleton instance. Throws if `init()` has not been called.
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
### `media.getImageUrl(objectKey: string, options?: ImageUrlOptions): string`
|
|
88
|
+
|
|
89
|
+
Build a CDN URL for an image variant. When `options` is omitted, returns the original CDN URL for the object key as-is.
|
|
90
|
+
|
|
91
|
+
- `objectKey` — Object key **with** extension (e.g. `"avatar/image/usr-1/abc.jpg"`)
|
|
92
|
+
- `options` — Optional variant transformation options
|
|
75
93
|
|
|
76
94
|
```typescript
|
|
77
|
-
import {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
);
|
|
95
|
+
import { IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
|
|
96
|
+
|
|
97
|
+
// With variant options
|
|
98
|
+
media.getImageUrl('avatar/image/usr-1/photo.jpg', {
|
|
99
|
+
extension: IMAGE_FORMAT.WEBP,
|
|
100
|
+
width: 400,
|
|
101
|
+
height: 300,
|
|
102
|
+
resizeType: IMAGE_RESIZE_TYPE.FILL,
|
|
103
|
+
quality: 85,
|
|
104
|
+
});
|
|
89
105
|
// → "https://cdn.example.com/avatar/image/usr-1/photo-400x300-fill-q85.webp"
|
|
106
|
+
|
|
107
|
+
// Without options — returns the original URL
|
|
108
|
+
media.getImageUrl('avatar/image/usr-1/photo.jpg');
|
|
109
|
+
// → "https://cdn.example.com/avatar/image/usr-1/photo.jpg"
|
|
90
110
|
```
|
|
91
111
|
|
|
92
|
-
####
|
|
112
|
+
#### ImageUrlOptions
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
| ------------ | ----------------- | -------- | ------------------------------------- |
|
|
96
|
-
| `width` | `number` | Yes | Target width in pixels (1-4096) |
|
|
97
|
-
| `height` | `number` | Yes | Target height in pixels (1-4096) |
|
|
98
|
-
| `resizeType` | `ImageResizeType` | Yes | Resize strategy: `fit`, `fill`, `auto` |
|
|
99
|
-
| `format` | `ImageFormat` | No | Output format (default: `webp`) |
|
|
100
|
-
| `quality` | `number` | No | Quality 1-100 |
|
|
114
|
+
All fields are optional.
|
|
101
115
|
|
|
102
|
-
|
|
116
|
+
| Property | Type | Required | Description |
|
|
117
|
+
| ------------ | ----------------- | -------- | -------------------------------------- |
|
|
118
|
+
| `extension` | `ImageFormat` | No | Output format (`webp`, `avif`, `jpeg`, `png`, `gif`, `ico`, `svg`, `jpg`) |
|
|
119
|
+
| `width` | `number` | No | Target width in pixels (1–4096) |
|
|
120
|
+
| `height` | `number` | No | Target height in pixels (1–4096) |
|
|
121
|
+
| `resizeType` | `ImageResizeType` | No | Resize strategy: `fit`, `fill`, `force`, `fill-down`, `auto` |
|
|
122
|
+
| `quality` | `number` | No | Quality 1–100 |
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
### `media.getVideoHlsUrl(objectKey: string): string`
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
import { buildVideoHlsUrl } from '@sproux/media-sdk';
|
|
126
|
+
Build an HLS playlist URL from an object key (without extension).
|
|
108
127
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
128
|
+
```typescript
|
|
129
|
+
media.getVideoHlsUrl('gallery/video/usr-1/video');
|
|
112
130
|
// → "https://cdn.example.com/gallery/video/usr-1/video/playlist.m3u8"
|
|
113
131
|
```
|
|
114
132
|
|
|
115
|
-
### `
|
|
133
|
+
### `media.getVideoThumbnailUrl(objectKey: string): string`
|
|
116
134
|
|
|
117
|
-
Build a thumbnail URL from an
|
|
135
|
+
Build a thumbnail URL from an object key (without extension).
|
|
118
136
|
|
|
119
137
|
```typescript
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const thumbnailUrl = buildVideoThumbnailUrl(
|
|
123
|
-
'https://cdn.example.com/gallery/video/usr-1/video.mp4'
|
|
124
|
-
);
|
|
138
|
+
media.getVideoThumbnailUrl('gallery/video/usr-1/video');
|
|
125
139
|
// → "https://cdn.example.com/gallery/video/usr-1/video/thumbnail.webp"
|
|
126
140
|
```
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
## Enums
|
|
129
143
|
|
|
130
|
-
|
|
144
|
+
Use the provided enums for type-safe image options:
|
|
131
145
|
|
|
132
146
|
```typescript
|
|
133
|
-
import {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
147
|
+
import { IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
|
|
148
|
+
|
|
149
|
+
// IMAGE_FORMAT
|
|
150
|
+
IMAGE_FORMAT.WEBP // 'webp'
|
|
151
|
+
IMAGE_FORMAT.AVIF // 'avif'
|
|
152
|
+
IMAGE_FORMAT.JPEG // 'jpeg'
|
|
153
|
+
IMAGE_FORMAT.PNG // 'png'
|
|
154
|
+
IMAGE_FORMAT.GIF // 'gif'
|
|
155
|
+
IMAGE_FORMAT.ICO // 'ico'
|
|
156
|
+
IMAGE_FORMAT.SVG // 'svg'
|
|
157
|
+
IMAGE_FORMAT.JPG // 'jpg'
|
|
158
|
+
|
|
159
|
+
// IMAGE_RESIZE_TYPE
|
|
160
|
+
IMAGE_RESIZE_TYPE.FIT // 'fit'
|
|
161
|
+
IMAGE_RESIZE_TYPE.FILL // 'fill'
|
|
162
|
+
IMAGE_RESIZE_TYPE.FORCE // 'force'
|
|
163
|
+
IMAGE_RESIZE_TYPE.FILL_DOWN // 'fill-down'
|
|
164
|
+
IMAGE_RESIZE_TYPE.AUTO // 'auto'
|
|
140
165
|
```
|
|
141
166
|
|
|
142
167
|
## Constants
|
|
143
168
|
|
|
144
|
-
The SDK exports constants for type-safe usage:
|
|
145
|
-
|
|
146
169
|
```typescript
|
|
147
170
|
import {
|
|
148
|
-
MEDIA_TYPE,
|
|
149
|
-
MEDIA_PURPOSE,
|
|
150
|
-
MEDIA_STATUS,
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
MEDIA_TYPE, // { IMAGE: 'image', VIDEO: 'video' }
|
|
172
|
+
MEDIA_PURPOSE, // { AVATAR: 'avatar', HERO: 'hero', GALLERY: 'gallery' }
|
|
173
|
+
MEDIA_STATUS, // { PENDING, UPLOADED, PROCESSING, READY, ERROR }
|
|
174
|
+
R2_PATH, // { HLS_PLAYLIST: 'playlist.m3u8', THUMBNAIL: 'thumbnail.webp' }
|
|
175
|
+
MEDIA_DEFAULTS, // { IMAGE_FORMAT: 'webp' }
|
|
153
176
|
} from '@sproux/media-sdk';
|
|
154
177
|
```
|
|
155
178
|
|
|
@@ -157,50 +180,35 @@ import {
|
|
|
157
180
|
|
|
158
181
|
```typescript
|
|
159
182
|
import type {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
ImageResizeType, // 'fit' | 'fill' | 'auto'
|
|
165
|
-
ParsedMediaUrl,
|
|
166
|
-
ImageVariantOptions,
|
|
183
|
+
ImageFormat, // 'webp' | 'avif' | 'jpeg' | 'png' | 'gif' | 'ico' | 'svg' | 'jpg'
|
|
184
|
+
ImageResizeType, // 'fit' | 'fill' | 'force' | 'fill-down' | 'auto'
|
|
185
|
+
SprouxMediaConfig, // { cdnUrl: string }
|
|
186
|
+
ImageUrlOptions, // { extension?, width?, height?, resizeType?, quality? }
|
|
167
187
|
} from '@sproux/media-sdk';
|
|
168
188
|
```
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
This SDK expects media URLs to follow this structure:
|
|
190
|
+
> `ImageFormat` and `ImageResizeType` are string union types derived from their respective enums. You can use either the enum values (`IMAGE_FORMAT.WEBP`) or raw strings (`'webp'`).
|
|
173
191
|
|
|
174
|
-
|
|
175
|
-
{cdnBase}/{purpose}/{type}/{userId}/{filename}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
| Segment | Description | Valid Values |
|
|
179
|
-
| ---------- | ----------------------------------------- | --------------------------------- |
|
|
180
|
-
| `cdnBase` | CDN origin including protocol | Any valid URL origin |
|
|
181
|
-
| `purpose` | Media use case | `avatar`, `hero`, `gallery` |
|
|
182
|
-
| `type` | Media type | `image`, `video` |
|
|
183
|
-
| `userId` | User identifier | Any string |
|
|
184
|
-
| `filename` | File name with extension | `{name}.{extension}` |
|
|
192
|
+
## URL Structure
|
|
185
193
|
|
|
186
|
-
### Image Variant URL
|
|
194
|
+
### Image Variant URL
|
|
187
195
|
|
|
188
196
|
```
|
|
189
|
-
{
|
|
197
|
+
{cdnUrl}/{objectKey}-{width}x{height}-{resizeType}[-q{quality}].{extension}
|
|
190
198
|
```
|
|
191
199
|
|
|
192
200
|
Example: `https://cdn.example.com/avatar/image/usr-1/photo-200x200-fit-q80.webp`
|
|
193
201
|
|
|
194
|
-
### Video HLS URL
|
|
202
|
+
### Video HLS URL
|
|
195
203
|
|
|
196
204
|
```
|
|
197
|
-
{
|
|
205
|
+
{cdnUrl}/{objectKey}/playlist.m3u8
|
|
198
206
|
```
|
|
199
207
|
|
|
200
|
-
### Video Thumbnail URL
|
|
208
|
+
### Video Thumbnail URL
|
|
201
209
|
|
|
202
210
|
```
|
|
203
|
-
{
|
|
211
|
+
{cdnUrl}/{objectKey}/thumbnail.webp
|
|
204
212
|
```
|
|
205
213
|
|
|
206
214
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -27,11 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
MEDIA_STATUS: () => MEDIA_STATUS,
|
|
28
28
|
MEDIA_TYPE: () => MEDIA_TYPE,
|
|
29
29
|
R2_PATH: () => R2_PATH,
|
|
30
|
-
|
|
31
|
-
buildVariantString: () => buildVariantString,
|
|
32
|
-
buildVideoHlsUrl: () => buildVideoHlsUrl,
|
|
33
|
-
buildVideoThumbnailUrl: () => buildVideoThumbnailUrl,
|
|
34
|
-
parseMediaUrl: () => parseMediaUrl
|
|
30
|
+
SprouxMedia: () => SprouxMedia
|
|
35
31
|
});
|
|
36
32
|
module.exports = __toCommonJS(index_exports);
|
|
37
33
|
|
|
@@ -52,136 +48,160 @@ var MEDIA_STATUS = {
|
|
|
52
48
|
READY: "ready",
|
|
53
49
|
ERROR: "error"
|
|
54
50
|
};
|
|
55
|
-
var IMAGE_FORMAT = {
|
|
56
|
-
WEBP
|
|
57
|
-
AVIF
|
|
58
|
-
JPEG
|
|
59
|
-
PNG
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
var IMAGE_FORMAT = /* @__PURE__ */ ((IMAGE_FORMAT2) => {
|
|
52
|
+
IMAGE_FORMAT2["WEBP"] = "webp";
|
|
53
|
+
IMAGE_FORMAT2["AVIF"] = "avif";
|
|
54
|
+
IMAGE_FORMAT2["JPEG"] = "jpeg";
|
|
55
|
+
IMAGE_FORMAT2["PNG"] = "png";
|
|
56
|
+
IMAGE_FORMAT2["GIF"] = "gif";
|
|
57
|
+
IMAGE_FORMAT2["ICO"] = "ico";
|
|
58
|
+
IMAGE_FORMAT2["SVG"] = "svg";
|
|
59
|
+
IMAGE_FORMAT2["JPG"] = "jpg";
|
|
60
|
+
return IMAGE_FORMAT2;
|
|
61
|
+
})(IMAGE_FORMAT || {});
|
|
62
|
+
var IMAGE_RESIZE_TYPE = /* @__PURE__ */ ((IMAGE_RESIZE_TYPE2) => {
|
|
63
|
+
IMAGE_RESIZE_TYPE2["FIT"] = "fit";
|
|
64
|
+
IMAGE_RESIZE_TYPE2["FILL"] = "fill";
|
|
65
|
+
IMAGE_RESIZE_TYPE2["FORCE"] = "force";
|
|
66
|
+
IMAGE_RESIZE_TYPE2["FILL_DOWN"] = "fill-down";
|
|
67
|
+
IMAGE_RESIZE_TYPE2["AUTO"] = "auto";
|
|
68
|
+
return IMAGE_RESIZE_TYPE2;
|
|
69
|
+
})(IMAGE_RESIZE_TYPE || {});
|
|
67
70
|
var R2_PATH = {
|
|
68
71
|
HLS_PLAYLIST: "playlist.m3u8",
|
|
69
72
|
THUMBNAIL: "thumbnail.webp"
|
|
70
73
|
};
|
|
71
74
|
var MEDIA_DEFAULTS = {
|
|
72
|
-
IMAGE_FORMAT:
|
|
75
|
+
IMAGE_FORMAT: "webp" /* WEBP */
|
|
73
76
|
};
|
|
74
77
|
|
|
75
|
-
// src/
|
|
76
|
-
var
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`Invalid media URL path: expected at least 4 segments (purpose/type/userId/filename), got ${segments.length} in "${pathname}"`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const [purpose, type, userId, filename] = segments;
|
|
93
|
-
if (!VALID_PURPOSES.has(purpose)) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Invalid media purpose: "${purpose}". Expected one of: ${[...VALID_PURPOSES].join(", ")}`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (!VALID_TYPES.has(type)) {
|
|
99
|
-
throw new Error(
|
|
100
|
-
`Invalid media type: "${type}". Expected one of: ${[...VALID_TYPES].join(", ")}`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
if (!userId) {
|
|
104
|
-
throw new Error("Missing userId segment in media URL");
|
|
105
|
-
}
|
|
106
|
-
if (!filename) {
|
|
107
|
-
throw new Error("Missing filename segment in media URL");
|
|
108
|
-
}
|
|
109
|
-
const dotIndex = filename.lastIndexOf(".");
|
|
110
|
-
if (dotIndex <= 0) {
|
|
111
|
-
throw new Error(`Invalid filename: "${filename}". Expected format: "name.extension"`);
|
|
112
|
-
}
|
|
113
|
-
const name = filename.slice(0, dotIndex);
|
|
114
|
-
const extension = filename.slice(dotIndex + 1);
|
|
115
|
-
if (!extension) {
|
|
116
|
-
throw new Error(`Missing file extension in filename: "${filename}"`);
|
|
117
|
-
}
|
|
118
|
-
const cdnBase = `${url.protocol}//${url.host}`;
|
|
119
|
-
const objectKey = pathname;
|
|
120
|
-
return {
|
|
121
|
-
originalUrl,
|
|
122
|
-
cdnBase,
|
|
123
|
-
objectKey,
|
|
124
|
-
purpose,
|
|
125
|
-
type,
|
|
126
|
-
userId,
|
|
127
|
-
name,
|
|
128
|
-
extension
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/build-variant-string.ts
|
|
133
|
-
function buildVariantString(options) {
|
|
134
|
-
validateVariantOptions(options);
|
|
135
|
-
const parts = [`${options.width}x${options.height}`, options.resizeType];
|
|
136
|
-
if (options.quality != null) {
|
|
137
|
-
parts.push(`q${options.quality}`);
|
|
138
|
-
}
|
|
139
|
-
return parts.join("-");
|
|
140
|
-
}
|
|
141
|
-
function validateVariantOptions(options) {
|
|
142
|
-
if (!Number.isInteger(options.width) || options.width < 1 || options.width > 4096) {
|
|
143
|
-
throw new Error(`Invalid width: ${options.width}. Must be an integer between 1 and 4096.`);
|
|
144
|
-
}
|
|
145
|
-
if (!Number.isInteger(options.height) || options.height < 1 || options.height > 4096) {
|
|
146
|
-
throw new Error(`Invalid height: ${options.height}. Must be an integer between 1 and 4096.`);
|
|
147
|
-
}
|
|
148
|
-
if (options.quality != null) {
|
|
149
|
-
if (!Number.isInteger(options.quality) || options.quality < 1 || options.quality > 100) {
|
|
150
|
-
throw new Error(
|
|
151
|
-
`Invalid quality: ${options.quality}. Must be an integer between 1 and 100.`
|
|
152
|
-
);
|
|
78
|
+
// src/sproux-media.ts
|
|
79
|
+
var SprouxMedia = class _SprouxMedia {
|
|
80
|
+
static instance = null;
|
|
81
|
+
cdnUrl;
|
|
82
|
+
constructor(config) {
|
|
83
|
+
this.cdnUrl = config.cdnUrl.replace(/\/+$/, "");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Initialize the singleton with CDN configuration.
|
|
87
|
+
* Returns the singleton instance.
|
|
88
|
+
*/
|
|
89
|
+
static init(config) {
|
|
90
|
+
if (_SprouxMedia.instance) {
|
|
91
|
+
return _SprouxMedia.instance;
|
|
153
92
|
}
|
|
93
|
+
return _SprouxMedia.instance = new _SprouxMedia(config);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns the existing singleton instance.
|
|
97
|
+
* Throws if `init()` has not been called.
|
|
98
|
+
*/
|
|
99
|
+
static getInstance() {
|
|
100
|
+
if (!_SprouxMedia.instance) {
|
|
101
|
+
throw new Error("SprouxMedia has not been initialized. Call SprouxMedia.init() first.");
|
|
102
|
+
}
|
|
103
|
+
return _SprouxMedia.instance;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build a CDN URL for an image, optionally with variant transformation.
|
|
107
|
+
*
|
|
108
|
+
* @param objectKey - Object key with extension (e.g. "avatar/image/usr-1/abc.jpg")
|
|
109
|
+
* @param options - Optional image variant options (extension, dimensions, resize type, quality)
|
|
110
|
+
* @returns Full CDN URL — variant URL if options are provided, original URL otherwise
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // With variant options
|
|
114
|
+
* media.getImageUrl('avatar/image/usr-1/abc.jpg', {
|
|
115
|
+
* extension: 'webp', width: 200, height: 200, resizeType: 'fit', quality: 80,
|
|
116
|
+
* });
|
|
117
|
+
* // → "https://cdn.example.com/avatar/image/usr-1/abc-200x200-fit-q80.webp"
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* // Without options — returns the original URL
|
|
121
|
+
* media.getImageUrl('avatar/image/usr-1/abc.jpg');
|
|
122
|
+
* // → "https://cdn.example.com/avatar/image/usr-1/abc.jpg"
|
|
123
|
+
*/
|
|
124
|
+
getImageUrl(objectKey, options) {
|
|
125
|
+
if (!options || Object.keys(options).length === 0) {
|
|
126
|
+
return `${this.cdnUrl}/${objectKey}`;
|
|
127
|
+
}
|
|
128
|
+
this.validateImageOptions(options);
|
|
129
|
+
const { name, extension: originalExt } = this.parseObjectKey(objectKey);
|
|
130
|
+
const ext = options.extension ?? originalExt;
|
|
131
|
+
const variant = this.buildVariantString(options);
|
|
132
|
+
return variant ? `${this.cdnUrl}/${name}-${variant}.${ext}` : `${this.cdnUrl}/${objectKey}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Build a CDN URL for a video HLS playlist.
|
|
136
|
+
*
|
|
137
|
+
* @param objectKey - Object key without extension (e.g. "gallery/video/usr-1/xyz")
|
|
138
|
+
* @returns Full CDN URL for the HLS playlist
|
|
139
|
+
*/
|
|
140
|
+
getVideoHlsUrl(objectKey) {
|
|
141
|
+
return `${this.cdnUrl}/${objectKey}/${R2_PATH.HLS_PLAYLIST}`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Build a CDN URL for a video thumbnail.
|
|
145
|
+
*
|
|
146
|
+
* @param objectKey - Object key without extension (e.g. "gallery/video/usr-1/xyz")
|
|
147
|
+
* @returns Full CDN URL for the thumbnail
|
|
148
|
+
*/
|
|
149
|
+
getVideoThumbnailUrl(objectKey) {
|
|
150
|
+
return `${this.cdnUrl}/${objectKey}/${R2_PATH.THUMBNAIL}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parse an object key into name (path without extension) and extension.
|
|
154
|
+
*/
|
|
155
|
+
parseObjectKey(objectKey) {
|
|
156
|
+
const lastDot = objectKey.lastIndexOf(".");
|
|
157
|
+
if (lastDot === -1) {
|
|
158
|
+
return { name: objectKey, extension: "" };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
name: objectKey.substring(0, lastDot),
|
|
162
|
+
extension: objectKey.substring(lastDot + 1)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build a deterministic variant string from image options.
|
|
167
|
+
*
|
|
168
|
+
* Format: {width}x{height}-{resizeType}[-q{quality}]
|
|
169
|
+
*/
|
|
170
|
+
buildVariantString(options) {
|
|
171
|
+
const parts = [];
|
|
172
|
+
if (options.width != null && options.height != null) {
|
|
173
|
+
parts.push(`${options.width}x${options.height}`);
|
|
174
|
+
}
|
|
175
|
+
if (options.resizeType != null) {
|
|
176
|
+
parts.push(options.resizeType);
|
|
177
|
+
}
|
|
178
|
+
if (options.quality != null) {
|
|
179
|
+
parts.push(`q${options.quality}`);
|
|
180
|
+
}
|
|
181
|
+
return parts.join("-");
|
|
154
182
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// src/build-video-thumbnail-url.ts
|
|
178
|
-
function buildVideoThumbnailUrl(originalUrl) {
|
|
179
|
-
const parsed = parseMediaUrl(originalUrl);
|
|
180
|
-
if (parsed.type !== "video") {
|
|
181
|
-
throw new Error(`Expected video URL, got type "${parsed.type}" in "${originalUrl}"`);
|
|
183
|
+
validateImageOptions(options) {
|
|
184
|
+
if (options.width != null) {
|
|
185
|
+
if (!Number.isInteger(options.width) || options.width < 1 || options.width > 4096) {
|
|
186
|
+
throw new Error(`Invalid width: ${options.width}. Must be an integer between 1 and 4096.`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (options.height != null) {
|
|
190
|
+
if (!Number.isInteger(options.height) || options.height < 1 || options.height > 4096) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Invalid height: ${options.height}. Must be an integer between 1 and 4096.`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (options.quality != null) {
|
|
197
|
+
if (!Number.isInteger(options.quality) || options.quality < 1 || options.quality > 100) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Invalid quality: ${options.quality}. Must be an integer between 1 and 100.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
182
203
|
}
|
|
183
|
-
|
|
184
|
-
}
|
|
204
|
+
};
|
|
185
205
|
// Annotate the CommonJS export names for ESM import in node:
|
|
186
206
|
0 && (module.exports = {
|
|
187
207
|
IMAGE_FORMAT,
|
|
@@ -191,10 +211,6 @@ function buildVideoThumbnailUrl(originalUrl) {
|
|
|
191
211
|
MEDIA_STATUS,
|
|
192
212
|
MEDIA_TYPE,
|
|
193
213
|
R2_PATH,
|
|
194
|
-
|
|
195
|
-
buildVariantString,
|
|
196
|
-
buildVideoHlsUrl,
|
|
197
|
-
buildVideoThumbnailUrl,
|
|
198
|
-
parseMediaUrl
|
|
214
|
+
SprouxMedia
|
|
199
215
|
});
|
|
200
216
|
//# sourceMappingURL=index.cjs.map
|