@zivue/zuuid 0.1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/client.d.ts +29 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +39 -0
  7. package/dist/entity.d.ts +106 -0
  8. package/dist/entity.d.ts.map +1 -0
  9. package/dist/entity.js +101 -0
  10. package/dist/hash.d.ts +3 -0
  11. package/dist/hash.d.ts.map +1 -0
  12. package/dist/hash.js +9 -0
  13. package/dist/identity.d.ts +14 -0
  14. package/dist/identity.d.ts.map +1 -0
  15. package/dist/identity.js +39 -0
  16. package/dist/index.d.ts +9 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +8 -0
  19. package/dist/providers/tmdb/client.d.ts +30 -0
  20. package/dist/providers/tmdb/client.d.ts.map +1 -0
  21. package/dist/providers/tmdb/client.js +92 -0
  22. package/dist/providers/tmdb/constants.d.ts +5 -0
  23. package/dist/providers/tmdb/constants.d.ts.map +1 -0
  24. package/dist/providers/tmdb/constants.js +4 -0
  25. package/dist/providers/tmdb/index.d.ts +7 -0
  26. package/dist/providers/tmdb/index.d.ts.map +1 -0
  27. package/dist/providers/tmdb/index.js +6 -0
  28. package/dist/providers/tmdb/movie.d.ts +164 -0
  29. package/dist/providers/tmdb/movie.d.ts.map +1 -0
  30. package/dist/providers/tmdb/movie.js +537 -0
  31. package/dist/providers/tmdb/person.d.ts +84 -0
  32. package/dist/providers/tmdb/person.d.ts.map +1 -0
  33. package/dist/providers/tmdb/person.js +345 -0
  34. package/dist/providers/tmdb/tv.d.ts +181 -0
  35. package/dist/providers/tmdb/tv.d.ts.map +1 -0
  36. package/dist/providers/tmdb/tv.js +522 -0
  37. package/dist/providers/tmdb/types.d.ts +36 -0
  38. package/dist/providers/tmdb/types.d.ts.map +1 -0
  39. package/dist/providers/tmdb/types.js +1 -0
  40. package/dist/source.d.ts +35 -0
  41. package/dist/source.d.ts.map +1 -0
  42. package/dist/source.js +53 -0
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +1 -0
  46. package/dist/uuid.d.ts +3 -0
  47. package/dist/uuid.d.ts.map +1 -0
  48. package/dist/uuid.js +32 -0
  49. package/package.json +72 -0
