Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/docs/content/docs/getting-started/features.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,30 @@ const posterUrl = tmdb.images.poster(movie.poster_path, "w500");
Default sizes can be configured once in `TMDBOptions.images` and are applied automatically when no
size is specified at call time. See the [Images option](/docs/getting-started/options/images) for the full reference.

### Autocomplete Image Paths

If you prefer API responses to already contain full image URLs, enable `images.autocomplete_paths`.
This opt-in transformation rewrites supported relative image fields such as `poster_path` and
`backdrop_path` using your configured image defaults.

```ts
const tmdb = new TMDB(apiKey, {
images: {
autocomplete_paths: true,
default_image_sizes: {
posters: "w500",
},
},
});

const movie = await tmdb.movies.details({ movie_id: 550 });

console.log(movie.poster_path);
// → "https://image.tmdb.org/t/p/w500/e1mjopzAS2KNsvpbpahQ1a6SkSn.jpg"
```

This is disabled by default so existing consumers still receive the original TMDB relative paths.

---

## Error Handling
Expand Down
47 changes: 47 additions & 0 deletions apps/docs/content/docs/getting-started/options/images.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ description: Configure image URL generation, HTTPS, and default sizes for all im
The `images` option controls how the client builds image URLs.
You can toggle HTTPS and set default sizes for each image category so they are applied
automatically across all responses without needing to specify a size on every call.
It can also opt in to expanding relative TMDB image paths returned by the API into full URLs.

```ts
const tmdb = new TMDB(apiKey, {
images: {
secure_images_url: true,
autocomplete_paths: true,
default_image_sizes: {
posters: "w500",
backdrops: "w1280",
Expand All @@ -36,6 +38,48 @@ For all standard use cases, leave it as `true`.

---

### `autocomplete_paths`

**Type:** `boolean` · **Default:** `false`

When enabled, the client automatically rewrites supported image path fields in API responses into
full image URLs using the current image configuration.

This affects relative TMDB paths such as `poster_path`, `backdrop_path`, `profile_path`,
`logo_path`, `still_path`, and `file_path` values inside image collections like `posters` or
`backdrops`.

```ts
const tmdb = new TMDB(apiKey, {
images: {
autocomplete_paths: true,
default_image_sizes: {
posters: "original",
},
},
});

const movie = await tmdb.movies.details({ movie_id: 550 });

