tmdb-kit 0.1.0-beta.3 → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 isaced
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,17 +1,26 @@
1
1
  # tmdb-kit
2
2
 
3
- A tiny, typed, zero-runtime-dependency TMDB SDK for Node.js, Bun, Deno, and edge runtimes.
3
+ [![npm version](https://img.shields.io/npm/v/tmdb-kit)](https://www.npmjs.com/package/tmdb-kit)
4
+ [![CI](https://img.shields.io/github/actions/workflow/status/isaced/tmdb-kit/ci.yml?branch=main)](https://github.com/isaced/tmdb-kit/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/tmdb-kit)](https://github.com/isaced/tmdb-kit/blob/main/LICENSE)
6
+ [![node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/isaced/tmdb-kit/actions/workflows/ci.yml)
4
8
 
5
- This package intentionally does not generate code from OpenAPI. It ships a small hand-designed API surface for common TMDB workflows, plus a typed `request<T>()` escape hatch for endpoints that are not wrapped yet.
9
+ A minimal, fully typed TMDB SDK with zero runtime dependencies. Built for Node.js, Bun, Deno, and edge runtimes.
10
+
11
+ `tmdb-kit` covers the most common TMDB workflows — movies, TV shows, search, trending, people, genres, and more — with a small, hand-crafted API designed for day-to-day use.
6
12
 
7
13
  ## Features
8
14
 
9
- - Zero runtime dependencies.
10
- - Built with tsdown and published as ESM + CommonJS.
11
- - Uses only Web standard APIs: `fetch`, `Headers`, `URL`, and `AbortSignal`.
12
- - Works in Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and other fetch-compatible runtimes.
13
- - Typed resources for movies, TV, search, trending, people, genres, configuration, credits, videos, and images.
14
- - High-coverage unit tests with mocked fetch; no real TMDB requests in tests.
15
+ - **Zero dependencies** — nothing to audit, nothing to conflict.
16
+ - **Universal runtime** — uses only `fetch`, `Headers`, `URL`, and `AbortSignal`. Works on Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and any fetch-compatible environment.
17
+ - **Strict TypeScript** — full type safety with autocomplete for request options, response shapes, image sizes, and error handling.
18
+ - **ESM + CommonJS** — dual-format output via tsdown, ready for any module system.
19
+ - **Typed resources** — first-class methods for movies, TV, search, trending, people, genres, configuration, and images.
20
+ - **Injectable transport** — swap the HTTP layer for tests, caching, or instrumentation without mocking `fetch`.
21
+ - **Image helpers** — build CDN / proxy / signed URLs with per-request transform overrides.
22
+ - **Search type guards** — narrow `search.multi()` results to movies, TV, or people with runtime-checked helpers.
23
+ - **100% test coverage** — comprehensive unit tests with mocked fetch; no real TMDB requests.
15
24
 
16
25
  ## Install
17
26
 
@@ -55,13 +64,15 @@ const tmdb = createTMDB({ accessToken: Deno.env.get('TMDB_ACCESS_TOKEN')! })
55
64
 
56
65
  ## Authentication
57
66
 
58
- The recommended option is TMDB's API Read Access Token:
67
+ The SDK targets the **TMDB v3 API** (`api.themoviedb.org/3`). Two authentication methods are supported:
68
+
69
+ **API Read Access Token** (recommended) — works with both v3 and v4:
59
70
 
60
71
  ```ts
61
72
  createTMDB({ accessToken: 'tmdb-read-access-token' })
62
73
  ```
63
74
 
64
- The legacy v3 `api_key` query parameter is also supported:
75
+ **Legacy v3 API key** — sent as an `api_key` query parameter:
65
76
 
66
77
  ```ts
67
78
  createTMDB({ apiKey: 'tmdb-v3-api-key' })
@@ -69,6 +80,61 @@ createTMDB({ apiKey: 'tmdb-v3-api-key' })
69
80
 
70
81
  Exactly one of `accessToken` or `apiKey` is required.
71
82
 
83
+ ## Configuration
84
+
85
+ The factory accepts a few options beyond authentication:
86
+
87
+ ```ts
88
+ import { createTMDB } from 'tmdb-kit'
89
+
90
+ const tmdb = createTMDB({
91
+ accessToken: 'tmdb-read-access-token',
92
+
93
+ // Defaults to https://api.themoviedb.org/3. Useful for tests or proxies.
94
+ baseUrl: 'https://api.themoviedb.org/3',
95
+
96
+ // Inject a fetch implementation. The SDK reads `globalThis.fetch` lazily
97
+ // on every request, so test-time mocks (vi.spyOn, etc.) are picked up
98
+ // automatically after the client has been constructed.
99
+ fetch: customFetch,
100
+
101
+ // Defaults applied to every request that supports them.
102
+ defaultLanguage: 'en-US',
103
+ defaultRegion: 'US',
104
+
105
+ // Custom image host. Defaults to https://image.tmdb.org/t/p.
106
+ imageBaseUrl: 'https://image.tmdb.org/t/p',
107
+
108
+ // Image URL transform applied by the ImagesHelper. See "Image URLs".
109
+ images: {
110
+ transform: (url) => url.replace('image.tmdb.org/t/p', 'img-proxy.example/tmdb'),
111
+ },
112
+
113
+ // Extra headers merged into every request before authentication.
114
+ headers: { 'X-Client': 'my-app' },
115
+ })
116
+ ```
117
+
118
+ ### Injecting a custom transport
119
+
120
+ For tests, caching, or instrumentation, you can swap the entire transport
121
+ layer instead of mocking `fetch`:
122
+
123
+ ```ts
124
+ import { createTMDB, type TMDBTransport } from 'tmdb-kit'
125
+
126
+ const stub: TMDBTransport = {
127
+ defaults: { imageBaseUrl: 'https://image.tmdb.org/t/p' },
128
+ get: async (path, _options) => ({ results: [] }),
129
+ }
130
+
131
+ const tmdb = createTMDB({ accessToken: 'x', transport: stub })
132
+ ```
133
+
134
+ `transport.get()` is called for every resource and for the `request()`
135
+ escape hatch, so a single stub covers the whole surface area.
136
+ `TMDBTransport` is exported as part of the public API.
137
+
72
138
  ## Supported Resources
73
139
 
74
140
  ```ts
@@ -93,6 +159,7 @@ tmdb.tv.images(1399)
93
159
  tmdb.tv.recommendations(1399)
94
160
  tmdb.tv.similar(1399)
95
161
  tmdb.tv.videos(1399)
162
+ tmdb.tv.seasonDetails(1399, 1)
96
163
 
97
164
  tmdb.search.movies('Dune')
98
165
  tmdb.search.tv('Dark')
@@ -129,20 +196,157 @@ const response = await tmdb.request<{ results: unknown[] }>('/discover/movie', {
129
196
  })
130
197
  ```
131
198
 
199
+ ## Image URLs
200
+
201
+ `tmdb.images.url(path, size)` builds a fully-qualified image URL. The
202
+ helper returns `null` for nullish paths so UI code can branch naturally:
203
+
204
+ ```ts
205
+ const posterUrl = tmdb.images.url(movie.poster_path, 'w500')
206
+ ```
207
+
208
+ ### Routing images through a CDN or proxy
209
+
210
+ Pass a `transform` callback to rewrite the resolved URL. The most common
211
+ use case is pointing TMDB image requests at a self-hosted proxy:
212
+
213
+ ```ts
214
+ const tmdb = createTMDB({
215
+ accessToken: 'x',
216
+ images: {
217
+ transform: (url) => url.replace('https://image.tmdb.org/t/p', 'https://img-proxy.example/tmdb'),
218
+ },
219
+ })
220
+
221
+ tmdb.images.url('/abc.jpg', 'w500')
222
+ // → 'https://img-proxy.example/tmdb/w500/abc.jpg'
223
+ ```
224
+
225
+ You can also pass a one-off transform per call — useful for adding
226
+ signed query parameters for selected sizes:
227
+
228
+ ```ts
229
+ tmdb.images.url('/abc.jpg', 'original', {
230
+ transform: (url) => `${url}?signature=...`,
231
+ })
232
+ ```
233
+
234
+ The transform is never invoked for nullish paths, so `null` results stay
235
+ `null`.
236
+
237
+ `buildImageUrl(path, size, options)` is exported directly for callers
238
+ that want to build URLs without a `TMDBClient` instance.
239
+
240
+ ## Search
241
+
242
+ `search.multi()` returns a union of movie / TV / person results. Filter
243
+ out media types you do not want and narrow the result type with the
244
+ provided type guards:
245
+
246
+ ```ts
247
+ import {
248
+ createTMDB,
249
+ isMovieSearchResult,
250
+ isTVSearchResult,
251
+ isPersonSearchResult,
252
+ } from 'tmdb-kit'
253
+
254
+ const { results } = await tmdb.search.multi('Inception', {
255
+ exclude: ['person'],
256
+ })
257
+
258
+ const movies = results.filter(isMovieSearchResult) // MovieMultiResult[]
259
+ const tvShows = results.filter(isTVSearchResult) // TVMultiResult[]
260
+ const people = results.filter(isPersonSearchResult) // PersonMultiResult[]
261
+ ```
262
+
263
+ - `exclude` performs client-side filtering — the TMDB multi-search
264
+ endpoint has no server-side media_type filter. `total_results` reflects
265
+ the unfiltered payload; the filtered count is just `results.length`.
266
+ - Short-name aliases `isMovie` / `isTV` / `isPerson` are also exported
267
+ for callers that prefer them.
268
+ - The single-resource search methods (`search.movies`, `search.tv`,
269
+ `search.people`) already return the narrowed result type, so the
270
+ guards are only useful for `search.multi()` payloads.
271
+
272
+ ## Image Sizes
273
+
274
+ The `ImageSize` type is intentionally permissive — TMDB has added new
275
+ sizes in the past (e.g. `h632` for profile) without notice, and the SDK
276
+ keeps the `(string & {})` escape hatch so any future size keeps
277
+ working at runtime. If you need the canonical set of documented sizes,
278
+ the SDK ships them as readonly constants:
279
+
280
+ ```ts
281
+ import {
282
+ KNOWN_BACKDROP_SIZES,
283
+ KNOWN_LOGO_SIZES,
284
+ KNOWN_POSTER_SIZES,
285
+ KNOWN_PROFILE_SIZES,
286
+ KNOWN_STILL_SIZES,
287
+ KNOWN_IMAGE_SIZES,
288
+ isKnownBackdropSize,
289
+ isKnownImageSize,
290
+ // …plus one helper per type
291
+ } from 'tmdb-kit'
292
+
293
+ // Iterate to build a size picker
294
+ for (const size of KNOWN_POSTER_SIZES) {
295
+ console.log(size) // 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original'
296
+ }
297
+
298
+ // Validate user-supplied sizes at runtime
299
+ if (!isKnownPosterSize(userInput)) {
300
+ throw new Error(`Unknown TMDB poster size: ${userInput}`)
301
+ }
302
+ ```
303
+
304
+ `KnownBackdropSize` / `KnownPosterSize` / `KnownLogoSize` /
305
+ `KnownProfileSize` / `KnownStillSize` / `KnownImageSize` are the
306
+ matching TS literal-union types for callers that want strict typing
307
+ without giving up forward-compatibility.
308
+
132
309
  ## Errors
133
310
 
134
311
  ```ts
135
- import { TMDBResponseError } from 'tmdb-kit'
312
+ import {
313
+ TMDBRateLimitError,
314
+ TMDBRequestError,
315
+ TMDBResponseError,
316
+ } from 'tmdb-kit'
136
317
 
137
318
  try {
138
319
  await tmdb.movies.popular()
139
320
  } catch (error) {
140
- if (error instanceof TMDBResponseError) {
321
+ if (error instanceof TMDBRateLimitError) {
322
+ // 429 — TMDB asked the caller to back off. The SDK does NOT retry
323
+ // automatically; back off for `retryAfter` seconds (or use your own
324
+ // policy) and try again.
325
+ console.log('rate limited; retry in', error.retryAfter, 's')
326
+ } else if (error instanceof TMDBResponseError) {
141
327
  console.log(error.status, error.statusCode, error.statusMessage)
328
+ } else if (error instanceof TMDBRequestError) {
329
+ // Thrown when a request fails before receiving a response (e.g. network
330
+ // error, no fetch implementation found, or invalid client configuration).
331
+ console.log('request error:', error.message)
142
332
  }
143
333
  }
144
334
  ```
145
335
 
336
+ `TMDBResponseError` exposes convenience getters for the most common
337
+ status codes:
338
+
339
+ | Getter | Status |
340
+ | --- | --- |
341
+ | `isUnauthorized` | 401 |
342
+ | `isForbidden` | 403 |
343
+ | `isNotFound` | 404 |
344
+ | `isRateLimit` | 429 |
345
+ | `isServerError` | 5xx |
346
+
347
+ All errors are also instances of the base class `TMDBError`; use that
348
+ when you want a single catch-all.
349
+
146
350
  ## Development
147
351
 
148
352
  ```bash