diff --git a/packages/contentchef-media/README.md b/packages/contentchef-media/README.md index 72f6e9a..14a5708 100644 --- a/packages/contentchef-media/README.md +++ b/packages/contentchef-media/README.md @@ -8,30 +8,59 @@ yarn @contentchef/contentchef-media ### Usage -This package provides methods to help you manage and interact with ContentChef's media +This package builds ContentChef media urls. Media can be hosted on **Cloudinary** (legacy) or +**Cloudflare** — you don't pick the provider: pass the media object and the right url is built +for you. -* `createUrl` helps you generate a proper url given a media publicId -* `imageUrl` helps you create an url for an image given a media publicId -* `videoUrl` helps you create an url for a video given a media publicId -* `rawFileUrl` helps you create an url for a raw file (pdf, zip, ecc.) given a media publicId +`createUrl` takes the whole media object (`{ publicId, provider, metadata }`) as it appears in a +published content payload: ```typescript -import { createUrl, imageUrl, videoUrl, rawFileUrl } from '@contentchef/contentchef-node'; +import { createUrl, ResourceType } from '@contentchef/contentchef-media'; -const mediaPublicId = 'publicId'; +// A media field taken straight from a published content payload +const media = content.payload.hero; // { publicId, provider, metadata } -const mediaUrl = createUrl(mediaPublicId); +// Provider and resource type (image / video / raw) are detected from the media object +const url = createUrl(media); -const image = imageUrl(mediaPublicId); +// Pass Cloudinary transformation options in the second argument +const resized = createUrl(media, { resize: { width: 200, height: 100, type: 'fill' } }); -const video = videoUrl(mediaPublicId); +// Override the detected resource type with the optional third argument +const asVideo = createUrl(media, {}, ResourceType.video); +``` + +Three per-type helpers wrap `createUrl` when you want to force the resource type explicitly: + +* `imageUrl(media, options?)` +* `videoUrl(media, options?)` +* `rawFileUrl(media, options?)` + +> **Migrating from v8:** these functions now take the whole media object instead of a bare +> `publicId` string. If you only have a `publicId`, wrap it: `createUrl({ publicId })`. + +### How urls are built -const rawFile = rawFileUrl(mediaPublicId); +The provider is read from `media.provider` (falling back to `media.metadata.provider`), and the +resource type from `media.metadata.resourceType` (defaulting to image). Anything that isn't +explicitly `cloudflare` is treated as Cloudinary, so existing media keep working unchanged. -// If you'd like to pass transformations you can do so in the second argument of each method -const transformations = { - height: 100, - width: 200 -} -const mediaUrl = createUrl(mediaPublicId, transformations); +**Transformations always use the Cloudinary option types.** For Cloudflare media they are mapped +to [Cloudflare Image Resizing](https://developers.cloudflare.com/images/transform-images/transform-via-url/) +parameters and rendered as `https://media.contentchef.io/cdn-cgi/image//`. +For example the resized call above yields: + +``` +https://media.contentchef.io/cdn-cgi/image/width=200,height=100,fit=cover/ ``` + +Notes for Cloudflare media: + +* The base host defaults to `https://media.contentchef.io`; override it per call with + `{ baseUrl: 'https://your-zone.example.com' }`. +* Image resizing is image-only. Video and raw files are served as plain delivery urls + (`https://media.contentchef.io/`) with transformations ignored. +* Only Cloudinary options with a Cloudflare counterpart are mapped (dimensions, `fit`, + `gravity`, `quality`, `format`, `dpr`, `rotate`, `background`, and common `effect`s); + unmappable options are ignored rather than producing a broken url. diff --git a/packages/contentchef-media/package.json b/packages/contentchef-media/package.json index dfaca80..ca17951 100644 --- a/packages/contentchef-media/package.json +++ b/packages/contentchef-media/package.json @@ -1,6 +1,6 @@ { "name": "@contentchef/contentchef-media", - "version": "8.0.0", + "version": "9.0.0-beta.1", "description": "Package for helping managing media with ContentChef", "author": "ContentChef", "maintainers": [ diff --git a/packages/contentchef-media/src/__tests__/createUrl.test.ts b/packages/contentchef-media/src/__tests__/createUrl.test.ts index d176930..518ddb2 100644 --- a/packages/contentchef-media/src/__tests__/createUrl.test.ts +++ b/packages/contentchef-media/src/__tests__/createUrl.test.ts @@ -1,81 +1,194 @@ -import { createUrl, imageUrl, videoUrl, rawFileUrl } from '..'; +import { + createUrl, + imageUrl, + IMedia, + rawFileUrl, + ResourceType, + videoUrl, +} from '..'; +import { + buildCloudflareUrl, + DEFAULT_CLOUDFLARE_BASE_URL, + toCloudflareOptions, +} from '../cloudflare'; -describe('createUrl should', () => { - const publicId = 'test-public-id'; - it('successfully created an url for a resource with https as protocol', () => { - const resource = createUrl(publicId); - const groups = resource.match(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)((\/\w+)*\/)([\w\-.]+[^#?\s]+)(.*)?(#[\w-]+)?$/); - const protocol = groups[2]; - expect(protocol).toEqual('https'); +const cloudinaryMedia = ( + publicId = 'test-public-id', + resourceType = 'image', +): IMedia => ({ + publicId, + provider: 'cloudinary', + metadata: { provider: 'cloudinary', resourceType }, +}); + +const cloudflareMedia = ( + publicId = 'space/img/logo.png', + resourceType = 'image', +): IMedia => ({ + publicId, + provider: 'cloudflare', + metadata: { provider: 'cloudflare', resourceType }, +}); + +describe('createUrl provider detection', () => { + it('treats media without a provider as cloudinary (legacy)', () => { + const url = createUrl({ publicId: 'legacy-id' }); + expect(url).toContain('res.cloudinary.com'); + expect(url).toContain('/image/'); }); - it('successfully created an url for a resource with provided cloud_name', () => { - const cloudName = 'amazingCloudName'; - const resource = createUrl(publicId, {cloud_name: cloudName}); + it('reads the provider from metadata when the top-level field is absent', () => { + const url = createUrl({ publicId: 'space/img/x.png', metadata: { provider: 'cloudflare' } }); + expect(url).toContain('media.contentchef.io'); + }); - expect(resource).toContain(cloudName); + it('lets the top-level provider win over metadata', () => { + const url = createUrl({ + publicId: 'space/img/x.png', + provider: 'cloudflare', + metadata: { provider: 'cloudinary' }, + }); + expect(url).toContain('media.contentchef.io'); }); }); -describe('imageUrl should', () => { - const publicId = 'test-public-id'; - it('successfully created an url for a resource with https as protocol', () => { - const resource = imageUrl(publicId); +describe('createUrl (cloudinary)', () => { + it('builds a secure url with https as protocol', () => { + const resource = createUrl(cloudinaryMedia()); const groups = resource.match(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)((\/\w+)*\/)([\w\-.]+[^#?\s]+)(.*)?(#[\w-]+)?$/); - const protocol = groups[2]; - expect(protocol).toEqual('https'); + expect(groups[2]).toEqual('https'); }); - it('successfully created an url for a resource with provided cloud_name', () => { - const cloudName = 'amazingCloudName'; - const resource = imageUrl(publicId, {cloud_name: cloudName}); - expect(resource).toContain(cloudName); + it('honours a provided cloud_name', () => { + expect(createUrl(cloudinaryMedia(), { cloud_name: 'amazingCloudName' })).toContain('amazingCloudName'); }); - it('have /image/ in generated url', () => { - const resource = imageUrl(publicId); - expect(resource).toContain('/image/'); - }) + it('infers image/video/raw from metadata', () => { + expect(createUrl(cloudinaryMedia('id', 'image'))).toContain('/image/'); + expect(createUrl(cloudinaryMedia('id', 'video'))).toContain('/video/'); + expect(createUrl(cloudinaryMedia('id', 'raw'))).toContain('/raw/'); + }); + + it('defaults to image when metadata has no resource type', () => { + expect(createUrl({ publicId: 'id' })).toContain('/image/'); + }); + + it('lets the explicit resourceType argument override metadata', () => { + expect(createUrl(cloudinaryMedia('id', 'image'), {}, ResourceType.video)).toContain('/video/'); + }); }); -describe('videoUrl should', () => { - const publicId = 'test-public-id'; - it('successfully created an url for a resource with https as protocol', () => { - const resource = videoUrl(publicId); - const groups = resource.match(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)((\/\w+)*\/)([\w\-.]+[^#?\s]+)(.*)?(#[\w-]+)?$/); - const protocol = groups[2]; - expect(protocol).toEqual('https'); +describe('createUrl (cloudflare)', () => { + it('builds a /cdn-cgi/image/ url for images off the default base', () => { + const url = createUrl(cloudflareMedia('space/img/logo.png'), { + resize: { width: 100, height: 200, type: 'fill' }, + }); + expect(url).toBe(`${DEFAULT_CLOUDFLARE_BASE_URL}/cdn-cgi/image/width=100,height=200,fit=cover/space/img/logo.png`); + }); + + it('returns a plain delivery url for images when nothing maps', () => { + expect(createUrl(cloudflareMedia('space/img/logo.png'))).toBe( + `${DEFAULT_CLOUDFLARE_BASE_URL}/space/img/logo.png`, + ); }); - it('successfully created an url for a resource with provided cloud_name', () => { - const cloudName = 'amazingCloudName'; - const resource = videoUrl(publicId, {cloud_name: cloudName}); - expect(resource).toContain(cloudName); + it('serves video as plain delivery (transformations ignored)', () => { + const url = createUrl(cloudflareMedia('space/video/clip.mp4', 'video'), { resize: { width: 100 } }); + expect(url).toBe(`${DEFAULT_CLOUDFLARE_BASE_URL}/space/video/clip.mp4`); + expect(url).not.toContain('cdn-cgi'); }); - it('have /image/ in generated url', () => { - const resource = videoUrl(publicId); - expect(resource).toContain('/video/'); - }) + it('serves raw files as plain delivery', () => { + expect(createUrl(cloudflareMedia('space/raw/doc.pdf', 'raw'))).toBe( + `${DEFAULT_CLOUDFLARE_BASE_URL}/space/raw/doc.pdf`, + ); + }); + + it('honours a baseUrl override and strips redundant slashes', () => { + const url = createUrl(cloudflareMedia('/space/img/logo.png'), { + baseUrl: 'https://cdn.example.com/', + resize: { width: 50 }, + }); + expect(url).toBe('https://cdn.example.com/cdn-cgi/image/width=50/space/img/logo.png'); + }); + + it('does not leak baseUrl or cloud_name into cloudflare params', () => { + const url = createUrl(cloudflareMedia('a/b.png'), { + baseUrl: 'https://cdn.example.com', + cloud_name: 'x', + resize: { width: 50 }, + }); + expect(url).toBe('https://cdn.example.com/cdn-cgi/image/width=50/a/b.png'); + }); }); -describe('rawFileUrl should', () => { - const publicId = 'test-public-id'; - it('successfully created an url for a resource with https as protocol', () => { - const resource = rawFileUrl(publicId); - const groups = resource.match(/^((http[s]?|ftp):\/)?\/?([^:/\s]+)((\/\w+)*\/)([\w\-.]+[^#?\s]+)(.*)?(#[\w-]+)?$/); - const protocol = groups[2]; - expect(protocol).toEqual('https'); +describe('per-type helpers force the resource type', () => { + it('imageUrl / videoUrl / rawFileUrl override the inferred type', () => { + expect(imageUrl(cloudinaryMedia('id', 'video'))).toContain('/image/'); + expect(videoUrl(cloudinaryMedia('id', 'image'))).toContain('/video/'); + expect(rawFileUrl(cloudinaryMedia('id', 'image'))).toContain('/raw/'); + }); + + it('imageUrl maps a cloudflare image', () => { + expect(imageUrl(cloudflareMedia('a/b.png'), { resize: { width: 10 } })).toBe( + `${DEFAULT_CLOUDFLARE_BASE_URL}/cdn-cgi/image/width=10/a/b.png`, + ); }); - it('successfully created an url for a resource with provided cloud_name', () => { - const cloudName = 'amazingCloudName'; - const resource = rawFileUrl(publicId, {cloud_name: cloudName}); - expect(resource).toContain(cloudName); + it('videoUrl serves a cloudflare video as plain delivery', () => { + expect(videoUrl(cloudflareMedia('a/clip.mp4'))).toBe(`${DEFAULT_CLOUDFLARE_BASE_URL}/a/clip.mp4`); }); - it('have /image/ in generated url', () => { - const resource = rawFileUrl(publicId); - expect(resource).toContain('/raw/'); - }) + it('rawFileUrl serves a cloudflare raw file as plain delivery', () => { + expect(rawFileUrl(cloudflareMedia('a/doc.pdf'))).toBe(`${DEFAULT_CLOUDFLARE_BASE_URL}/a/doc.pdf`); + }); +}); + +describe('toCloudflareOptions mapping', () => { + it('maps resize dimensions and fit', () => { + expect(toCloudflareOptions({ resize: { width: 10, height: 20, type: 'fit' } })) + .toEqual(['width=10', 'height=20', 'fit=contain']); + }); + + it('maps compass gravity to sides and corners', () => { + expect(toCloudflareOptions({ gravity: 'north' as any })).toEqual(['gravity=top']); + expect(toCloudflareOptions({ gravity: 'south_east' as any })).toEqual(['gravity=1x1']); + expect(toCloudflareOptions({ gravity: 'auto' as any })).toEqual(['gravity=auto']); + }); + + it('maps format, falling back to fetchFormat, and normalises jpg', () => { + expect(toCloudflareOptions({ format: 'auto' })).toEqual(['format=auto']); + expect(toCloudflareOptions({ fetchFormat: 'jpg' })).toEqual(['format=jpeg']); + }); + + it('passes numeric quality through and drops auto quality', () => { + expect(toCloudflareOptions({ quality: 75 })).toEqual(['quality=75']); + expect(toCloudflareOptions({ quality: 'auto' })).toEqual([]); + }); + + it('only allows cloudflare-supported rotations', () => { + expect(toCloudflareOptions({ rotate: 90 })).toEqual(['rotate=90']); + expect(toCloudflareOptions({ rotate: 45 })).toEqual([]); + }); + + it('escapes the hash in a hex background', () => { + expect(toCloudflareOptions({ background: '#ff0000' })).toEqual(['background=%23ff0000']); + }); + + it('maps common effects with approximate scaling', () => { + expect(toCloudflareOptions({ effect: { name: 'brightness', value: 50 } })).toEqual(['brightness=1.5']); + expect(toCloudflareOptions({ effect: { name: 'grayscale' } })).toEqual(['saturation=0']); + expect(toCloudflareOptions({ effect: { name: 'sepia', value: 80 } })).toEqual([]); + }); +}); + +describe('buildCloudflareUrl', () => { + it('joins multiple options with commas', () => { + const url = buildCloudflareUrl('a/b.png', { resize: { width: 100 }, format: 'auto', quality: 80 }); + expect(url).toBe(`${DEFAULT_CLOUDFLARE_BASE_URL}/cdn-cgi/image/width=100,format=auto,quality=80/a/b.png`); + }); + + it('returns a plain delivery url when no options map to params', () => { + expect(buildCloudflareUrl('space/raw/doc.pdf')).toBe(`${DEFAULT_CLOUDFLARE_BASE_URL}/space/raw/doc.pdf`); + }); }); diff --git a/packages/contentchef-media/src/cloudflare.ts b/packages/contentchef-media/src/cloudflare.ts new file mode 100644 index 0000000..6e719c1 --- /dev/null +++ b/packages/contentchef-media/src/cloudflare.ts @@ -0,0 +1,175 @@ +import { TransformerOption } from '@cld-apis/types'; + +export const DEFAULT_CLOUDFLARE_BASE_URL = 'https://media.contentchef.io'; + +const FIT_MAP: { [resizeType: string]: string } = { + crop: 'crop', + imaggaCrop: 'crop', + fill: 'cover', + fill_pad: 'cover', + lfill: 'cover', + thumb: 'cover', + fit: 'contain', + mfit: 'contain', + scale: 'contain', + limit: 'scale-down', + imaggaScale: 'scale-down', + pad: 'pad', + lpad: 'pad', + mpad: 'pad', +}; + +const GRAVITY_MAP: { [gravity: string]: string } = { + auto: 'auto', + 'auto:subject': 'auto', + faces: 'auto', + 'faces:center': 'auto', + face: 'face', + 'face:center': 'face', + north: 'top', + south: 'bottom', + east: 'right', + west: 'left', + center: '0.5x0.5', + north_west: '0x0', + north_east: '1x0', + south_west: '0x1', + south_east: '1x1', +}; + +const FORMAT_MAP: { [format: string]: string } = { + auto: 'auto', + webp: 'webp', + avif: 'avif', + jpg: 'jpeg', + jpeg: 'jpeg', + png: 'png', + gif: 'gif', + json: 'json', +}; + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} + +function round2(value: number): number { + return Math.round(value * 100) / 100; +} + +function toNumber(value: unknown): number | undefined { + if (value === null || value === undefined || value === '') { + return undefined; + } + const n = Number(value); + return isNaN(n) ? undefined : n; +} + +function mapEffect(effect: { name: string; value?: number | string | string[] | number[] }): string | undefined { + const raw = Array.isArray(effect.value) ? effect.value[0] : effect.value; + const value = toNumber(raw); + switch (effect.name) { + case 'blur': + return value !== undefined ? `blur=${clamp(Math.round(value / 8), 1, 250)}` : undefined; + case 'brightness': + return value !== undefined ? `brightness=${round2(1 + value / 100)}` : undefined; + case 'contrast': + return value !== undefined ? `contrast=${round2(1 + value / 100)}` : undefined; + case 'saturation': + return value !== undefined ? `saturation=${round2(1 + value / 100)}` : undefined; + case 'sharpen': + return `sharpen=${value !== undefined ? clamp(value, 0, 10) : 1}`; + case 'grayscale': + case 'blackwhite': + return 'saturation=0'; + default: + return undefined; + } +} + +function encodeBackground(background: string): string { + return background.replace(/#/g, '%23'); +} + +export function toCloudflareOptions(options: TransformerOption = {}): string[] { + const params: string[] = []; + const { resize, gravity, quality, format, fetchFormat, dpr, rotate, background, effect } = options; + + if (resize) { + if (resize.width !== undefined && resize.width !== null) { + params.push(`width=${resize.width}`); + } + if (resize.height !== undefined && resize.height !== null) { + params.push(`height=${resize.height}`); + } + if (resize.type !== undefined && resize.type !== null) { + const fit = FIT_MAP[String(resize.type)]; + if (fit) { + params.push(`fit=${fit}`); + } + } + } + + if (gravity !== undefined && gravity !== null) { + const mapped = GRAVITY_MAP[String(gravity)]; + if (mapped) { + params.push(`gravity=${mapped}`); + } + } + + const fmt = format || fetchFormat; + if (fmt) { + const mapped = FORMAT_MAP[String(fmt).toLowerCase()]; + if (mapped) { + params.push(`format=${mapped}`); + } + } + + if (quality !== undefined && quality !== null) { + const numericQuality = toNumber(quality); + if (numericQuality !== undefined) { + params.push(`quality=${clamp(Math.round(numericQuality), 1, 100)}`); + } + } + + const dprValue = toNumber(dpr); + if (dprValue !== undefined) { + params.push(`dpr=${dprValue}`); + } + + const rotateValue = toNumber(rotate); + if (rotateValue !== undefined && (rotateValue === 90 || rotateValue === 180 || rotateValue === 270)) { + params.push(`rotate=${rotateValue}`); + } + + if (background) { + params.push(`background=${encodeBackground(background)}`); + } + + if (effect && effect.name) { + const mappedEffect = mapEffect(effect); + if (mappedEffect) { + params.push(mappedEffect); + } + } + + return params; +} + +function normalizeBase(baseUrl?: string): string { + const base = baseUrl || DEFAULT_CLOUDFLARE_BASE_URL; + return base.replace(/\/+$/, ''); +} + +function normalizeSource(publicId: string): string { + return String(publicId).replace(/^\/+/, ''); +} + +export function buildCloudflareUrl(publicId: string, options: TransformerOption = {}, baseUrl?: string): string { + const base = normalizeBase(baseUrl); + const source = normalizeSource(publicId); + const params = toCloudflareOptions(options); + if (params.length === 0) { + return `${base}/${source}`; + } + return `${base}/cdn-cgi/image/${params.join(',')}/${source}`; +} diff --git a/packages/contentchef-media/src/index.ts b/packages/contentchef-media/src/index.ts index dcd20d6..13b7a30 100644 --- a/packages/contentchef-media/src/index.ts +++ b/packages/contentchef-media/src/index.ts @@ -1,46 +1,142 @@ import { TransformerOption, TransformerVideoOption } from '@cld-apis/types'; -import buildUrl from 'cloudinary-build-url'; +import cloudinaryBuildUrl from 'cloudinary-build-url'; +import { buildCloudflareUrl } from './cloudflare'; +import { IMedia, MediaProvider, resolveProvider } from './types'; -export type IMediaOptions = (TransformerOption | TransformerVideoOption) & {cloud_name?: string}; -export type IImageOptions = TransformerOption & {cloud_name?: string}; -export type IVideoOptions = TransformerVideoOption & {cloud_name?: string}; +export type IMediaOptions = (TransformerOption | TransformerVideoOption) & { + cloud_name?: string; +}; +export type IImageOptions = TransformerOption & { cloud_name?: string }; +export type IVideoOptions = TransformerVideoOption & { cloud_name?: string }; + +export type BaseUrlAware = { baseUrl?: string }; +export type IMediaUrlOptions = IMediaOptions & BaseUrlAware; +export type IMediaImageOptions = IImageOptions & BaseUrlAware; +export type IMediaVideoOptions = IVideoOptions & BaseUrlAware; +export type IMediaFileOptions = IMediaOptions & BaseUrlAware; export enum ResourceType { - image = 'image', - video = 'video', - raw = 'raw' -}; + image = 'image', + video = 'video', + raw = 'raw', +} const defaultCloudName = 'contentchef'; -export function createUrl(publicId: string, options: IMediaOptions = {cloud_name: defaultCloudName}, resourceType: ResourceType = ResourceType.image): string { - const {cloud_name, ...transformations} = options; - return buildUrl(publicId, {cloud: {cloudName: cloud_name || defaultCloudName, resourceType, secure: true}, transformations}); +function inferResourceType(media: IMedia): ResourceType { + const resourceType = media.metadata && media.metadata.resourceType; + if (resourceType === ResourceType.video) { + return ResourceType.video; + } + if (resourceType === ResourceType.raw) { + return ResourceType.raw; + } + return ResourceType.image; } -export function imageUrl(publicId: string, options?: IImageOptions) { - return createUrl(publicId, options, ResourceType.image); +/** + * Builds a delivery URL for a media object taken from a published content payload. + * + * The provider and the resource type are detected from the media object itself + * (`metadata.resourceType`, defaulting to image); pass `resourceType` to force it. + * + * @param media - Media reference, e.g. `{ publicId, provider, metadata }`. + * @param options - Transformation options; `baseUrl` overrides the default delivery host. + * @param resourceType - Overrides the resource type inferred from `metadata`. + * @returns The media delivery URL. + * + * @example + * createUrl(media); + * createUrl(media, { resize: { width: 200 } }); + * createUrl(media, {}, ResourceType.video); + */ +export function createUrl( + media: IMedia, + options: IMediaUrlOptions = {}, + resourceType?: ResourceType, +): string { + const type = resourceType || inferResourceType(media); + const { cloud_name, baseUrl, ...transformations } = options; + if (resolveProvider(media) === MediaProvider.cloudflare) { + const cloudflareOptions = + type === ResourceType.image + ? (transformations as TransformerOption) + : undefined; + return buildCloudflareUrl(media.publicId, cloudflareOptions, baseUrl); + } + return cloudinaryBuildUrl(media.publicId, { + cloud: { + cloudName: cloud_name || defaultCloudName, + resourceType: type, + secure: true, + }, + transformations, + }); } -export function videoUrl(publicId: string, options?: IVideoOptions) { - return createUrl(publicId, options, ResourceType.video); +/** + * Builds a delivery URL for the given media, forcing the image resource type. + * + * @param media - Media reference, e.g. `{ publicId, provider, metadata }`. + * @param options - Transformation options; `baseUrl` overrides the default delivery host. + * @returns The image delivery URL. + */ +export function imageUrl(media: IMedia, options?: IMediaImageOptions) { + return createUrl(media, options, ResourceType.image); } -export function rawFileUrl(publicId: string, options?: IMediaOptions) { - return createUrl(publicId, options, ResourceType.raw); +/** + * Builds a delivery URL for the given media, forcing the video resource type. + * + * @param media - Media reference, e.g. `{ publicId, provider, metadata }`. + * @param options - Transformation options; `baseUrl` overrides the default delivery host. + * @returns The video delivery URL. + */ +export function videoUrl(media: IMedia, options?: IMediaVideoOptions) { + return createUrl(media, options, ResourceType.video); } +/** + * Builds a delivery URL for the given media (pdf, zip, etc.), forcing the raw resource type. + * + * @param media - Media reference, e.g. `{ publicId, provider, metadata }`. + * @param options - Transformation options; `baseUrl` overrides the default delivery host. + * @returns The raw-file delivery URL. + */ +export function rawFileUrl(media: IMedia, options?: IMediaFileOptions) { + return createUrl(media, options, ResourceType.raw); +} + +export { IMedia, IMediaMetadata } from './types'; + export { - AudioCodec, Border, ColorSpace, CompassGravity, Condition, - ConditionExpression, CustomFunction, Effect, Expression, - Flag, FPS, FPSType, Gravity, - Offset, - Position, - Radius, - Resize, - ResizeType, - Rotation, StringValue, TextStyle, Transformation, TransformerBaseOptions, - TransformerOption, - TransformerVideoOption, Variable, VColorSpace, VEffect, VFlag + AudioCodec, + Border, + ColorSpace, + CompassGravity, + Condition, + ConditionExpression, + CustomFunction, + Effect, + Expression, + Flag, + FPS, + FPSType, + Gravity, + Offset, + Position, + Radius, + Resize, + ResizeType, + Rotation, + StringValue, + TextStyle, + Transformation, + TransformerBaseOptions, + TransformerOption, + TransformerVideoOption, + Variable, + VColorSpace, + VEffect, + VFlag, } from '@cld-apis/types'; - diff --git a/packages/contentchef-media/src/types.ts b/packages/contentchef-media/src/types.ts new file mode 100644 index 0000000..59ceb10 --- /dev/null +++ b/packages/contentchef-media/src/types.ts @@ -0,0 +1,26 @@ +export enum MediaProvider { + cloudinary = 'cloudinary', + cloudflare = 'cloudflare', +} + +export interface IMediaMetadata { + provider?: MediaProvider | string; + resourceType?: string; + format?: string; + width?: number; + height?: number; + aspectRatio?: number; + name?: string; + [key: string]: unknown; +} + +export interface IMedia { + publicId: string; + provider?: MediaProvider | string; + metadata?: IMediaMetadata; +} + +export function resolveProvider(media: IMedia): MediaProvider { + const provider = (media && media.provider) || (media && media.metadata && media.metadata.provider); + return provider === MediaProvider.cloudflare ? MediaProvider.cloudflare : MediaProvider.cloudinary; +}