@zivue/zuuid 0.2.0 → 0.2.2

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 (58) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +148 -5
  3. package/dist/client.d.ts +17 -0
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +36 -0
  6. package/dist/identity.d.ts +1 -1
  7. package/dist/identity.d.ts.map +1 -1
  8. package/dist/identity.js +1 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/providers/common.d.ts +1 -0
  13. package/dist/providers/common.d.ts.map +1 -1
  14. package/dist/providers/common.js +8 -0
  15. package/dist/providers/gamesdb/client.d.ts +19 -0
  16. package/dist/providers/gamesdb/client.d.ts.map +1 -0
  17. package/dist/providers/gamesdb/client.js +57 -0
  18. package/dist/providers/gamesdb/constants.d.ts +2 -0
  19. package/dist/providers/gamesdb/constants.d.ts.map +1 -1
  20. package/dist/providers/gamesdb/constants.js +2 -0
  21. package/dist/providers/gamesdb/game.d.ts +7 -5
  22. package/dist/providers/gamesdb/game.d.ts.map +1 -1
  23. package/dist/providers/gamesdb/game.js +205 -22
  24. package/dist/providers/gamesdb/index.d.ts +2 -0
  25. package/dist/providers/gamesdb/index.d.ts.map +1 -1
  26. package/dist/providers/gamesdb/index.js +2 -0
  27. package/dist/providers/gamesdb/platform.d.ts +5 -2
  28. package/dist/providers/gamesdb/platform.d.ts.map +1 -1
  29. package/dist/providers/gamesdb/platform.js +87 -4
  30. package/dist/providers/gamesdb/types.d.ts +21 -0
  31. package/dist/providers/gamesdb/types.d.ts.map +1 -0
  32. package/dist/providers/gamesdb/types.js +1 -0
  33. package/dist/providers/imdb/client.d.ts +16 -0
  34. package/dist/providers/imdb/client.d.ts.map +1 -0
  35. package/dist/providers/imdb/client.js +29 -0
  36. package/dist/providers/imdb/constants.d.ts +4 -0
  37. package/dist/providers/imdb/constants.d.ts.map +1 -0
  38. package/dist/providers/imdb/constants.js +3 -0
  39. package/dist/providers/imdb/index.d.ts +4 -0
  40. package/dist/providers/imdb/index.d.ts.map +1 -0
  41. package/dist/providers/imdb/index.js +3 -0
  42. package/dist/providers/imdb/movie.d.ts +16 -0
  43. package/dist/providers/imdb/movie.d.ts.map +1 -0
  44. package/dist/providers/imdb/movie.js +183 -0
  45. package/dist/providers/imdb/types.d.ts +10 -0
  46. package/dist/providers/imdb/types.d.ts.map +1 -0
  47. package/dist/providers/imdb/types.js +1 -0
  48. package/dist/providers/jikan/transform.d.ts.map +1 -1
  49. package/dist/providers/jikan/transform.js +4 -2
  50. package/dist/providers/openlibrary/book.d.ts.map +1 -1
  51. package/dist/providers/openlibrary/book.js +2 -1
  52. package/dist/providers/openstreetmap/place.d.ts.map +1 -1
  53. package/dist/providers/openstreetmap/place.js +4 -2
  54. package/dist/providers/tmdb/movie.d.ts.map +1 -1
  55. package/dist/providers/tmdb/movie.js +9 -22
  56. package/dist/providers/tmdb/tv.d.ts.map +1 -1
  57. package/dist/providers/tmdb/tv.js +4 -3
  58. package/package.json +13 -1
@@ -1,10 +1,59 @@
1
+ import { providerZuuid } from "../../identity.js";
2
+ import { createSourceRecord } from "../../source.js";
1
3
  import { GAMESDB_GAME_CATEGORY, GAMESDB_PROVIDER } from "./constants.js";
