tmdb-kit 1.1.0 → 1.2.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/README.md CHANGED
@@ -19,11 +19,13 @@ A minimal, fully typed SDK and read-only command-line interface (CLI) for The Mo
19
19
  - [Authentication](#authentication)
20
20
  - [Configuration](#configuration)
21
21
  - [Custom transport](#custom-transport)
22
+ - [Cancellation](#cancellation)
22
23
  - [Supported resources](#supported-resources)
23
24
  - [Image URLs](#image-urls)
24
25
  - [Search](#search)
25
26
  - [Image sizes](#image-sizes)
26
27
  - [Errors](#errors)
28
+ - [Optional helpers](#optional-helpers)
27
29
  - [CLI](#cli)
28
30
  - [Credentials and config](#credentials-and-config)
29
31
  - [Commands](#commands)
@@ -42,9 +44,10 @@ A minimal, fully typed SDK and read-only command-line interface (CLI) for The Mo
42
44
  - **ESM output**: modern module output with bundled type declarations.
43
45
  - **Typed resources**: first-class methods for movies, TV, search, trending, people, genres, configuration, and images.
44
46
  - **Injectable transport**: swap the HTTP layer for tests, caching, or instrumentation without mocking `fetch`.
47
+ - **Optional helpers**: async pagination, `Retry-After`-aware retries, `AbortSignal` timeouts, and an LRU memory cache — all opt-in and tree-shakeable.
45
48
  - **Image helpers**: build content delivery network (CDN), proxy, or signed URLs with per-request transform overrides.
46
49
  - **Search type guards**: narrow `search.multi()` results to movies, TV, or people with runtime-checked helpers.
47
- - **Tested**: unit and resource tests mock `fetch`; an optional live integration suite validates real API behavior.
50
+ - **Tested**: unit and resource tests mock `fetch`; an optional live integration suite validates real API behavior, including the helpers.
48
51
 
49
52
  ## Install
50
53
 
@@ -100,24 +103,24 @@ A legacy v3 API key is sent as an `api_key` query parameter:
100
103
  createTMDB({ apiKey: 'your_api_key_here' })
101
104
  ```
102
105
 
103
- Pass exactly one of `accessToken` or `apiKey`. The client throws when both or neither are set.
106
+ Pass exactly one of `accessToken` or `apiKey`. The client throws when both or neither are set. When you supply a `transport`, credentials are optional because the transport owns authentication.
104
107
 
105
108
  ### Configuration
106
109
 
107
110
  `createTMDB` accepts these options:
108
111
 
109
- | Option | Default | Description |
110
- | ------------------ | ------------------------------------ | ----------------------------------------------------------------- |
111
- | `accessToken` | required unless `apiKey` is set | API Read Access Token, sent as a Bearer header. |
112
- | `apiKey` | required unless `accessToken` is set | Legacy v3 API key, sent as `api_key`. |
113
- | `baseUrl` | `https://api.themoviedb.org/3` | API base URL. Useful for proxies and tests. |
114
- | `fetch` | `globalThis.fetch` at request time | Custom fetch implementation. |
115
- | `defaultLanguage` | TMDB default | Language for endpoints that accept `language`. |
116
- | `defaultRegion` | TMDB default | Region for movie lists and search endpoints that accept `region`. |
117
- | `imageBaseUrl` | `https://image.tmdb.org/t/p` | Image host used by `tmdb.images`. |
118
- | `images.transform` | none | Default URL transform for `tmdb.images.url()`. |
119
- | `headers` | none | Extra headers merged into every request before authentication. |
120
- | `transport` | built-in HTTP client | Custom transport, see [Custom transport](#custom-transport). |
112
+ | Option | Default | Description |
113
+ | ------------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------ |
114
+ | `accessToken` | required unless `apiKey` or `transport` is set | API Read Access Token, sent as a Bearer header. |
115
+ | `apiKey` | required unless `accessToken` or `transport` is set | Legacy v3 API key, sent as `api_key`. |
116
+ | `baseUrl` | `https://api.themoviedb.org/3` | API base URL. Useful for proxies and tests. |
117
+ | `fetch` | `globalThis.fetch` at request time | Custom fetch implementation. |
118
+ | `defaultLanguage` | TMDB default | Language for endpoints that accept `language`. |
119
+ | `defaultRegion` | TMDB default | Region for movie lists and search endpoints that accept `region`. |
120
+ | `imageBaseUrl` | `https://image.tmdb.org/t/p` | Image host used by `tmdb.images`. |
121
+ | `images.transform` | none | Default URL transform for `tmdb.images.url()`. |
122
+ | `headers` | none | Extra headers merged into every request before authentication. |
123
+ | `transport` | built-in HTTP client | Custom transport; credentials are not required. See [Custom transport](#custom-transport). |
121
124
 
122
125
  A typical client:
123
126
 
@@ -147,23 +150,37 @@ const stub: TMDBTransport = {
147
150
  get: async (path, _options) => ({ results: [] }),
148
151
  }
149
152
 
150
- const tmdb = createTMDB({ accessToken: 'your_access_token_here', transport: stub })
153
+ const tmdb = createTMDB({ transport: stub })
151
154
  ```
152
155
 
153
- `transport.get()` runs for every resource and for `request()`, so a single stub covers the whole surface. `TMDBTransport` is part of the public API.
156
+ `transport.get()` runs for every resource and for `request()`, so a single stub covers the whole surface. `TMDBTransport` is part of the public API. The built-in `TMDBHttpClient` is exported too, so you can wrap it instead of reimplementing authentication.
157
+
158
+ ### Cancellation
159
+
160
+ Every resource method accepts an `AbortSignal` through its options. Pass the same signal to cancel one or more requests:
161
+
162
+ ```ts
163
+ const controller = new AbortController()
164
+ const promise = tmdb.movies.popular({ signal: controller.signal })
165
+
166
+ controller.abort()
167
+ await promise // rejects with an AbortError
168
+ ```
169
+
170
+ `client.request()` accepts the same `signal`. The SDK does not impose a timeout by default; use the optional `withTimeout()` helper (see [Optional helpers](#optional-helpers)) or your runtime's `AbortSignal.timeout()` when you need one.
154
171
 
155
172
  ### Supported resources
156
173
 
157
- | Resource | Methods |
158
- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
- | `tmdb.movies` | `popular()`, `topRated()`, `nowPlaying()`, `upcoming()`, `details()`, `credits()`, `images()`, `recommendations()`, `similar()`, `videos()` |
160
- | `tmdb.tv` | `popular()`, `topRated()`, `airingToday()`, `onTheAir()`, `details()`, `credits()`, `images()`, `recommendations()`, `similar()`, `videos()`, `seasonDetails()` |
161
- | `tmdb.search` | `movies()`, `tv()`, `people()`, `multi()` |
162
- | `tmdb.trending` | `all()`, `movies()`, `tv()`, `people()` |
163
- | `tmdb.people` | `popular()`, `details()`, `combinedCredits()` |
164
- | `tmdb.genres` | `movies()`, `tv()` |
165
- | `tmdb.configuration` | `details()`, `countries()`, `jobs()`, `languages()`, `primaryTranslations()`, `timezones()` |
166
- | `tmdb.images` | `url()` |
174
+ | Resource | Methods |
175
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
176
+ | `tmdb.movies` | `popular()`, `topRated()`, `nowPlaying()`, `upcoming()`, `details()`, `credits()`, `images()`, `recommendations()`, `similar()`, `videos()` |
177
+ | `tmdb.tv` | `popular()`, `topRated()`, `airingToday()`, `onTheAir()`, `details()`, `credits()`, `images()`, `recommendations()`, `similar()`, `videos()`, `seasonDetails()`, `seasonCredits()`, `seasonAggregateCredits()`, `seasonImages()`, `seasonVideos()`, `episodeDetails()`, `aggregateCredits()`, `episodeGroups()`, `episodeGroup()` |
178
+ | `tmdb.search` | `movies()`, `tv()`, `people()`, `multi()` |
179
+ | `tmdb.trending` | `all()`, `movies()`, `tv()`, `people()` |
180
+ | `tmdb.people` | `popular()`, `details()`, `combinedCredits()` |
181
+ | `tmdb.genres` | `movies()`, `tv()` |
182
+ | `tmdb.configuration` | `details()`, `countries()`, `jobs()`, `languages()`, `primaryTranslations()`, `timezones()` |
183
+ | `tmdb.images` | `url()` |
167
184
 
168
185
  For an endpoint that is not wrapped yet, use the `request()` escape hatch:
169
186
 
@@ -249,19 +266,23 @@ The SDK also exports `KNOWN_BACKDROP_SIZES`, `KNOWN_LOGO_SIZES`, `KNOWN_PROFILE_
249
266
  ### Errors
250
267
 
251
268
  ```ts
252
- import { TMDBRateLimitError, TMDBRequestError, TMDBResponseError } from 'tmdb-kit'
269
+ import { TMDBRateLimitError, TMDBRequestError, TMDBResponseError, TMDBTimeoutError } from 'tmdb-kit'
253
270
 
254
271
  try {
255
272
  await tmdb.movies.popular()
256
273
  } catch (error) {
257
274
  if (error instanceof TMDBRateLimitError) {
258
- // 429: TMDB asked the caller to back off. The SDK does not retry.
275
+ // 429: TMDB asked the caller to back off. The SDK does not retry by
276
+ // default; use retry() or withRetry() if you want automatic backoff.
259
277
  console.log('rate limited; retry in', error.retryAfter, 's')
260
278
  } else if (error instanceof TMDBResponseError) {
261
279
  console.log(error.status, error.statusCode, error.statusMessage)
262
280
  } else if (error instanceof TMDBRequestError) {
263
281
  // Network failure, missing fetch, or invalid client configuration.
264
282
  console.log('request error:', error.message)
283
+ } else if (error instanceof TMDBTimeoutError) {
284
+ // Thrown by withTimeout(), not by the HTTP layer.
285
+ console.log('timed out after', error.timeoutMs, 'ms')
265
286
  }
266
287
  }
267
288
  ```
@@ -276,11 +297,71 @@ try {
276
297
  | `isRateLimit` | 429 |
277
298
  | `isServerError` | 5xx |
278
299
 
279
- All errors extend `TMDBError`, which you can use as a single catch-all.
300
+ All errors extend `TMDBError`, which you can use as a single catch-all. `TMDBTimeoutError` is thrown by `withTimeout()` and exposes `timeoutMs`; the HTTP layer never throws it directly.
301
+
302
+ ### Optional helpers
303
+
304
+ These helpers are opt-in and never change the default client behavior. They are exported from the package root and tree-shake out of ESM bundles when unused.
305
+
306
+ **Pagination**
307
+
308
+ ```ts
309
+ import { paginate, paginateItems } from 'tmdb-kit'
310
+
311
+ for await (const page of paginate((page) => tmdb.movies.popular({ page, region: 'CN' }))) {
312
+ console.log(page.page, page.results.length)
313
+ }
314
+
315
+ // Or iterate items directly.
316
+ for await (const movie of paginateItems((page) => tmdb.movies.popular({ page }))) {
317
+ console.log(movie.title)
318
+ }
319
+ ```
320
+
321
+ `startPage` defaults to 1. `paginate` stops at `total_pages`, an empty page, `endPage` (default 500), or an aborted signal. `endPage` is the last page number to request, so it yields nothing when it is lower than `startPage`.
322
+
323
+ **Retry**
324
+
325
+ ```ts
326
+ import { createTMDB, retry, TMDBHttpClient, withRetry } from 'tmdb-kit'
327
+
328
+ // Wrap one call.
329
+ const movie = await retry(() => tmdb.movies.details(550), { retries: 3 })
330
+
331
+ // Or wrap the transport once. The transport owns authentication, so the
332
+ // factory does not need credentials.
333
+ const http = new TMDBHttpClient({ accessToken: 'your_access_token_here' })
334
+ const retrying = createTMDB({ transport: withRetry(http, { retries: 3 }) })
335
+ ```
336
+
337
+ Retries 429 and any response that carries `Retry-After` (including 503), retries 408, 5xx and network failures with exponential backoff, and leaves other errors untouched. Defaults are 2 retries, a 250 ms base delay, factor 2, full jitter, and a 60 s cap on `Retry-After`; use `retryOn` to replace the predicate and `onRetry` to observe delays. A wrapper-level `signal` and a per-request `signal` are combined, so aborting either cancels the call. The SDK itself still never retries by default.
338
+
339
+ **Timeout**
340
+
341
+ ```ts
342
+ import { withTimeout } from 'tmdb-kit'
343
+
344
+ const movie = await withTimeout(5_000, (signal) => tmdb.movies.details(550, { signal }))
345
+ ```
346
+
347
+ Timeouts reject with `TMDBTimeoutError`. The task must forward the signal to the SDK. Pass a parent `signal` in the options to cancel it externally; the parent's reason is preserved.
348
+
349
+ **Memory cache**
350
+
351
+ ```ts
352
+ import { createTMDB, TMDBHttpClient, withMemoryCache } from 'tmdb-kit'
353
+
354
+ const http = new TMDBHttpClient({ accessToken: 'your_access_token_here' })
355
+ const cached = createTMDB({
356
+ transport: withMemoryCache(http, { ttlMs: 60_000, maxEntries: 200 }),
357
+ })
358
+ ```
359
+
360
+ Defaults are a 5-minute TTL and 100 entries. The cache key includes the path, query, and headers, only successful responses are stored, and `clear()` invalidates every entry. Use one cache instance per client or credential: authentication is not part of the key.
280
361
 
281
362
  ## CLI
282
363
 
283
- The `tmdb` command is a read-only client for the same TMDB API. It reads credentials from the environment or a config file, prints human-readable summaries by default, and accepts `--json` for raw TMDB payloads.
364
+ The `tmdb` command is a read-only client for the same TMDB API. It reads credentials from the environment or a config file, prints human-readable summaries by default, and accepts `--format json` (or its `--json` alias) for raw TMDB payloads and `--format ndjson` for line-oriented output.
284
365
 
285
366
  The CLI runs on Node.js 18+ and Bun.
286
367
 
@@ -332,69 +413,144 @@ Use `--lang` to override the config language for a single command. `auth logout`
332
413
 
333
414
  ### Commands
334
415
 
335
- Every API command accepts `--lang` and `--json`. Run `tmdb <command> --help` for the full option list.
336
-
337
- | Command | Description | Command options |
338
- | --------------------------------- | --------------------------------------------------------------------------------- | ---------------------------- |
339
- | `tmdb search <query>` | Search all media, movies, TV, or people. | `--type`, `--year`, `--page` |
340
- | `tmdb movie list [category]` | List movies. Default `popular`; also `top-rated`, `now-playing`, `upcoming`. | `--page`, `--region` |
341
- | `tmdb movie get <id>` | Movie details. | `--append` |
342
- | `tmdb movie credits <id>` | Movie cast and crew. | `--limit` |
343
- | `tmdb movie images <id>` | Movie image URLs. | `--image-language` |
344
- | `tmdb movie videos <id>` | Movie videos and trailers. | none |
345
- | `tmdb movie similar <id>` | Similar movies. | `--page`, `--region` |
346
- | `tmdb movie recommendations <id>` | Recommended movies. | `--page`, `--region` |
347
- | `tmdb tv list [category]` | List TV shows. Default `popular`; also `top-rated`, `airing-today`, `on-the-air`. | `--page` |
348
- | `tmdb tv get <id>` | TV show details. | `--append` |
349
- | `tmdb tv credits <id>` | TV cast and crew. | `--limit` |
350
- | `tmdb tv images <id>` | TV image URLs. | `--image-language` |
351
- | `tmdb tv videos <id>` | TV videos and trailers. | none |
352
- | `tmdb tv similar <id>` | Similar TV shows. | `--page` |
353
- | `tmdb tv recommendations <id>` | Recommended TV shows. | `--page` |
354
- | `tmdb tv season <id> <season>` | TV season and episodes. Use `0` for specials. | none |
355
- | `tmdb person get <id>` | Person details. | none |
356
- | `tmdb person credits <id>` | Combined cast and crew credits. | `--limit` |
357
- | `tmdb trending [type]` | Trending media. Default `all`; also `movie`, `tv`, `person`. | `--window`, `--page` |
358
- | `tmdb genres [type]` | Genres. Default `movie`; also `tv`. | none |
416
+ Every API command accepts `--lang`, `--format`, `--fields`, and `--compact`. Commands that return a paginated list also accept `--all` and `--max-items` with `--format ndjson`. Run `tmdb <command> --help` for the full option list.
417
+
418
+ | Command | Description | Command options |
419
+ | ------------------------------------------------ | --------------------------------------------------------------------------------- | ---------------------------- |
420
+ | `tmdb search <query>` | Search all media, movies, TV, or people. | `--type`, `--year`, `--page` |
421
+ | `tmdb movie list [category]` | List movies. Default `popular`; also `top-rated`, `now-playing`, `upcoming`. | `--page`, `--region` |
422
+ | `tmdb movie get <id>` | Movie details. | `--append` |
423
+ | `tmdb movie credits <id>` | Movie cast and crew. | `--limit` |
424
+ | `tmdb movie images <id>` | Movie image URLs. | `--image-language` |
425
+ | `tmdb movie videos <id>` | Movie videos and trailers. | none |
426
+ | `tmdb movie similar <id>` | Similar movies. | `--page`, `--region` |
427
+ | `tmdb movie recommendations <id>` | Recommended movies. | `--page`, `--region` |
428
+ | `tmdb tv list [category]` | List TV shows. Default `popular`; also `top-rated`, `airing-today`, `on-the-air`. | `--page` |
429
+ | `tmdb tv get <id>` | TV show details. | `--append` |
430
+ | `tmdb tv credits <id>` | TV cast and crew. | `--limit` |
431
+ | `tmdb tv images <id>` | TV image URLs. | `--image-language` |
432
+ | `tmdb tv videos <id>` | TV videos and trailers. | none |
433
+ | `tmdb tv similar <id>` | Similar TV shows. | `--page` |
434
+ | `tmdb tv recommendations <id>` | Recommended TV shows. | `--page` |
435
+ | `tmdb tv season <id> <season>` | TV season and episodes. Use `0` for specials. | none |
436
+ | `tmdb tv episode <id> <season> <episode>` | TV episode details. Use `0` for specials. | `--append` |
437
+ | `tmdb tv aggregate-credits <id>` | Aggregate cast and crew credits for a show. | `--limit` |
438
+ | `tmdb tv season-credits <id> <season>` | Season cast and crew credits. | `--limit` |
439
+ | `tmdb tv season-aggregate-credits <id> <season>` | Aggregate season cast and crew credits. | `--limit` |
440
+ | `tmdb tv season-images <id> <season>` | Season image URLs. | `--image-language` |
441
+ | `tmdb tv season-videos <id> <season>` | Season videos and trailers. | none |
442
+ | `tmdb tv episode-groups <id>` | Alternate episode groups for a show. | none |
443
+ | `tmdb tv episode-group <group-id>` | Episodes in an alternate group. | none |
444
+ | `tmdb person get <id>` | Person details. | none |
445
+ | `tmdb person credits <id>` | Combined cast and crew credits. | `--limit` |
446
+ | `tmdb trending [type]` | Trending media. Default `all`; also `movie`, `tv`, `person`. | `--window`, `--page` |
447
+ | `tmdb genres [type]` | Genres. Default `movie`; also `tv`. | none |
359
448
 
360
449
  Credential and config commands:
361
450
 
362
- | Command | Description | Command options |
363
- | ------------------------------- | ------------------------------------------------------ | -------------------------- |
364
- | `tmdb auth login` | Validate and save credentials. | `--token-stdin`, `--force` |
365
- | `tmdb auth status` | Show credential source, masked token, and config path. | `--json` |
366
- | `tmdb auth logout` | Remove saved credentials. | none |
367
- | `tmdb config show` | Show config values with secrets masked. | `--json` |
368
- | `tmdb config path` | Print the config file path. | none |
369
- | `tmdb config test` | Verify credentials with an authenticated request. | `--json` |
370
- | `tmdb config set <key> [value]` | Set `token`, `api-key`, `language`, or `region`. | none |
371
- | `tmdb config unset <key>` | Remove a config value. | none |
451
+ | Command | Description | Command options |
452
+ | ------------------------------- | ------------------------------------------------------ | ----------------------------------- |
453
+ | `tmdb auth login` | Validate and save credentials. | `--token-stdin`, `--force` |
454
+ | `tmdb auth status` | Show credential source, masked token, and config path. | `--format`, `--fields`, `--compact` |
455
+ | `tmdb auth logout` | Remove saved credentials. | none |
456
+ | `tmdb config show` | Show config values with secrets masked. | `--format`, `--fields`, `--compact` |
457
+ | `tmdb config path` | Print the config file path. | none |
458
+ | `tmdb config test` | Verify credentials with an authenticated request. | `--format`, `--fields`, `--compact` |
459
+ | `tmdb config set <key> [value]` | Set `token`, `api-key`, `language`, or `region`. | none |
460
+ | `tmdb config unset <key>` | Remove a config value. | none |
372
461
 
373
462
  ### Common options
374
463
 
375
- | Option | Description |
376
- | ------------------------- | ---------------------------------------------------------------------------------- |
377
- | `--json` | Print the raw TMDB JSON response instead of the formatted summary. |
378
- | `--lang <code>` | Language for this command. Defaults to config `language`, then `en-US`. |
379
- | `--page <number>` | Page number. Defaults to `1`. |
380
- | `--region <code>` | Region for movie lists, similar, and recommendations. Defaults to config `region`. |
381
- | `--limit <number>` | Maximum rows for credits. Defaults to `10`. |
382
- | `--append <list>` | Comma-separated `append_to_response` values, such as `credits,videos`. |
383
- | `--image-language <list>` | Comma-separated image language codes. |
384
- | `--year <year>` | Movie release year or TV first-air year. Only with `--type movie` or `--type tv`. |
385
- | `--type <value>` | Media selector for `search`: `all`, `movie`, `tv`, or `person`. |
386
- | `--window <day\|week>` | Trending window. Defaults to `week`. |
464
+ | Option | Description |
465
+ | ------------------------------- | ----------------------------------------------------------------------------------- |
466
+ | `--format <text\|json\|ndjson>` | Output format. `json` prints the raw response, `ndjson` prints one record per line. |
467
+ | `--fields <list>` | Comma-separated top-level fields to keep. Requires `--format json` or `ndjson`. |
468
+ | `--json` | Alias for `--format json`. |
469
+ | `--compact` | Print JSON on a single line instead of pretty-printing it. |
470
+ | `--all` | Follow pagination to the last page. List commands only; requires `ndjson`. |
471
+ | `--max-items <number>` | Stop after this many items. List commands only; requires `ndjson`. |
472
+ | `--lang <code>` | Language for this command. Defaults to config `language`, then `en-US`. |
473
+ | `--page <number>` | Page number. Defaults to `1`. |
474
+ | `--region <code>` | Region for movie lists, similar, and recommendations. Defaults to config `region`. |
475
+ | `--limit <number>` | Maximum rows for credits. Defaults to `10`. |
476
+ | `--append <list>` | Comma-separated `append_to_response` values, such as `credits,videos`. |
477
+ | `--image-language <list>` | Comma-separated image language codes. |
478
+ | `--year <year>` | Movie release year or TV first-air year. Only with `--type movie` or `--type tv`. |
479
+ | `--type <value>` | Media selector for `search`: `all`, `movie`, `tv`, or `person`. |
480
+ | `--window <day\|week>` | Trending window. Defaults to `week`. |
387
481
 
388
482
  `--version` prints the CLI version.
389
483
 
390
484
  ### Output and exit codes
391
485
 
392
- The default output is a compact summary for reading in a terminal. Use `--json` for scripts:
486
+ The default output is a compact summary for reading in a terminal. Use `--format json` for scripts; `--json` is a shorter alias:
393
487
 
394
488
  ```bash
395
- tmdb movie get 550 --json | jq '.title, .release_date'
489
+ tmdb movie get 550 --format json | jq '.title, .release_date'
490
+ tmdb movie get 550 --json | jq '.title'
491
+ tmdb movie get 550 --format json --compact # single line, for line-oriented tools
396
492
  ```
397
493
 
494
+ `--compact` only affects `json` output and is ignored for `text`. Combining `--json` with an explicit `--format text` or `--format ndjson` is a usage error.
495
+
496
+ #### NDJSON
497
+
498
+ `--format ndjson` (alias `jsonl`) prints one JSON object per line. Responses with a `results` array stream as three record kinds:
499
+
500
+ <!-- prettier-ignore -->
501
+ ```json
502
+ {"type":"meta","page":1,"total_pages":5,"total_results":100}
503
+ {"type":"item","index":0,"value":{"id":550,"title":"Fight Club"}}
504
+ {"type":"summary","count":20,"pages":1}
505
+ ```
506
+
507
+ `meta` is the response envelope without `results`, `index` is zero-based, and `summary` closes the stream with the item count and pages read. Every other response emits a single record:
508
+
509
+ <!-- prettier-ignore -->
510
+ ```json
511
+ {"type":"result","value":{"id":550,"title":"Fight Club"}}
512
+ ```
513
+
514
+ Because the stream is line-oriented, a missing `summary` line means the command failed; the reason is still on stderr as a single `{"type":"error",...}` record.
515
+
516
+ #### Pagination
517
+
518
+ List commands (`search`, `trending`, `movie list/similar/recommendations`, and `tv list/similar/recommendations`) accept `--all` to follow pagination from `--page` to the last page, and `--max-items` to stop early:
519
+
520
+ ```bash
521
+ tmdb trending movie --format ndjson --all --max-items 100
522
+ ```
523
+
524
+ `--max-items` caps the number of `item` records with or without `--all`. Both flags require `--format ndjson`, and `--all` may issue many requests: TMDB caps lists at 500 pages and the command stops at that limit.
525
+
526
+ #### Selecting fields
527
+
528
+ `--fields` keeps only the listed top-level fields. For detail commands it projects the response; for list commands it projects each entry in `results` while keeping the envelope:
529
+
530
+ ```bash
531
+ tmdb movie get 550 --format json --fields id,title,release_date
532
+ tmdb movie list --format ndjson --fields id,title | jq -c 'select(.type == "item")'
533
+ ```
534
+
535
+ Fields that a record does not have are omitted. If none of the requested fields exist, the command exits `2` and lists the available names. Nested paths are not supported, and `--fields` requires `--format json` or `--format ndjson`.
536
+
537
+ When JSON output is active, errors go to stderr as one JSON object and stdout stays empty, so a pipeline never receives a partial payload:
538
+
539
+ <!-- prettier-ignore -->
540
+ ```json
541
+ {"type":"error","kind":"not_found","message":"TMDB request failed (404): ...","exitCode":4,"status":404}
542
+ ```
543
+
544
+ | Field | Meaning |
545
+ | ------------ | ---------------------------------------------------------------------------- |
546
+ | `type` | Always `error`. |
547
+ | `kind` | `usage`, `auth`, `not_found`, `rate_limit`, `network`, `api`, or `internal`. |
548
+ | `message` | Human-readable description. |
549
+ | `exitCode` | Process exit code, matching the table below. |
550
+ | `status` | HTTP status, when the failure came from a TMDB response. |
551
+ | `requestId` | TMDB `x-request-id`, when the response included one. |
552
+ | `retryAfter` | Seconds to wait before retrying, for rate limit errors. |
553
+
398
554
  Exit codes:
399
555
 
400
556
  | Code | Meaning |
@@ -421,6 +577,12 @@ tmdb trending movie --window day --json | jq '.results[].title'
421
577
  # Season 1 episodes for a TV show
422
578
  tmdb tv season 1399 1 --json | jq '.episodes[].name'
423
579
 
580
+ # Episode details (season 0 = specials)
581
+ tmdb tv episode 1399 1 1
582
+
583
+ # Alternate episode groups
584
+ tmdb tv episode-groups 1399
585
+
424
586
  # Save defaults for later commands
425
587
  tmdb config set language zh-CN
426
588
  tmdb config set region CN
@@ -438,12 +600,14 @@ bun run test:coverage
438
600
  bun run build
439
601
  ```
440
602
 
441
- Run the integration suite with a real token:
603
+ Run the integration suite with a real token. It exercises every SDK resource, the optional helpers, and the CLI against the live API:
442
604
 
443
605
  ```bash
444
606
  TMDB_ACCESS_TOKEN=your_access_token_here bun run test:integration
445
607
  ```
446
608
 
609
+ Set `TMDB_API_ENDPOINT` to run the SDK suites against a compatible mirror. The `Integration` GitHub workflow runs the same suite nightly, on manual dispatch, and on pull requests that touch the SDK, CLI, or integration tests. Add a `TMDB_ACCESS_TOKEN` repository secret to enable it, and an optional `TMDB_API_KEY` secret to also cover legacy v3 key auth; fork and Dependabot pull requests skip the live job because repository secrets are not available to them.
610
+
447
611
  Build and run the CLI from source:
448
612
 
449
613
  ```bash