@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 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 original media URLs.
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
- buildImageVariantUrl,
23
- buildVideoHlsUrl,
24
- buildVideoThumbnailUrl,
25
- parseMediaUrl,
26
- } from '@sproux/media-sdk';
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
- // Build an image variant URL
29
- const imageUrl = buildImageVariantUrl(
30
- 'https://cdn.example.com/avatar/image/usr-1/profile.jpg',
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 = buildVideoHlsUrl(
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 thumbnailUrl = buildVideoThumbnailUrl(
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
- ## API Reference
49
+ ## Singleton Usage
49
50
 
50
- ### `parseMediaUrl(url: string): ParsedMediaUrl`
51
+ ```typescript
52
+ import { SprouxMedia, IMAGE_FORMAT, IMAGE_RESIZE_TYPE } from '@sproux/media-sdk';
51
53
 
52
- Parse a media URL into its structured components.
54
+ // Initialize once at startup
55
+ SprouxMedia.init({ cdnUrl: 'https://cdn.example.com' });
53
56
 
54
- **Expected URL pattern:** `{cdnBase}/{purpose}/{type}/{userId}/{name}.{extension}`
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
- ```typescript
57
- import { parseMediaUrl } from '@sproux/media-sdk';
67
+ // Without options — returns the original URL
68
+ const original = media.getImageUrl('avatar/image/usr-1/photo.jpg');
69
+ ```
58
70
 
59
- const parsed = parseMediaUrl('https://cdn.example.com/avatar/image/usr-1/abc.jpg');
60
- // {
61
- // originalUrl: "https://cdn.example.com/avatar/image/usr-1/abc.jpg",
62
- // cdnBase: "https://cdn.example.com",
63
- // objectKey: "avatar/image/usr-1/abc.jpg",
64
- // purpose: "avatar",
65
- // type: "image",
66
- // userId: "usr-1",
67
- // name: "abc",
68
- // extension: "jpg"
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
- ### `buildImageVariantUrl(originalUrl: string, options: ImageVariantOptions): string`
83
+ ### `SprouxMedia.getInstance(): SprouxMedia`
84
+
85
+ Returns the existing singleton instance. Throws if `init()` has not been called.
73
86
 
74
- Build a CDN URL for an image variant with specific dimensions, resize type, and quality.
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 { buildImageVariantUrl } from '@sproux/media-sdk';
78
-
79
- const url = buildImageVariantUrl(
80
- 'https://cdn.example.com/avatar/image/usr-1/photo.jpg',
81
- {
82
- width: 400,
83
- height: 300,
84
- resizeType: 'fill',
85
- format: 'webp', // optional, defaults to 'webp'
86
- quality: 85, // optional
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
- #### ImageVariantOptions
112
+ #### ImageUrlOptions
93
113
 
94
- | Property | Type | Required | Description |
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
- ### `buildVideoHlsUrl(originalUrl: string): string`
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
- Build an HLS playlist URL from an original video URL.
124
+ ### `media.getVideoHlsUrl(objectKey: string): string`
105
125
 
106
- ```typescript
107
- import { buildVideoHlsUrl } from '@sproux/media-sdk';
126
+ Build an HLS playlist URL from an object key (without extension).
108
127
 
109
- const hlsUrl = buildVideoHlsUrl(
110
- 'https://cdn.example.com/gallery/video/usr-1/video.mp4'
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
- ### `buildVideoThumbnailUrl(originalUrl: string): string`
133
+ ### `media.getVideoThumbnailUrl(objectKey: string): string`
116
134
 
117
- Build a thumbnail URL from an original video URL.
135
+ Build a thumbnail URL from an object key (without extension).
118
136
 
119
137
  ```typescript
120
- import { buildVideoThumbnailUrl } from '@sproux/media-sdk';
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
- ### `buildVariantString(options: ImageVariantOptions): string`
142
+ ## Enums
129
143
 
130
- Build a deterministic variant string from image variant options. This is used internally by `buildImageVariantUrl`.
144
+ Use the provided enums for type-safe image options:
131
145
 
132
146
  ```typescript
133
- import { buildVariantString } from '@sproux/media-sdk';
134
-
135
- buildVariantString({ width: 200, height: 200, resizeType: 'fit' });
136
- // → "200x200-fit"
137
-
138
- buildVariantString({ width: 200, height: 200, resizeType: 'fit', quality: 80 });
139
- // → "200x200-fit-q80"
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, // { IMAGE: 'image', VIDEO: 'video' }
149
- MEDIA_PURPOSE, // { AVATAR: 'avatar', HERO: 'hero', GALLERY: 'gallery' }
150
- MEDIA_STATUS, // { PENDING, UPLOADED, PROCESSING, READY, ERROR }
151
- IMAGE_FORMAT, // { WEBP, AVIF, JPEG, PNG, JPG }
152
- IMAGE_RESIZE_TYPE // { FIT, FILL, AUTO }
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
- MediaType, // 'image' | 'video'
161
- MediaPurpose, // 'avatar' | 'hero' | 'gallery'
162
- MediaStatus, // 'pending' | 'uploaded' | 'processing' | 'ready' | 'error'
163
- ImageFormat, // 'webp' | 'avif' | 'jpeg' | 'png' | 'jpg'
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
- ## URL Structure
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 Structure
194
+ ### Image Variant URL
187
195
 
188
196
  ```
189
- {cdnBase}/{purpose}/image/{userId}/{name}-{width}x{height}-{resizeType}[-q{quality}].{format}
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 Structure
202
+ ### Video HLS URL
195
203
 
196
204
  ```
197
- {cdnBase}/{purpose}/video/{userId}/{name}/playlist.m3u8
205
+ {cdnUrl}/{objectKey}/playlist.m3u8
198
206
  ```
199
207
 
200
- ### Video Thumbnail URL Structure
208
+ ### Video Thumbnail URL
201
209
 
202
210
  ```
203
- {cdnBase}/{purpose}/video/{userId}/{name}/thumbnail.webp
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
- buildImageVariantUrl: () => buildImageVariantUrl,
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: "webp",
57
- AVIF: "avif",
58
- JPEG: "jpeg",
59
- PNG: "png",
60
- JPG: "jpg"
61
- };
62
- var IMAGE_RESIZE_TYPE = {
63
- FIT: "fit",
64
- FILL: "fill",
65
- AUTO: "auto"
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: IMAGE_FORMAT.WEBP
75
+ IMAGE_FORMAT: "webp" /* WEBP */
73
76
  };
74
77
 
75
- // src/parse-media-url.ts
76
- var VALID_PURPOSES = new Set(Object.values(MEDIA_PURPOSE));
77
- var VALID_TYPES = new Set(Object.values(MEDIA_TYPE));
78
- function parseMediaUrl(originalUrl) {
79
- let url;
80
- try {
81
- url = new URL(originalUrl);
82
- } catch {
83
- throw new Error(`Invalid media URL: "${originalUrl}"`);
84
- }
85
- const pathname = url.pathname.startsWith("/") ? url.pathname.slice(1) : url.pathname;
86
- const segments = pathname.split("/");
87
- if (segments.length < 4) {
88
- throw new Error(
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
- // src/build-image-variant-url.ts
158
- function buildImageVariantUrl(originalUrl, options) {
159
- const parsed = parseMediaUrl(originalUrl);
160
- if (parsed.type !== "image") {
161
- throw new Error(`Expected image URL, got type "${parsed.type}" in "${originalUrl}"`);
162
- }
163
- const variant = buildVariantString(options);
164
- const format = options.format ?? MEDIA_DEFAULTS.IMAGE_FORMAT;
165
- return `${parsed.cdnBase}/${parsed.purpose}/${parsed.type}/${parsed.userId}/${parsed.name}-${variant}.${format}`;
166
- }
167
-
168
- // src/build-video-hls-url.ts
169
- function buildVideoHlsUrl(originalUrl) {
170
- const parsed = parseMediaUrl(originalUrl);
171
- if (parsed.type !== "video") {
172
- throw new Error(`Expected video URL, got type "${parsed.type}" in "${originalUrl}"`);
173
- }
174
- return `${parsed.cdnBase}/${parsed.purpose}/video/${parsed.userId}/${parsed.name}/${R2_PATH.HLS_PLAYLIST}`;
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
- return `${parsed.cdnBase}/${parsed.purpose}/video/${parsed.userId}/${parsed.name}/${R2_PATH.THUMBNAIL}`;
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
- buildImageVariantUrl,
195
- buildVariantString,
196
- buildVideoHlsUrl,
197
- buildVideoThumbnailUrl,
198
- parseMediaUrl
214
+ SprouxMedia
199
215
  });
200
216
  //# sourceMappingURL=index.cjs.map