2
- import { addDescription, addDetail, addMedia, addTag, arrayField, baseDataFromSource, finalizeData, numberField, objectPayload, stringField, valueAsString } from "../common.js";
4
+ import { addAlias, addDescription, addDetail, addMedia, addRelation, addTag, arrayField, baseDataFromSource, finalizeData, normalizeRating, numberField, objectField, objectPayload, stringField, valueAsString } from "../common.js";
5
+ export async function fetchGamesDbGameSourceRecord(provider, input) {
6
+ const id = gamesDbNumericId(input.id, "GamesDB game id");
7
+ const payload = await provider.getJson("/Games/ByGameID", {
8
+ id,
9
+ fields: "players,publishers,genres,overview,last_updated,rating,platform,coop,youtube,os,processor,ram,hdd,video,sound,developers,alternates",
10
+ include: "boxart,platform"
11
+ });
12
+ if (!payload)
13
+ return undefined;
14
+ return createSourceRecord({ source: { provider: GAMESDB_PROVIDER, category: GAMESDB_GAME_CATEGORY, externalId: id }, payload });
15
+ }
16
+ export async function searchGamesDbGameSourceRecords(provider, input) {
17
+ const payload = await fetchGamesDbGameSearchPayload(provider, input);
18
+ const results = gamesFromPayload(objectPayload(payload)).filter((game) => valueAsString(game.id));
19
+ return {
20
+ results: await Promise.all(results.map((game) => createSourceRecord({ source: { provider: GAMESDB_PROVIDER, category: GAMESDB_GAME_CATEGORY, externalId: valueAsString(game.id) ?? "" }, payload: game }))),
21
+ pagination: gamesDbPagination(objectPayload(payload), results.length)
22
+ };
23
+ }
24
+ export async function searchGamesDbGames(provider, input, options = {}) {
25
+ const payload = objectPayload(await fetchGamesDbGameSearchPayload(provider, input));
26
+ const results = [];
27
+ for (const game of gamesFromPayload(payload)) {
28
+ const id = valueAsString(game.id);
29
+ const title = stringField(game, "game_title") ?? stringField(game, "name") ?? stringField(game, "title");
30
+ if (!id || !title)
31
+ continue;
32
+ const zuuid = await providerZuuid({ provider: GAMESDB_PROVIDER, category: GAMESDB_GAME_CATEGORY, externalId: id });
33
+ const rawRating = numberField(game, "rating");
34
+ results.push({
35
+ id: zuuid,
36
+ zuuid,
37
+ category: GAMESDB_GAME_CATEGORY,
38
+ title,
39
+ date: releaseDate(stringField(game, "release_date")) ?? null,
40
+ cover: mediaUrl(primaryImage(game, payload, id), imageBaseUrl(payload, options.imageBaseUrl)) ?? null,
41
+ rating: normalizeRating(rawRating, 0, 10) ?? null,
42
+ weight: null,
43
+ relationType: null,
44
+ attribute: null,
45
+ order: null,
46
+ source: { source: GAMESDB_PROVIDER, category: GAMESDB_GAME_CATEGORY, value: id }
47
+ });
48
+ }
49
+ return { results, pagination: gamesDbPagination(payload, results.length) };
50
+ }
3
51
  export async function transformGamesDbGame(source, options = {}) {
4
52
  if (source.source.provider !== GAMESDB_PROVIDER || source.source.category !== GAMESDB_GAME_CATEGORY) {
5
53
  throw new Error(`unsupported GamesDB source: ${source.source.provider}:${source.source.category}`);
6
54
  }
7
- const payload = objectPayload(source.payload);
55
+ const envelope = objectPayload(source.payload);
56
+ const payload = gameFromPayload(envelope, source.source.externalId);
8
57
  const id = source.source.externalId.trim() || valueAsString(payload.id);
9
58
  const title = stringField(payload, "game_title") ?? stringField(payload, "name") ?? stringField(payload, "title");
10
59
  if (!id)
@@ -12,26 +61,78 @@ export async function transformGamesDbGame(source, options = {}) {
12
61
  if (!title)
13
62
  throw new Error("missing required GamesDB game field: game_title");
14
63
  const data = await baseDataFromSource(source, GAMESDB_PROVIDER, GAMESDB_GAME_CATEGORY, GAMESDB_GAME_CATEGORY, id, title);
15
- data.rating = numberField(payload, "rating");
64
+ addAlias(data, title, "title", true, GAMESDB_PROVIDER);
65
+ for (const alternate of alternates(payload))
66
+ addAlias(data, alternate, "alternate", false, GAMESDB_PROVIDER);
67
+ const rawRating = numericRating(payload.rating);
68
+ data.rating = normalizeRating(rawRating, 0, 10);
69
+ addDetail(data, GAMESDB_PROVIDER, "provider_rating", rawRating);
70
+ if (rawRating === undefined)
71
+ addDetail(data, GAMESDB_PROVIDER, "content_rating", valueAsString(payload.rating));
16
72
  data.primaryDate = releaseDate(stringField(payload, "release_date"));
17
73
  addDescription(data, GAMESDB_PROVIDER, stringField(payload, "overview"));
18
- addTagsFromNamedArray(data, payload, "genres");
19
- addTagsFromNamedArray(data, payload, "platforms");
20
- addDetail(data, GAMESDB_PROVIDER, "developers", joinedNames(payload, "developers"));
21
- addDetail(data, GAMESDB_PROVIDER, "publishers", joinedNames(payload, "publishers"));
22
- addDetail(data, GAMESDB_PROVIDER, "players", stringField(payload, "players"));
23
- addDetail(data, GAMESDB_PROVIDER, "release_date", stringField(payload, "release_date"));
24
- addMedia(data, GAMESDB_PROVIDER, mediaUrl(stringField(payload, "boxart") ?? stringField(payload, "box_art") ?? stringField(payload, "cover"), options.imageBaseUrl), "cover");
74
+ addTagsFromNamedArray(data, payload, envelope, "genres");
75
+ addTagsFromNamedArray(data, payload, envelope, "platforms");
76
+ addDetail(data, GAMESDB_PROVIDER, "platform", lookupName(envelope, "platforms", payload.platform));
77
+ addDetail(data, GAMESDB_PROVIDER, "developers", joinedNames(payload, envelope, "developers"));
78
+ addDetail(data, GAMESDB_PROVIDER, "publishers", joinedNames(payload, envelope, "publishers"));
79
+ for (const key of ["players", "coop", "youtube", "os", "processor", "ram", "hdd", "video", "sound", "last_updated", "release_date"]) {
80
+ addDetail(data, GAMESDB_PROVIDER, key, valueAsString(payload[key]));
81
+ }
82
+ for (const media of mediaCandidates(payload, envelope, id, options.imageBaseUrl)) {
83
+ addMedia(data, GAMESDB_PROVIDER, media.url, media.category, "image", media.primary);
84
+ }
85
+ const platformId = valueAsString(payload.platform);
86
+ if (platformId)
87
+ await addRelation(data, GAMESDB_PROVIDER, "platform", platformId, "released_on", lookupName(envelope, "platforms", payload.platform));
25
88
  return finalizeData(data, source);
26
89
  }
90
+ async function fetchGamesDbGameSearchPayload(provider, input) {
91
+ const query = input.query.trim();
92
+ if (!query)
93
+ throw new Error("GamesDB game search query must not be empty");
94
+ return (await provider.getJson("/Games/ByGameName", {
95
+ name: query,
96
+ fields: "players,publishers,genres,overview,rating,platform,developers",
97
+ include: "boxart,platform",
98
+ ...(input.page ? { page: String(input.page) } : {})
99
+ })) ?? { data: { games: [] } };
100
+ }
101
+ function gamesDbNumericId(value, label) {
102
+ const id = String(value).trim();
103
+ if (!/^\d+$/.test(id))
104
+ throw new Error(`${label} must be numeric: ${id}`);
105
+ return id;
106
+ }
107
+ function gamesDbPagination(payload, resultCount) {
108
+ const pages = objectPayload(payload.pages ?? {});
109
+ const page = numberField(pages, "current") ?? numberField(pages, "previous") ?? 1;
110
+ const totalPages = numberField(pages, "total") ?? (resultCount ? 1 : 0);
111
+ return { page, totalPages, totalResults: resultCount };
112
+ }
113
+ function gameFromPayload(envelope, externalId) {
114
+ const games = gamesFromPayload(envelope);
115
+ if (!games.length)
116
+ return envelope;
117
+ return games.find((game) => valueAsString(game.id) === externalId.trim()) ?? games[0] ?? envelope;
118
+ }
119
+ function gamesFromPayload(payload) {
120
+ const data = objectPayload(payload.data ?? {});
121
+ const games = data.games ?? payload.games;
122
+ if (Array.isArray(games))
123
+ return games.filter(isObject);
124
+ if (isObject(games))
125
+ return Object.values(games).filter(isObject);
126
+ return [];
127
+ }
27
128
  function releaseDate(value) {
28
129
  if (!value)
29
130
  return undefined;
30
131
  if (/^\d{4}-\d{2}-\d{2}$/.test(value))
31
132
  return value;
32
- const match = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(value);
33
- if (match)
34
- return `${match[3]}-${match[1].padStart(2, "0")}-${match[2].padStart(2, "0")}`;
133
+ const slash = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(value);
134
+ if (slash)
135
+ return `${slash[3]}-${slash[1].padStart(2, "0")}-${slash[2].padStart(2, "0")}`;
35
136
  return /^\d{4}$/.test(value) ? `${value}-01-01` : undefined;
36
137
  }
37
138
  function mediaUrl(path, baseUrl) {
@@ -41,19 +142,101 @@ function mediaUrl(path, baseUrl) {
41
142
  return path;
42
143
  return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
43
144
  }
44
- function joinedNames(payload, key) {
45
- const names = arrayField(payload, key).map(valueName).filter((value) => !!value);
145
+ function imageBaseUrl(envelope, fallback) {
146
+ const boxart = objectPayload(objectPayload(envelope.include ?? {}).boxart ?? objectPayload(envelope.data ?? {}).boxart ?? envelope.boxart ?? {});
147
+ const baseUrl = objectPayload(boxart.base_url ?? {});
148
+ return stringField(baseUrl, "original") ?? stringField(baseUrl, "large") ?? fallback;
149
+ }
150
+ function primaryImage(payload, envelope, id) {
151
+ return mediaCandidates(payload, envelope, id, undefined)[0]?.url;
152
+ }
153
+ function mediaCandidates(payload, envelope, id, fallbackBaseUrl) {
154
+ const baseUrl = imageBaseUrl(envelope, fallbackBaseUrl);
155
+ const output = [];
156
+ const direct = stringField(payload, "boxart") ?? stringField(payload, "box_art") ?? stringField(payload, "cover");
157
+ const directUrl = mediaUrl(direct, baseUrl);
158
+ if (directUrl)
159
+ output.push({ url: directUrl, category: "cover", primary: true });
160
+ for (const image of boxartImages(envelope, id)) {
161
+ const filename = stringField(image, "filename") ?? stringField(image, "value") ?? stringField(image, "url");
162
+ const url = mediaUrl(filename, baseUrl);
163
+ if (!url || output.some((item) => item.url === url))
164
+ continue;
165
+ const type = stringField(image, "type") ?? "boxart";
166
+ const side = stringField(image, "side");
167
+ output.push({ url, category: side ? `${type}_${side}` : type, primary: output.length === 0 || side === "front" });
168
+ }
169
+ return output;
170
+ }
171
+ function boxartImages(envelope, id) {
172
+ const boxart = objectPayload(objectPayload(envelope.include ?? {}).boxart ?? objectPayload(envelope.data ?? {}).boxart ?? envelope.boxart ?? {});
173
+ const data = boxart.data;
174
+ if (Array.isArray(data))
175
+ return data.filter(isObject);
176
+ if (isObject(data)) {
177
+ const byId = data[id];
178
+ if (Array.isArray(byId))
179
+ return byId.filter(isObject);
180
+ if (isObject(byId))
181
+ return [byId];
182
+ return Object.values(data).flatMap((value) => Array.isArray(value) ? value.filter(isObject) : isObject(value) ? [value] : []);
183
+ }
184
+ return [];
185
+ }
186
+ function joinedNames(payload, envelope, key) {
187
+ const names = arrayField(payload, key).map((value) => valueName(value, envelope, key)).filter((value) => !!value);
46
188
  return names.length ? names.join(", ") : undefined;
47
189
  }
48
- function addTagsFromNamedArray(data, payload, key) {
190
+ function addTagsFromNamedArray(data, payload, envelope, key) {
49
191
  for (const item of arrayField(payload, key))
50
- addTag(data, valueName(item));
192
+ addTag(data, valueName(item, envelope, key));
51
193
  }
52
- function valueName(value) {
194
+ function valueName(value, envelope, key) {
53
195
  if (typeof value === "string")
54
- return value;
55
- if (!value || typeof value !== "object" || Array.isArray(value))
196
+ return lookupName(envelope, key, value) ?? value;
197
+ if (typeof value === "number")
198
+ return lookupName(envelope, key, value) ?? String(value);
199
+ if (!isObject(value))
200
+ return undefined;
201
+ return stringField(value, "name") ?? stringField(value, "genre") ?? stringField(value, "platform");
202
+ }
203
+ function lookupName(envelope, key, id) {
204
+ const normalizedId = valueAsString(id);
205
+ if (!normalizedId)
56
206
  return undefined;
57
- const object = value;
58
- return stringField(object, "name") ?? stringField(object, "genre") ?? stringField(object, "platform");
207
+ const data = objectPayload(envelope.data ?? {});
208
+ const include = objectPayload(envelope.include ?? {});
209
+ const includeKey = key === "platforms" ? "platform" : key;
210
+ const includeEntry = objectPayload(include[includeKey] ?? {});
211
+ const values = includeEntry.data ?? data[key] ?? envelope[key];
212
+ if (Array.isArray(values))
213
+ return values.filter(isObject).find((item) => valueAsString(item.id) === normalizedId)?.name;
214
+ if (isObject(values)) {
215
+ const item = values[normalizedId];
216
+ if (isObject(item))
217
+ return stringField(item, "name");
218
+ if (typeof item === "string")
219
+ return item;
220
+ }
221
+ return undefined;
222
+ }
223
+ function alternates(payload) {
224
+ const value = payload.alternates;
225
+ if (typeof value === "string")
226
+ return value.split(/[|,]/).map((item) => item.trim()).filter(Boolean);
227
+ if (Array.isArray(value))
228
+ return value.map((item) => valueAsString(item)).filter((item) => !!item);
229
+ return [];
230
+ }
231
+ function numericRating(value) {
232
+ if (typeof value === "number")
233
+ return Number.isFinite(value) ? value : undefined;
234
+ if (typeof value === "string") {
235
+ const parsed = Number(value);
236
+ return Number.isFinite(parsed) ? parsed : undefined;
237
+ }
238
+ return undefined;
239
+ }
240
+ function isObject(value) {
241
+ return !!value && typeof value === "object" && !Array.isArray(value);
59
242
  }
@@ -1,4 +1,6 @@
1
+ export * from "./client.js";
1
2
  export * from "./constants.js";
2
3
  export * from "./game.js";
3
4
  export * from "./platform.js";
5
+ export * from "./types.js";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/gamesdb/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/gamesdb/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
@@ -1,3 +1,5 @@
1
+ export * from "./client.js";
1
2
  export * from "./constants.js";
2
3
  export * from "./game.js";
3
4
  export * from "./platform.js";
5
+ export * from "./types.js";
@@ -1,4 +1,7 @@
1
1
  import type { ZuuidData } from "../../entity.js";
2
- import type { SourceRecord } from "../../source.js";
3
- export declare function transformGamesDbPlatform(source: SourceRecord): Promise<ZuuidData>;
2
+ import { type SourceRecord } from "../../source.js";
3
+ import type { GamesDbProvider } from "./client.js";
4
+ import type { FetchGamesDbPlatformInput, GamesDbTransformOptions } from "./types.js";
5
+ export declare function fetchGamesDbPlatformSourceRecord(provider: GamesDbProvider, input: FetchGamesDbPlatformInput): Promise<SourceRecord | undefined>;
6
+ export declare function transformGamesDbPlatform(source: SourceRecord, options?: GamesDbTransformOptions): Promise<ZuuidData>;
4
7
  //# sourceMappingURL=platform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/providers/gamesdb/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIpD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAiBvF"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/providers/gamesdb/platform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAIxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,yBAAyB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAErF,wBAAsB,gCAAgC,CACpD,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CASnC;AAED,wBAAsB,wBAAwB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,SAAS,CAAC,CAmB9H"}
@@ -1,10 +1,23 @@
1
+ import { createSourceRecord } from "../../source.js";
1
2
  import { addAlias, addDescription, addDetail, addMedia, addTag, baseDataFromSource, finalizeData, objectPayload, stringField, valueAsString } from "../common.js";
2
3
  import { GAMESDB_PLATFORM_CATEGORY, GAMESDB_PROVIDER } from "./constants.js";
3
- export async function transformGamesDbPlatform(source) {
4
+ export async function fetchGamesDbPlatformSourceRecord(provider, input) {
5
+ const id = gamesDbNumericId(input.id, "GamesDB platform id");
6
+ const payload = await provider.getJson("/Platforms/ByPlatformID", {
7
+ id,
8
+ fields: "icon,console,controller,developer,manufacturer,media,cpu,memory,graphics,sound,maxcontrollers,display,overview,youtube",
9
+ include: "boxart"
10
+ });
11
+ if (!payload)
12
+ return undefined;
13
+ return createSourceRecord({ source: { provider: GAMESDB_PROVIDER, category: GAMESDB_PLATFORM_CATEGORY, externalId: id }, payload });
14
+ }
15
+ export async function transformGamesDbPlatform(source, options = {}) {
4
16
  if (source.source.provider !== GAMESDB_PROVIDER || source.source.category !== GAMESDB_PLATFORM_CATEGORY) {
5
17
  throw new Error(`unsupported GamesDB source: ${source.source.provider}:${source.source.category}`);
6
18
  }
7
- const payload = objectPayload(source.payload);
19
+ const envelope = objectPayload(source.payload);
20
+ const payload = platformFromPayload(envelope, source.source.externalId);
8
21
  const id = source.source.externalId.trim() || valueAsString(payload.id);
9
22
  const title = stringField(payload, "name") ?? stringField(payload, "platform");
10
23
  if (!id)
@@ -13,11 +26,81 @@ export async function transformGamesDbPlatform(source) {
13
26
  throw new Error("missing required GamesDB platform field: name");
14
27
  const data = await baseDataFromSource(source, GAMESDB_PROVIDER, GAMESDB_PLATFORM_CATEGORY, GAMESDB_PLATFORM_CATEGORY, id, title);
15
28
  addAlias(data, title, "title", true, GAMESDB_PROVIDER);
29
+ addAlias(data, stringField(payload, "alias"), "slug", false, GAMESDB_PROVIDER);
16
30
  addDescription(data, GAMESDB_PROVIDER, stringField(payload, "overview") ?? stringField(payload, "description"));
17
- for (const key of ["manufacturer", "developer", "release_date", "cpu", "memory", "graphics", "sound", "display"])
31
+ for (const key of ["manufacturer", "developer", "media", "release_date", "cpu", "memory", "graphics", "sound", "display", "maxcontrollers", "youtube"])
18
32
  addDetail(data, GAMESDB_PROVIDER, key, valueAsString(payload[key]));
19
- addMedia(data, GAMESDB_PROVIDER, stringField(payload, "icon") ?? stringField(payload, "image") ?? stringField(payload, "console"), "image");
33
+ for (const media of mediaCandidates(payload, envelope, id, options.imageBaseUrl))
34
+ addMedia(data, GAMESDB_PROVIDER, media.url, media.category, "image", media.primary);
20
35
  addTag(data, "platform");
21
36
  addTag(data, "game");
22
37
  return finalizeData(data, source);
23
38
  }
39
+ function gamesDbNumericId(value, label) {
40
+ const id = String(value).trim();
41
+ if (!/^\d+$/.test(id))
42
+ throw new Error(`${label} must be numeric: ${id}`);
43
+ return id;
44
+ }
45
+ function platformFromPayload(envelope, externalId) {
46
+ const platforms = platformsFromPayload(envelope);
47
+ if (!platforms.length)
48
+ return envelope;
49
+ return platforms.find((platform) => valueAsString(platform.id) === externalId.trim()) ?? platforms[0] ?? envelope;
50
+ }
51
+ function platformsFromPayload(payload) {
52
+ const data = objectPayload(payload.data ?? {});
53
+ const platforms = data.platforms ?? payload.platforms;
54
+ if (Array.isArray(platforms))
55
+ return platforms.filter(isObject);
56
+ if (isObject(platforms))
57
+ return Object.values(platforms).filter(isObject);
58
+ return [];
59
+ }
60
+ function mediaCandidates(payload, envelope, id, fallbackBaseUrl) {
61
+ const baseUrl = imageBaseUrl(envelope, fallbackBaseUrl);
62
+ const output = [];
63
+ for (const [key, category] of [["icon", "icon"], ["console", "console"], ["controller", "controller"], ["image", "image"]]) {
64
+ const url = mediaUrl(stringField(payload, key), baseUrl);
65
+ if (url)
66
+ output.push({ url, category, primary: output.length === 0 });
67
+ }
68
+ for (const image of boxartImages(envelope, id)) {
69
+ const filename = stringField(image, "filename") ?? stringField(image, "value") ?? stringField(image, "url");
70
+ const url = mediaUrl(filename, baseUrl);
71
+ if (!url || output.some((item) => item.url === url))
72
+ continue;
73
+ output.push({ url, category: stringField(image, "type") ?? "boxart", primary: output.length === 0 });
74
+ }
75
+ return output;
76
+ }
77
+ function imageBaseUrl(envelope, fallback) {
78
+ const boxart = objectPayload(objectPayload(envelope.include ?? {}).boxart ?? objectPayload(envelope.data ?? {}).boxart ?? envelope.boxart ?? {});
79
+ const baseUrl = objectPayload(boxart.base_url ?? {});
80
+ return stringField(baseUrl, "original") ?? stringField(baseUrl, "large") ?? fallback;
81
+ }
82
+ function boxartImages(envelope, id) {
83
+ const boxart = objectPayload(objectPayload(envelope.include ?? {}).boxart ?? objectPayload(envelope.data ?? {}).boxart ?? envelope.boxart ?? {});
84
+ const data = boxart.data;
85
+ if (Array.isArray(data))
86
+ return data.filter(isObject);
87
+ if (isObject(data)) {
88
+ const byId = data[id];
89
+ if (Array.isArray(byId))
90
+ return byId.filter(isObject);
91
+ if (isObject(byId))
92
+ return [byId];
93
+ return Object.values(data).flatMap((value) => Array.isArray(value) ? value.filter(isObject) : isObject(value) ? [value] : []);
94
+ }
95
+ return [];
96
+ }
97
+ function mediaUrl(path, baseUrl) {
98
+ if (!path)
99
+ return undefined;
100
+ if (/^https?:\/\//.test(path) || !baseUrl)
101
+ return path;
102
+ return `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
103
+ }
104
+ function isObject(value) {
105
+ return !!value && typeof value === "object" && !Array.isArray(value);
106
+ }
@@ -0,0 +1,21 @@
1
+ export type GamesDbFetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
2
+ export type GamesDbProviderOptions = {
3
+ apiKey: string;
4
+ apiBase?: string;
5
+ fetch?: GamesDbFetchLike;
6
+ imageBaseUrl?: string | null;
7
+ };
8
+ export type GamesDbTransformOptions = {
9
+ imageBaseUrl?: string | null;
10
+ };
11
+ export type FetchGamesDbGameInput = {
12
+ id: string | number;
13
+ };
14
+ export type FetchGamesDbPlatformInput = {
15
+ id: string | number;
16
+ };
17
+ export type GamesDbSearchInput = {
18
+ query: string;
19
+ page?: number;
20
+ };
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/gamesdb/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE9F,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { SourceRecord } from "../../source.js";
2
+ import type { ZuuidData } from "../../entity.js";
3
+ import { type FetchImdbTitleInput } from "./movie.js";
4
+ import type { ImdbFetchLike, ImdbProviderOptions, ImdbTransformOptions } from "./types.js";
5
+ export declare class ImdbProvider {
6
+ readonly fetcher: ImdbFetchLike;
7
+ readonly titleBaseUrl: string | null;
8
+ readonly userAgent: string;
9
+ constructor(options?: ImdbProviderOptions);
10
+ transformOptions(): ImdbTransformOptions;
11
+ fetchMovieSourceRecord(input: FetchImdbTitleInput): Promise<SourceRecord | undefined>;
12
+ fetchMovie(input: FetchImdbTitleInput): Promise<ZuuidData | undefined>;
13
+ fetchTvSourceRecord(input: FetchImdbTitleInput): Promise<SourceRecord | undefined>;
14
+ fetchTv(input: FetchImdbTitleInput): Promise<ZuuidData | undefined>;
15
+ }
16
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/imdb/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAA4F,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAChJ,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE3F,qBAAa,YAAY;IACvB,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,OAAO,GAAE,mBAAwB;IAM7C,gBAAgB,IAAI,oBAAoB;IAIlC,sBAAsB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAIrF,UAAU,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAKtE,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC;IAIlF,OAAO,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;CAI1E"}
@@ -0,0 +1,29 @@
1
+ import { IMDB_DEFAULT_USER_AGENT, IMDB_TITLE_BASE_URL } from "./constants.js";
2
+ import { fetchImdbMovieSourceRecord, fetchImdbTvSourceRecord, transformImdbMovie, transformImdbTv } from "./movie.js";
3
+ export class ImdbProvider {
4
+ fetcher;
5
+ titleBaseUrl;
6
+ userAgent;
7
+ constructor(options = {}) {
8
+ this.fetcher = options.fetch ?? fetch;
9
+ this.titleBaseUrl = options.titleBaseUrl === undefined ? IMDB_TITLE_BASE_URL : options.titleBaseUrl;
10
+ this.userAgent = options.userAgent ?? IMDB_DEFAULT_USER_AGENT;
11
+ }
12
+ transformOptions() {
13
+ return { titleBaseUrl: this.titleBaseUrl };
14
+ }
15
+ async fetchMovieSourceRecord(input) {
16
+ return fetchImdbMovieSourceRecord(this, input);
17
+ }
18
+ async fetchMovie(input) {
19
+ const source = await this.fetchMovieSourceRecord(input);
20
+ return source ? transformImdbMovie(source, this.transformOptions()) : undefined;
21
+ }
22
+ async fetchTvSourceRecord(input) {
23
+ return fetchImdbTvSourceRecord(this, input);
24
+ }
25
+ async fetchTv(input) {
26
+ const source = await this.fetchTvSourceRecord(input);
27
+ return source ? transformImdbTv(source, this.transformOptions()) : undefined;
28
+ }
29
+ }
@@ -0,0 +1,4 @@
1
+ export declare const IMDB_PROVIDER = "imdb";
2
+ export declare const IMDB_TITLE_BASE_URL = "https://www.imdb.com/title";
3
+ export declare const IMDB_DEFAULT_USER_AGENT = "@zivue/zuuid IMDb scraper (+https://github.com/zivue/zuuid)";
4
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/providers/imdb/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,SAAS,CAAC;AACpC,eAAO,MAAM,mBAAmB,+BAA+B,CAAC;AAChE,eAAO,MAAM,uBAAuB,gEAAgE,CAAC"}
@@ -0,0 +1,3 @@
1
+ export const IMDB_PROVIDER = "imdb";
2
+ export const IMDB_TITLE_BASE_URL = "https://www.imdb.com/title";
3
+ export const IMDB_DEFAULT_USER_AGENT = "@zivue/zuuid IMDb scraper (+https://github.com/zivue/zuuid)";
@@ -0,0 +1,4 @@
1
+ export * from "./client.js";
2
+ export * from "./movie.js";
3
+ export * from "./types.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/imdb/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./client.js";
2
+ export * from "./movie.js";
3
+ export * from "./types.js";
@@ -0,0 +1,16 @@
1
+ import type { SearchResponse, ZuuidData } from "../../entity.js";
2
+ import { type SourceRecord } from "../../source.js";
3
+ import type { ImdbProvider } from "./client.js";
4
+ import type { ImdbTransformOptions } from "./types.js";
5
+ export type FetchImdbTitleInput = {
6
+ id: string | number;
7
+ };
8
+ export type ImdbSearchInput = {
9
+ query: string;
10
+ };
11
+ export declare function fetchImdbMovieSourceRecord(provider: ImdbProvider, input: FetchImdbTitleInput): Promise<SourceRecord | undefined>;
12
+ export declare function fetchImdbTvSourceRecord(provider: ImdbProvider, input: FetchImdbTitleInput): Promise<SourceRecord | undefined>;
13
+ export declare function transformImdbMovie(source: SourceRecord, options?: ImdbTransformOptions): Promise<ZuuidData>;
14
+ export declare function transformImdbTv(source: SourceRecord, options?: ImdbTransformOptions): Promise<ZuuidData>;
15
+ export declare function searchImdbMovies(): Promise<SearchResponse<never>>;
16
+ //# sourceMappingURL=movie.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"movie.d.ts","sourceRoot":"","sources":["../../../src/providers/imdb/movie.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAkBxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAsB,0BAA0B,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAEtI;AAED,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAEnI;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,SAAS,CAAC,CAErH;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,SAAS,CAAC,CAElH;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAEvE"}