console.log(movie.poster_path);
// "https://image.tmdb.org/t/p/original/e1mjopzAS2KNsvpbpahQ1a6SkSn.jpg"
```

If `default_image_sizes` is not configured, the built-in per-type defaults are used instead:

| Image Type | Default Size |
| --------------- | ------------ |
| `poster_path` | `w500` |
| `backdrop_path` | `w780` |
| `logo_path` | `w185` |
| `profile_path` | `w185` |
| `still_path` | `w300` |

This option is disabled by default to preserve the original TMDB response semantics, where image
fields contain relative paths that can be passed to `tmdb.images.*` manually.

---

### `default_image_sizes`

**Type:** `Partial<DefaultImageSizesConfig>` · **Optional**
Expand Down Expand Up @@ -73,6 +117,9 @@ https://image.tmdb.org/t/p/w500/abc123.jpg
> Prefer `original` only when you need the full-resolution asset and are willing to accept
> larger file sizes. For UI thumbnails, sized variants like `w500` or `w780` are recommended.

When `autocomplete_paths` is enabled, these defaults are also used to expand image fields returned
by API responses.

### Useful Links

<Cards>
Expand Down
25 changes: 16 additions & 9 deletions apps/docs/content/docs/getting-started/options/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const tmdb = new TMDB("your-api-key", {
timezone: "Europe/Rome",
images: {
secure_images_url: true,
autocomplete_paths: true,
default_image_sizes: {
posters: "w500",
backdrops: "w1280",
Expand All @@ -26,15 +27,15 @@ const tmdb = new TMDB("your-api-key", {

## Available Options

| Option | Type | Description |
| --------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `language` | `Language` | Default language for all responses (ISO 639-1 or primary translation code). |
| `region` | `CountryISO3166_1` | Default region for localized content like release dates and watch providers. |
| `images` | `ImagesConfig` | Configuration for image base URLs and default image sizes. |
| `timezone` | `Timezone` | Default timezone for TV series airing time calculations. |
| `logger` | `boolean \| TMDBLoggerFn` | Enable the built-in console logger or supply a custom logging function. |
| `deduplication` | `boolean` | Deduplicate concurrent identical requests. Defaults to `true`. |
| `interceptors` | `{ request?: ..., response?: { onSuccess?, onError? } }` | Hook into the request/response lifecycle before or after each API call. |
| Option | Type | Description |
| --------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `language` | `Language` | Default language for all responses (ISO 639-1 or primary translation code). |
| `region` | `CountryISO3166_1` | Default region for localized content like release dates and watch providers. |
| `images` | `ImagesConfig` | Configuration for image base URLs, default image sizes, and optional response path autocompletion. |
| `timezone` | `Timezone` | Default timezone for TV series airing time calculations. |
| `logger` | `boolean \| TMDBLoggerFn` | Enable the built-in console logger or supply a custom logging function. |
| `deduplication` | `boolean` | Deduplicate concurrent identical requests. Defaults to `true`. |
| `interceptors` | `{ request?: ..., response?: { onSuccess?, onError? } }` | Hook into the request/response lifecycle before or after each API call. |

---

Expand Down Expand Up @@ -83,11 +84,13 @@ See the [Region reference](/docs/getting-started/options/region) for all support

Configures how image URLs are generated. Includes options to toggle HTTPS and set default sizes
for each image category so you don't need to pass sizes on every individual request.
It can also opt in to rewriting relative image paths in API responses to full URLs.

```ts
const tmdb = new TMDB(apiKey, {
images: {
secure_images_url: true,
autocomplete_paths: true,
default_image_sizes: {
posters: "w500",
backdrops: "w1280",
Expand All @@ -97,6 +100,10 @@ const tmdb = new TMDB(apiKey, {
});
```

With `autocomplete_paths: true`, fields like `poster_path` are returned as complete URLs instead
of TMDB-relative paths. See the [Images configuration reference](/docs/getting-started/options/images)
for supported fields, defaults, and examples.

See the [Images configuration reference](/docs/getting-started/options/images) for all available size options per category.

---
Expand Down
16 changes: 12 additions & 4 deletions apps/docs/content/docs/types/options/images.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ All image URLs are composed of a base URL, a size, and a file path.

Top-level image configuration passed to the constructor via the [`images`](/options#images) option.

| Property | Type | Default | Description |
| --------------------- | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `secure_images_url` | `boolean` | `true` | When `true`, uses the HTTPS base URL. Set to `false` only if HTTPS is unavailable in your environment. |
| `default_image_sizes` | [`Partial<DefaultImageSizesConfig>`](#defaultimagesizesconfig) | — | Default sizes for each image category. Applied automatically when no size is specified at call time. |
| Property | Type | Default | Description |
| --------------------- | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `secure_images_url` | `boolean` | `true` | When `true`, uses the HTTPS base URL. Set to `false` only if HTTPS is unavailable in your environment. |
| `autocomplete_paths` | `boolean` | `false` | When `true`, expands supported relative image paths in API responses to full URLs using the configured image sizes. |
| `default_image_sizes` | [`Partial<DefaultImageSizesConfig>`](#defaultimagesizesconfig) | — | Default sizes for each image category. Applied automatically when no size is specified at call time. |

`autocomplete_paths` is opt-in to avoid changing the default TMDB response shape semantics.
When enabled, known image fields such as `poster_path`, `backdrop_path`, `profile_path`,
`logo_path`, `still_path`, and image collection `file_path` values are converted to final URLs.

If a category does not have an explicit value in `default_image_sizes`, the built-in method
defaults are used: posters `w500`, backdrops `w780`, logos `w185`, profiles `w185`, stills `w300`.

---

Expand Down
12 changes: 9 additions & 3 deletions packages/tmdb/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TMDBAPIErrorResponse, TMDBError } from "./errors/tmdb";
import { ImageAPI } from "./images/images";
import type { ImagesConfig } from "./types/config/images";
import { TMDBLogger, TMDBLoggerFn } from "./utils/logger";
import { isJwt } from "./utils";
import type {
Expand All @@ -23,12 +25,14 @@ export class ApiClient {
private requestInterceptors: RequestInterceptor[];
private onSuccessInterceptor?: ResponseSuccessInterceptor;
private onErrorInterceptor?: ResponseErrorInterceptor;
private imageApi?: ImageAPI;

constructor(
accessToken: string,
options: {
logger?: boolean | TMDBLoggerFn;
deduplication?: boolean;
images?: ImagesConfig;
interceptors?: {
request?: RequestInterceptor | RequestInterceptor[];
response?: { onSuccess?: ResponseSuccessInterceptor; onError?: ResponseErrorInterceptor };
Expand All @@ -42,6 +46,7 @@ export class ApiClient {
this.requestInterceptors = raw == null ? [] : Array.isArray(raw) ? raw : [raw];
this.onSuccessInterceptor = options.interceptors?.response?.onSuccess;
this.onErrorInterceptor = options.interceptors?.response?.onError;
this.imageApi = options.images?.autocomplete_paths ? new ImageAPI(options.images) : undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. autocomplete_paths gates url expansion 📎 Requirement gap ≡ Correctness

Image path autocompletion is only enabled when options.images?.autocomplete_paths is explicitly
set, so TMDB image path fields can still be returned as raw partial paths even when default image
options exist. This violates the requirement to autocomplete image path fields to full URLs using
default options rather than requiring an opt-in flag.
Agent Prompt
## Issue description
The API response transformation only autocompletes TMDB image path fields when `images.autocomplete_paths` is explicitly enabled, which contradicts the requirement to expand image paths using default options.

## Issue Context
The response pipeline already supports transforming sanitized responses via `ImageAPI.autocompleteImagePaths()`, but transformer construction is gated behind `autocomplete_paths`, and the config docs explicitly describe it as disabled by default.

## Fix Focus Areas
- packages/tmdb/src/client.ts[49-49]
- packages/tmdb/src/client.ts[265-272]
- packages/tmdb/src/types/config/images.ts[65-72]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

/**
Expand Down Expand Up @@ -257,12 +262,13 @@ export class ApiClient {

const data = await res.json();
const sanitized = this.sanitizeNulls<T>(data);
const transformed = this.imageApi ? this.imageApi.autocompleteImagePaths(sanitized) : sanitized;

if (this.onSuccessInterceptor) {
const result = await this.onSuccessInterceptor(sanitized);
return result !== undefined ? (result as T) : sanitized;
const result = await this.onSuccessInterceptor(transformed);
return result !== undefined ? (result as T) : transformed;
}

return sanitized;
return transformed;
}
}
97 changes: 96 additions & 1 deletion packages/tmdb/src/images/images.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { ImagesConfig } from "../types";
import { ImageCollectionKey, ImagesConfig } from "../types";
import { BackdropSize, IMAGE_BASE_URL, IMAGE_SECURE_BASE_URL, LogoSize, PosterSize, ProfileSize, StillSize } from "../types/config/images";
import { isPlainObject } from "../utils";

const IMAGE_PATH_BUILDERS = {
backdrop_path: "backdrop",
logo_path: "logo",
poster_path: "poster",
profile_path: "profile",
still_path: "still",
} as const;

const IMAGE_COLLECTION_BUILDERS: Record<ImageCollectionKey, keyof typeof IMAGE_PATH_BUILDERS | "logo_path"> = {
backdrops: "backdrop_path",
logos: "logo_path",
posters: "poster_path",
profiles: "profile_path",
stills: "still_path",
};

type ImagePathKey = keyof typeof IMAGE_PATH_BUILDERS;

export class ImageAPI {
private options: ImagesConfig;
Expand Down Expand Up @@ -32,4 +51,80 @@ export class ImageAPI {
public still(path: string, size: StillSize = this.options.default_image_sizes?.still || "w300"): string {
return this.buildUrl(path, size);
}

/**
* Recursively processes an object or array to autocomplete image paths by transforming string values
* and tracking the current image collection context.
*
* @template T - The type of the value being processed
* @param value - The value to process (can be an object, array, string, or primitive)
* @param collectionKey - Optional image collection key to maintain context during recursion
* @returns The processed value with autocompleted image paths, maintaining the original type
*
* @remarks
* - Arrays are recursively mapped over each entry
* - String values are transformed using {@link transformPathValue}
* - Object keys that match image collection keys update the collection context
* - Non-plain objects (e.g. Date/class instances) are returned unchanged
*/
public autocompleteImagePaths<T>(value: T, collectionKey?: ImageCollectionKey): T {
if (Array.isArray(value)) {
return value.map((entry) => this.autocompleteImagePaths(entry, collectionKey)) as T;
}

if (!isPlainObject(value)) {
return value;
}

const transformed: Record<string, unknown> = Object.create(null);
for (const [key, entry] of Object.entries(value)) {
// Guard against prototype pollution vectors in untrusted response data
if (key === "__proto__" || key === "constructor" || key === "prototype") {
transformed[key] = entry;
continue;
}

if (typeof entry === "string") {
transformed[key] = this.transformPathValue(key, entry, collectionKey);
continue;
}

const nextCollectionKey = this.isImageCollectionKey(key) ? key : collectionKey;
transformed[key] = this.autocompleteImagePaths(entry, nextCollectionKey);
}

return transformed as T;
}

// MARK: Private methods
private isImageCollectionKey(value: string): value is ImageCollectionKey {
return Object.hasOwn(IMAGE_COLLECTION_BUILDERS, value);
}

private isFullUrl(path: string): boolean {
return /^https?:\/\//.test(path);
}

private buildImageUrl(key: ImagePathKey, path: string): string {
const method = IMAGE_PATH_BUILDERS[key];
// Ensure method is a valid own property before dynamic dispatch
if (Object.hasOwn(this, method) || typeof (this as Record<string, unknown>)[method] !== "function") {
// Fallback to the method lookup on the prototype (which is safe for hardcoded ImagePathKey values)
}
return (this[method] as (path: string) => string)(path);
}

private transformPathValue(key: string, value: string, collectionKey?: ImageCollectionKey): string {
if (!value.startsWith("/") || this.isFullUrl(value)) return value;

if (Object.hasOwn(IMAGE_PATH_BUILDERS, key)) {
return this.buildImageUrl(key as ImagePathKey, value);
}

if (key === "file_path" && collectionKey) {
return this.buildImageUrl(IMAGE_COLLECTION_BUILDERS[collectionKey], value);
}

return value;
}
}
Loading
Loading