@@ -0,0 +1,522 @@
1
+ import { createZuuidData } from "../../entity.js";
2
+ import { providerZuuid } from "../../identity.js";
3
+ import { attachSourceMetadata, createSourceRecord } from "../../source.js";
4
+ import { TMDB_BACKDROP_BASE_URL, TMDB_POSTER_BASE_URL, TMDB_PROVIDER } from "./constants.js";
5
+ export const TMDB_TV_CATEGORY = "tv";
6
+ export const ZUUID_TV_CATEGORY = "tvshow";
7
+ export async function fetchTmdbTvSourceRecord(provider, input) {
8
+ const id = String(input.id).trim();
9
+ if (!id) {
10
+ throw new Error("TMDB tv id must not be empty");
11
+ }
12
+ if (!/^\d+$/.test(id)) {
13
+ throw new Error(`TMDB tv id must be numeric: ${id}`);
14
+ }
15
+ const payload = await provider.getJson(`/tv/${id}`, {
16
+ language: provider.language,
17
+ append_to_response: "aggregate_credits,external_ids,images,keywords,recommendations,similar,translations,content_ratings"
18
+ });
19
+ if (!payload) {
20
+ return undefined;
21
+ }
22
+ return createSourceRecord({
23
+ source: { provider: TMDB_PROVIDER, category: TMDB_TV_CATEGORY, externalId: id },
24
+ payload: payload
25
+ });
26
+ }
27
+ export async function searchTmdbTvSourceRecords(provider, input) {
28
+ const payload = await fetchTmdbTvSearchResults(provider, input);
29
+ return {
30
+ results: await sourceRecordsFromSearchResults(TMDB_TV_CATEGORY, payload.results),
31
+ pagination: paginationFromTmdbSearchResponse(payload)
32
+ };
33
+ }
34
+ export async function searchTmdbTv(provider, input, options = {}) {
35
+ const payload = await fetchTmdbTvSearchResults(provider, input);
36
+ const searchResults = [];
37
+ for (const result of payload.results ?? []) {
38
+ if (!result.id) {
39
+ continue;
40
+ }
41
+ const externalId = String(result.id);
42
+ const title = stringField(result.name) ?? stringField(result.original_name);
43
+ if (!title) {
44
+ continue;
45
+ }
46
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category: TMDB_TV_CATEGORY, externalId });
47
+ searchResults.push({
48
+ id: zuuid,
49
+ zuuid,
50
+ category: ZUUID_TV_CATEGORY,
51
+ title,
52
+ date: stringField(result.first_air_date) ?? null,
53
+ cover: mediaUrl(result.poster_path ?? undefined, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL) ?? null,
54
+ rating: typeof result.vote_average === "number" ? result.vote_average : null,
55
+ weight: typeof result.popularity === "number" ? result.popularity : null,
56
+ relationType: null,
57
+ attribute: null,
58
+ order: null,
59
+ source: { source: TMDB_PROVIDER, category: TMDB_TV_CATEGORY, value: externalId }
60
+ });
61
+ }
62
+ return {
63
+ results: searchResults,
64
+ pagination: paginationFromTmdbSearchResponse(payload)
65
+ };
66
+ }
67
+ async function fetchTmdbTvSearchResults(provider, input) {
68
+ const query = searchQuery(input.query, "TMDB tv search query");
69
+ const payload = await provider.getJson("/search/tv", {
70
+ ...searchParams(provider, input),
71
+ query,
72
+ ...(input.firstAirDateYear !== undefined ? { first_air_date_year: String(input.firstAirDateYear) } : {})
73
+ });
74
+ return payload ?? {};
75
+ }
76
+ export async function transformTmdbTv(source, options = {}) {
77
+ if (source.source.provider !== TMDB_PROVIDER || source.source.category !== TMDB_TV_CATEGORY) {
78
+ throw new Error(`unsupported TMDB source: ${source.source.provider}:${source.source.category}`);
79
+ }
80
+ const payload = source.payload;
81
+ const tmdbId = tmdbTvId(source, payload);
82
+ const title = stringField(payload.name) ?? stringField(payload.original_name);
83
+ if (!title) {
84
+ throw new Error("missing required TMDB tv field: name");
85
+ }
86
+ const zuuid = await providerZuuid({
87
+ provider: TMDB_PROVIDER,
88
+ category: TMDB_TV_CATEGORY,
89
+ externalId: tmdbId
90
+ });
91
+ const data = createZuuidData({ zuuid, category: ZUUID_TV_CATEGORY, primaryTitle: title });
92
+ const firstAirDate = stringField(payload.first_air_date);
93
+ if (firstAirDate) {
94
+ validateDate(firstAirDate, "first_air_date");
95
+ data.primaryDate = firstAirDate;
96
+ }
97
+ if (typeof payload.vote_average === "number") {
98
+ data.rating = payload.vote_average;
99
+ }
100
+ const overview = stringField(payload.overview);
101
+ if (overview) {
102
+ data.descriptions.push({ language: stringField(payload.original_language), value: overview, source: TMDB_PROVIDER });
103
+ }
104
+ const originalName = stringField(payload.original_name);
105
+ if (originalName && originalName !== title) {
106
+ data.aliases.push({
107
+ value: originalName,
108
+ language: stringField(payload.original_language),
109
+ aliasType: "original_name",
110
+ isPrimary: false,
111
+ source: TMDB_PROVIDER
112
+ });
113
+ }
114
+ addTranslations(data, payload, title);
115
+ addExternalIds(data, payload);
116
+ addImage(data, "poster", payload.poster_path, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL, true);
117
+ addImage(data, "backdrop", payload.backdrop_path, options.backdropBaseUrl ?? TMDB_BACKDROP_BASE_URL, false);
118
+ addImages(data, payload, options);
119
+ data.cover = data.media.find((media) => media.mediaCategory === "poster")?.url;
120
+ for (const genre of payload.genres ?? []) {
121
+ addTag(data, genre.name);
122
+ }
123
+ for (const keyword of payload.keywords?.results ?? []) {
124
+ addTag(data, keyword.name);
125
+ }
126
+ await addCreatedByRelations(data, payload);
127
+ await addAggregateCreditRelations(data, payload);
128
+ await addNamedRelations(data, payload.networks, "network", "network");
129
+ await addNamedRelations(data, payload.production_companies, "production_company", "company");
130
+ await addSeasonRelations(data, payload, tmdbId, options);
131
+ await addRelatedTv(data, "similar", payload.similar, options);
132
+ await addRelatedTv(data, "related", payload.recommendations, options);
133
+ addDetail(data, "original_language", payload.original_language);
134
+ addDetail(data, "status", payload.status);
135
+ addDetail(data, "show_type", payload.type);
136
+ addDetail(data, "tagline", payload.tagline);
137
+ addDetail(data, "homepage", payload.homepage);
138
+ addDetail(data, "first_air_date", payload.first_air_date);
139
+ addDetail(data, "last_air_date", payload.last_air_date);
140
+ addNumberDetail(data, "season_count", payload.number_of_seasons);
141
+ addNumberDetail(data, "episode_count", payload.number_of_episodes);
142
+ addNumberDetail(data, "vote_count", payload.vote_count);
143
+ addNumberDetail(data, "popularity", payload.popularity);
144
+ addBooleanDetail(data, "adult", payload.adult);
145
+ addBooleanDetail(data, "in_production", payload.in_production);
146
+ addBooleanDetail(data, "softcore", payload.softcore);
147
+ addNumberArrayDetail(data, "episode_run_time", payload.episode_run_time);
148
+ addArrayDetail(data, "languages", payload.languages);
149
+ addArrayDetail(data, "origin_country", payload.origin_country);
150
+ addTvCertifications(data, payload);
151
+ addStructuredDetail(data, "production_countries", payload.production_countries);
152
+ addStructuredDetail(data, "spoken_languages", payload.spoken_languages);
153
+ addStructuredDetail(data, "content_ratings", payload.content_ratings?.results);
154
+ addStructuredDetail(data, "last_episode_to_air", payload.last_episode_to_air);
155
+ addStructuredDetail(data, "next_episode_to_air", payload.next_episode_to_air);
156
+ addStructuredDetail(data, "watch_providers", payload.watch_providers?.results);
157
+ return attachSourceMetadata(data, source);
158
+ }
159
+ function tmdbTvId(source, payload) {
160
+ const sourceId = source.source.externalId.trim();
161
+ if (sourceId) {
162
+ return sourceId;
163
+ }
164
+ const payloadId = payload.id === undefined ? undefined : String(payload.id).trim();
165
+ if (payloadId) {
166
+ return payloadId;
167
+ }
168
+ throw new Error("missing required TMDB tv field: id");
169
+ }
170
+ function addExternalIds(data, payload) {
171
+ const ids = payload.external_ids ?? {};
172
+ addExternalId(data, "imdb", ZUUID_TV_CATEGORY, stringField(ids.imdb_id ?? undefined));
173
+ addExternalId(data, "tvdb", ZUUID_TV_CATEGORY, numberOrStringField(ids.tvdb_id));
174
+ addExternalId(data, "tvrage", ZUUID_TV_CATEGORY, numberOrStringField(ids.tvrage_id));
175
+ addExternalId(data, "wikidata", ZUUID_TV_CATEGORY, stringField(ids.wikidata_id ?? undefined));
176
+ addExternalId(data, "facebook", ZUUID_TV_CATEGORY, stringField(ids.facebook_id ?? undefined));
177
+ addExternalId(data, "instagram", ZUUID_TV_CATEGORY, stringField(ids.instagram_id ?? undefined));
178
+ addExternalId(data, "twitter", ZUUID_TV_CATEGORY, stringField(ids.twitter_id ?? undefined));
179
+ }
180
+ function addExternalId(data, source, category, value) {
181
+ if (!value || data.externalIds.some((id) => id.source === source && id.category === category && id.value === value)) {
182
+ return;
183
+ }
184
+ data.externalIds.push({ source, category, value });
185
+ }
186
+ function addTranslations(data, payload, primaryTitle) {
187
+ for (const item of payload.translations?.translations ?? []) {
188
+ const title = stringField(item.data?.name);
189
+ const language = stringField(item.iso_639_1);
190
+ const region = stringField(item.iso_3166_1);
191
+ if (title && title !== primaryTitle) {
192
+ data.aliases.push({ value: title, language, region, aliasType: "translation", isPrimary: false, source: TMDB_PROVIDER });
193
+ }
194
+ const overview = stringField(item.data?.overview);
195
+ if (overview) {
196
+ data.descriptions.push({ language, region, value: overview, source: TMDB_PROVIDER });
197
+ }
198
+ }
199
+ }
200
+ async function addCreatedByRelations(data, payload) {
201
+ for (const creator of payload.created_by ?? []) {
202
+ if (!creator.id || !creator.name) {
203
+ continue;
204
+ }
205
+ const id = String(creator.id);
206
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category: "person", externalId: id });
207
+ data.relations.push({
208
+ id: zuuid,
209
+ zuuid,
210
+ relationType: "creator",
211
+ direction: "outgoing",
212
+ title: creator.name,
213
+ category: "person",
214
+ date: null,
215
+ cover: mediaUrl(creator.profile_path ?? undefined, TMDB_POSTER_BASE_URL) ?? null,
216
+ rating: null,
217
+ weight: null,
218
+ source: TMDB_PROVIDER,
219
+ externalId: id,
220
+ attribute: null,
221
+ order: null
222
+ });
223
+ }
224
+ }
225
+ async function addAggregateCreditRelations(data, payload) {
226
+ for (const cast of payload.aggregate_credits?.cast ?? []) {
227
+ if (!cast.id || !cast.name) {
228
+ continue;
229
+ }
230
+ const id = String(cast.id);
231
+ const primaryRole = cast.roles?.[0];
232
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category: "person", externalId: id });
233
+ data.relations.push({
234
+ id: zuuid,
235
+ zuuid,
236
+ relationType: "performed_by",
237
+ direction: "outgoing",
238
+ title: cast.name,
239
+ category: "person",
240
+ date: null,
241
+ cover: mediaUrl(cast.profile_path ?? undefined, TMDB_POSTER_BASE_URL) ?? null,
242
+ rating: null,
243
+ weight: null,
244
+ source: TMDB_PROVIDER,
245
+ externalId: id,
246
+ attribute: stringField(primaryRole?.character) ?? null,
247
+ order: 0,
248
+ data: cast
249
+ });
250
+ }
251
+ for (const crew of payload.aggregate_credits?.crew ?? []) {
252
+ if (!crew.id || !crew.name) {
253
+ continue;
254
+ }
255
+ const id = String(crew.id);
256
+ const primaryJob = crew.jobs?.[0]?.job ?? crew.department;
257
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category: "person", externalId: id });
258
+ data.relations.push({
259
+ id: zuuid,
260
+ zuuid,
261
+ relationType: primaryJob ? relationForCrewJob(primaryJob) : "related_to",
262
+ direction: "outgoing",
263
+ title: crew.name,
264
+ category: "person",
265
+ date: null,
266
+ cover: mediaUrl(crew.profile_path ?? undefined, TMDB_POSTER_BASE_URL) ?? null,
267
+ rating: null,
268
+ weight: null,
269
+ source: TMDB_PROVIDER,
270
+ externalId: id,
271
+ attribute: stringField(primaryJob) ?? null,
272
+ order: 0,
273
+ data: crew
274
+ });
275
+ }
276
+ }
277
+ async function addNamedRelations(data, values, relationType, category) {
278
+ for (const [index, value] of (values ?? []).entries()) {
279
+ if (!value.id || !value.name) {
280
+ continue;
281
+ }
282
+ const id = String(value.id);
283
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category, externalId: id });
284
+ data.relations.push({
285
+ id: zuuid,
286
+ zuuid,
287
+ relationType,
288
+ direction: "outgoing",
289
+ title: value.name,
290
+ category,
291
+ date: null,
292
+ cover: mediaUrl("logo_path" in value ? value.logo_path ?? undefined : undefined, TMDB_POSTER_BASE_URL) ?? null,
293
+ rating: null,
294
+ weight: null,
295
+ source: TMDB_PROVIDER,
296
+ externalId: id,
297
+ attribute: stringField(value.origin_country) ?? null,
298
+ order: index
299
+ });
300
+ }
301
+ }
302
+ async function addSeasonRelations(data, payload, tvId, options) {
303
+ for (const season of payload.seasons ?? []) {
304
+ if (season.season_number === undefined || !season.name) {
305
+ continue;
306
+ }
307
+ const externalId = `${tvId}-${season.season_number}`;
308
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category: "tvseason", externalId });
309
+ data.relations.push({
310
+ id: zuuid,
311
+ zuuid,
312
+ relationType: "contains",
313
+ direction: "outgoing",
314
+ title: season.name,
315
+ category: "tvseason",
316
+ date: stringField(season.air_date) ?? null,
317
+ cover: mediaUrl(season.poster_path ?? undefined, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL) ?? null,
318
+ rating: null,
319
+ weight: null,
320
+ source: TMDB_PROVIDER,
321
+ externalId,
322
+ attribute: `season:${season.name}`,
323
+ order: season.season_number,
324
+ data: season
325
+ });
326
+ addImage(data, "season_poster", season.poster_path ?? undefined, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL, false);
327
+ }
328
+ }
329
+ async function addRelatedTv(data, recommendationType, list, options) {
330
+ for (const item of list?.results ?? []) {
331
+ if (!item.id) {
332
+ continue;
333
+ }
334
+ const id = String(item.id);
335
+ const title = stringField(item.name) ?? stringField(item.original_name);
336
+ if (!title) {
337
+ continue;
338
+ }
339
+ const zuuid = await providerZuuid({ provider: TMDB_PROVIDER, category: TMDB_TV_CATEGORY, externalId: id });
340
+ const rating = typeof item.vote_average === "number" ? item.vote_average : null;
341
+ data.recommendations.push({
342
+ id: zuuid,
343
+ zuuid,
344
+ recommendationType,
345
+ relationType: recommendationType,
346
+ weight: rating,
347
+ source: TMDB_PROVIDER,
348
+ title,
349
+ category: ZUUID_TV_CATEGORY,
350
+ date: stringField(item.first_air_date) ?? null,
351
+ cover: mediaUrl(item.poster_path ?? undefined, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL) ?? null,
352
+ rating,
353
+ externalId: id,
354
+ attribute: null,
355
+ order: null,
356
+ reasons: [recommendationType]
357
+ });
358
+ }
359
+ }
360
+ function addImages(data, payload, options) {
361
+ for (const image of payload.images?.posters ?? []) {
362
+ addImage(data, "poster", image.file_path, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL, false, image);
363
+ }
364
+ for (const image of payload.images?.backdrops ?? []) {
365
+ addImage(data, "backdrop", image.file_path, options.backdropBaseUrl ?? TMDB_BACKDROP_BASE_URL, false, image);
366
+ }
367
+ for (const image of payload.images?.logos ?? []) {
368
+ addImage(data, "logo", image.file_path, options.posterBaseUrl ?? TMDB_POSTER_BASE_URL, false, image);
369
+ }
370
+ }
371
+ function addImage(data, mediaCategory, path, baseUrl, isPrimary, image) {
372
+ const url = mediaUrl(path, baseUrl);
373
+ if (!url) {
374
+ return;
375
+ }
376
+ data.media.push({
377
+ url,
378
+ mediaType: "image",
379
+ mediaCategory,
380
+ width: image?.width,
381
+ height: image?.height,
382
+ isPrimary,
383
+ data: image,
384
+ source: TMDB_PROVIDER
385
+ });
386
+ }
387
+ function addTag(data, value) {
388
+ const tag = stringField(value)?.toLowerCase();
389
+ if (tag && !data.tags.includes(tag)) {
390
+ data.tags.push(tag);
391
+ }
392
+ }
393
+ function addDetail(data, key, value) {
394
+ const normalized = stringField(value);
395
+ if (normalized) {
396
+ data.details.push({ key, value: normalized, source: TMDB_PROVIDER });
397
+ }
398
+ }
399
+ function addNumberDetail(data, key, value) {
400
+ if (typeof value === "number") {
401
+ data.details.push({ key, value, source: TMDB_PROVIDER });
402
+ }
403
+ }
404
+ function addBooleanDetail(data, key, value) {
405
+ if (typeof value === "boolean") {
406
+ data.details.push({ key, value, source: TMDB_PROVIDER });
407
+ }
408
+ }
409
+ function addNumberArrayDetail(data, key, value) {
410
+ if (value?.length) {
411
+ data.details.push({ key, value, source: TMDB_PROVIDER });
412
+ }
413
+ }
414
+ function addArrayDetail(data, key, value) {
415
+ if (value?.length) {
416
+ data.details.push({ key, value, source: TMDB_PROVIDER });
417
+ }
418
+ }
419
+ function addTvCertifications(data, payload) {
420
+ const certifications = [];
421
+ for (const item of payload.content_ratings?.results ?? []) {
422
+ const region = stringField(item.iso_3166_1);
423
+ const certification = stringField(item.rating);
424
+ if (!region || !certification) {
425
+ continue;
426
+ }
427
+ certifications.push({
428
+ region,
429
+ certification,
430
+ descriptors: item.descriptors ?? []
431
+ });
432
+ }
433
+ if (!certifications.length) {
434
+ return;
435
+ }
436
+ data.details.push({
437
+ key: "certifications",
438
+ value: certifications,
439
+ source: TMDB_PROVIDER
440
+ });
441
+ }
442
+ function addStructuredDetail(data, key, value) {
443
+ if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) {
444
+ return;
445
+ }
446
+ data.details.push({ key, value, source: TMDB_PROVIDER });
447
+ }
448
+ function stringField(value) {
449
+ const normalized = value?.trim();
450
+ return normalized ? normalized : undefined;
451
+ }
452
+ function numberOrStringField(value) {
453
+ if (value === null || value === undefined) {
454
+ return undefined;
455
+ }
456
+ return stringField(String(value));
457
+ }
458
+ function relationForCrewJob(job) {
459
+ switch (job.toLowerCase()) {
460
+ case "creator":
461
+ case "executive producer":
462
+ case "producer":
463
+ return "produced_by";
464
+ case "director":
465
+ return "directed_by";
466
+ case "writer":
467
+ case "screenplay":
468
+ case "novel":
469
+ return "authored_by";
470
+ default:
471
+ return "related_to";
472
+ }
473
+ }
474
+ function mediaUrl(path, baseUrl) {
475
+ const value = stringField(path);
476
+ if (!value) {
477
+ return undefined;
478
+ }
479
+ if (value.startsWith("http://") || value.startsWith("https://")) {
480
+ return value;
481
+ }
482
+ return baseUrl ? `${baseUrl.replace(/\/$/, "")}/${value.replace(/^\//, "")}` : value;
483
+ }
484
+ function validateDate(value, field) {
485
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || Number.isNaN(Date.parse(`${value}T00:00:00.000Z`))) {
486
+ throw new Error(`invalid TMDB tv field ${field}: ${value}`);
487
+ }
488
+ }
489
+ function searchQuery(value, field) {
490
+ const normalized = stringField(value);
491
+ if (!normalized) {
492
+ throw new Error(`${field} must not be empty`);
493
+ }
494
+ return normalized;
495
+ }
496
+ function searchParams(provider, input) {
497
+ return {
498
+ language: input.language ?? provider.language,
499
+ page: String(input.page ?? 1),
500
+ include_adult: String(input.includeAdult ?? false)
501
+ };
502
+ }
503
+ async function sourceRecordsFromSearchResults(category, results) {
504
+ const records = [];
505
+ for (const result of results ?? []) {
506
+ if (!result.id) {
507
+ continue;
508
+ }
509
+ records.push(await createSourceRecord({
510
+ source: { provider: TMDB_PROVIDER, category, externalId: String(result.id) },
511
+ payload: result
512
+ }));
513
+ }
514
+ return records;
515
+ }
516
+ function paginationFromTmdbSearchResponse(payload) {
517
+ return {
518
+ page: payload.page ?? 1,
519
+ totalPages: payload.total_pages ?? 0,
520
+ totalResults: payload.total_results ?? 0
521
+ };
522
+ }
@@ -0,0 +1,36 @@
1
+ export type TmdbCredential = {
2
+ apiKey: string;
3
+ bearerToken?: never;
4
+ } | {
5
+ apiKey?: never;
6
+ bearerToken: string;
7
+ };
8
+ export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
9
+ export type TmdbProviderOptions = TmdbCredential & {
10
+ apiBase?: string;
11
+ fetch?: FetchLike;
12
+ posterBaseUrl?: string | null;
13
+ backdropBaseUrl?: string | null;
14
+ language?: string;
15
+ };
16
+ export type TmdbTransformOptions = {
17
+ posterBaseUrl?: string | null;
18
+ backdropBaseUrl?: string | null;
19
+ };
20
+ export type TmdbSearchInput = {
21
+ query: string;
22
+ page?: number;
23
+ includeAdult?: boolean;
24
+ language?: string;
25
+ region?: string;
26
+ year?: number;
27
+ primaryReleaseYear?: number;
28
+ firstAirDateYear?: number;
29
+ };
30
+ export type TmdbSearchResponse<T> = {
31
+ page?: number;
32
+ results?: T[];
33
+ total_pages?: number;
34
+ total_results?: number;
35
+ };
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/tmdb/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GACtB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GACvC;IAAE,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvF,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { JsonValue } from "./types.js";
2
+ import type { ZuuidData } from "./entity.js";
3
+ export type SourceRecordRef = {
4
+ provider: string;
5
+ category: string;
6
+ externalId: string;
7
+ };
8
+ export type ExternalId = {
9
+ source: string;
10
+ category: string;
11
+ value: string;
12
+ };
13
+ export type Provenance = {
14
+ source: SourceRecordRef;
15
+ observedAt: string;
16
+ confidence?: number;
17
+ contentHash?: string;
18
+ };
19
+ export type SourceRecord = {
20
+ source: SourceRecordRef;
21
+ payload: JsonValue;
22
+ contentHash: string;
23
+ observedAt: string;
24
+ publishedAt?: string;
25
+ };
26
+ export type CreateSourceRecordInput = {
27
+ source: SourceRecordRef;
28
+ payload: JsonValue;
29
+ observedAt?: string | Date;
30
+ publishedAt?: string | Date;
31
+ };
32
+ export declare function createSourceRecord(input: CreateSourceRecordInput): Promise<SourceRecord>;
33
+ export declare function attachSourceMetadata(dataset: ZuuidData, sourceRecord: SourceRecord, confidence?: number): ZuuidData;
34
+ export declare function externalIdFromSource(source: SourceRecordRef): ExternalId;
35
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,eAAe,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,SAAS,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,SAAS,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,YAAY,CAAC,CAY9F;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,EAClB,YAAY,EAAE,YAAY,EAC1B,UAAU,SAAM,GACf,SAAS,CAoBX;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,eAAe,GAAG,UAAU,CAMxE"}
package/dist/source.js ADDED
@@ -0,0 +1,53 @@
1
+ import { stablePayloadHash } from "./hash.js";
2
+ export async function createSourceRecord(input) {
3
+ return {
4
+ source: {
5
+ provider: input.source.provider,
6
+ category: input.source.category,
7
+ externalId: input.source.externalId
8
+ },
9
+ payload: input.payload,
10
+ contentHash: await stablePayloadHash(input.payload),
11
+ observedAt: normalizeTimestamp(input.observedAt ?? new Date()),
12
+ publishedAt: input.publishedAt ? normalizeTimestamp(input.publishedAt) : undefined
13
+ };
14
+ }
15
+ export function attachSourceMetadata(dataset, sourceRecord, confidence = 1.0) {
16
+ const next = structuredClone(dataset);
17
+ const externalId = externalIdFromSource(sourceRecord.source);
18
+ if (!next.externalIds.some((item) => sameExternalId(item, externalId))) {
19
+ next.externalIds.push(externalId);
20
+ }
21
+ const provenance = {
22
+ source: sourceRecord.source,
23
+ observedAt: sourceRecord.observedAt,
24
+ confidence,
25
+ contentHash: sourceRecord.contentHash
26
+ };
27
+ if (!next.provenance.some((item) => sameProvenance(item, provenance))) {
28
+ next.provenance.push(provenance);
29
+ }
30
+ return next;
31
+ }
32
+ export function externalIdFromSource(source) {
33
+ return {
34
+ source: source.provider,
35
+ category: source.category,
36
+ value: source.externalId
37
+ };
38
+ }
39
+ function normalizeTimestamp(value) {
40
+ return value instanceof Date ? value.toISOString() : value;
41
+ }
42
+ function sameSourceRef(a, b) {
43
+ return a.provider === b.provider && a.category === b.category && a.externalId === b.externalId;
44
+ }
45
+ function sameExternalId(a, b) {
46
+ return a.source === b.source && a.category === b.category && a.value === b.value;
47
+ }
48
+ function sameProvenance(a, b) {
49
+ return (sameSourceRef(a.source, b.source) &&
50
+ a.observedAt === b.observedAt &&
51
+ a.confidence === b.confidence &&
52
+ a.contentHash === b.contentHash);
53
+ }
@@ -0,0 +1,5 @@
1
+ export type JsonPrimitive = string | number | boolean | null;
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAC7D,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,SAAS,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/uuid.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function normalizeUuid(value: string): string;
2
+ export declare function uuidV5(name: string, namespace: string): Promise<string>;
3
+ //# sourceMappingURL=uuid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uuid.d.ts","sourceRoot":"","sources":["../src/uuid.ts"],"names":[],"mappings":"AAEA,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc7E"}