@ashdev/codex-plugin-metadata-openlibrary 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/README.md +190 -0
- package/dist/index.js +1070 -0
- package/dist/index.js.map +7 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/api.ts
|
|
13
|
+
var api_exports = {};
|
|
14
|
+
__export(api_exports, {
|
|
15
|
+
buildOpenLibraryUrl: () => buildOpenLibraryUrl,
|
|
16
|
+
clearCache: () => clearCache,
|
|
17
|
+
extractOlid: () => extractOlid,
|
|
18
|
+
getAuthor: () => getAuthor,
|
|
19
|
+
getCoverUrlById: () => getCoverUrlById,
|
|
20
|
+
getCoverUrlByIsbn: () => getCoverUrlByIsbn,
|
|
21
|
+
getCoverUrlByOlid: () => getCoverUrlByOlid,
|
|
22
|
+
getEditionByIsbn: () => getEditionByIsbn,
|
|
23
|
+
getWork: () => getWork,
|
|
24
|
+
getWorkEditions: () => getWorkEditions,
|
|
25
|
+
isValidIsbn: () => isValidIsbn,
|
|
26
|
+
normalizeIsbn: () => normalizeIsbn,
|
|
27
|
+
parseDescription: () => parseDescription,
|
|
28
|
+
parseLanguage: () => parseLanguage,
|
|
29
|
+
parseYear: () => parseYear,
|
|
30
|
+
searchBooks: () => searchBooks
|
|
31
|
+
});
|
|
32
|
+
function getCached(key) {
|
|
33
|
+
const entry = cache.get(key);
|
|
34
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL_MS) {
|
|
35
|
+
return entry.data;
|
|
36
|
+
}
|
|
37
|
+
if (entry) {
|
|
38
|
+
cache.delete(key);
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function setCache(key, data) {
|
|
43
|
+
cache.set(key, { data, timestamp: Date.now() });
|
|
44
|
+
}
|
|
45
|
+
async function fetchJson(url, description) {
|
|
46
|
+
const cached = getCached(url);
|
|
47
|
+
if (cached !== null) {
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
headers: {
|
|
53
|
+
"User-Agent": "Codex/1.0 (https://github.com/AshDevFr/codex; codex-plugin)",
|
|
54
|
+
Accept: "application/json"
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
if (response.status === 404) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
62
|
+
}
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
setCache(url, data);
|
|
65
|
+
return data;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`[openlibrary] Failed to fetch ${description}:`, error);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function normalizeIsbn(isbn) {
|
|
72
|
+
return isbn.replace(/[-\s]/g, "").toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
function isValidIsbn(isbn) {
|
|
75
|
+
const normalized = normalizeIsbn(isbn);
|
|
76
|
+
return normalized.length === 10 || normalized.length === 13;
|
|
77
|
+
}
|
|
78
|
+
async function getEditionByIsbn(isbn) {
|
|
79
|
+
const normalized = normalizeIsbn(isbn);
|
|
80
|
+
const url = `${BASE_URL}/isbn/${normalized}.json`;
|
|
81
|
+
return fetchJson(url, `edition by ISBN ${normalized}`);
|
|
82
|
+
}
|
|
83
|
+
async function getWork(workKey) {
|
|
84
|
+
const key = workKey.startsWith("/works/") ? workKey : `/works/${workKey}`;
|
|
85
|
+
const url = `${BASE_URL}${key}.json`;
|
|
86
|
+
return fetchJson(url, `work ${key}`);
|
|
87
|
+
}
|
|
88
|
+
async function getWorkEditions(workKey, limit = 5) {
|
|
89
|
+
const key = workKey.startsWith("/works/") ? workKey : `/works/${workKey}`;
|
|
90
|
+
const url = `${BASE_URL}${key}/editions.json?limit=${limit}`;
|
|
91
|
+
const response = await fetchJson(url, `editions for ${key}`);
|
|
92
|
+
return response?.entries || [];
|
|
93
|
+
}
|
|
94
|
+
async function getAuthor(authorKey) {
|
|
95
|
+
const key = authorKey.startsWith("/authors/") ? authorKey : `/authors/${authorKey}`;
|
|
96
|
+
const url = `${BASE_URL}${key}.json`;
|
|
97
|
+
return fetchJson(url, `author ${key}`);
|
|
98
|
+
}
|
|
99
|
+
async function searchBooks(query, options = {}) {
|
|
100
|
+
const { author, limit = 10 } = options;
|
|
101
|
+
if (author) {
|
|
102
|
+
const params2 = new URLSearchParams({
|
|
103
|
+
title: query,
|
|
104
|
+
author,
|
|
105
|
+
fields: SEARCH_FIELDS,
|
|
106
|
+
limit: String(limit)
|
|
107
|
+
});
|
|
108
|
+
const url2 = `${BASE_URL}/search.json?${params2}`;
|
|
109
|
+
const response = await fetchJson(
|
|
110
|
+
url2,
|
|
111
|
+
`search title="${query}" author="${author}"`
|
|
112
|
+
);
|
|
113
|
+
if (response?.docs?.length) {
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const params = new URLSearchParams({
|
|
118
|
+
q: query,
|
|
119
|
+
fields: SEARCH_FIELDS,
|
|
120
|
+
limit: String(limit)
|
|
121
|
+
});
|
|
122
|
+
if (author) {
|
|
123
|
+
params.set("author", author);
|
|
124
|
+
}
|
|
125
|
+
const url = `${BASE_URL}/search.json?${params}`;
|
|
126
|
+
return fetchJson(url, `search "${query}"`);
|
|
127
|
+
}
|
|
128
|
+
function getCoverUrlByIsbn(isbn, size) {
|
|
129
|
+
const normalized = normalizeIsbn(isbn);
|
|
130
|
+
return `${COVERS_BASE_URL}/b/isbn/${normalized}-${size}.jpg`;
|
|
131
|
+
}
|
|
132
|
+
function getCoverUrlById(coverId, size) {
|
|
133
|
+
return `${COVERS_BASE_URL}/b/id/${coverId}-${size}.jpg`;
|
|
134
|
+
}
|
|
135
|
+
function getCoverUrlByOlid(olid, size) {
|
|
136
|
+
const id = olid.replace(/^\/(?:books|works)\//, "");
|
|
137
|
+
return `${COVERS_BASE_URL}/b/olid/${id}-${size}.jpg`;
|
|
138
|
+
}
|
|
139
|
+
function parseYear(dateStr) {
|
|
140
|
+
if (!dateStr) return void 0;
|
|
141
|
+
const match = dateStr.match(/(?:^|[^0-9])(1[89]\d{2}|20\d{2})(?:[^0-9]|$)/);
|
|
142
|
+
if (match) {
|
|
143
|
+
return Number.parseInt(match[1], 10);
|
|
144
|
+
}
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
function parseDescription(desc) {
|
|
148
|
+
if (!desc) return void 0;
|
|
149
|
+
const raw = typeof desc === "string" ? desc : desc.value;
|
|
150
|
+
return stripHtml(raw);
|
|
151
|
+
}
|
|
152
|
+
function stripHtml(html) {
|
|
153
|
+
let text = html;
|
|
154
|
+
text = text.replace(/<\/(p|div|li|tr|h[1-6])>/gi, "\n");
|
|
155
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
156
|
+
text = text.replace(/<[^>]+>/g, "");
|
|
157
|
+
text = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'").replace(/ /g, " ");
|
|
158
|
+
text = text.replace(/[^\S\n]+/g, " ");
|
|
159
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
160
|
+
text = text.split("\n").map((line) => line.trim()).join("\n").trim();
|
|
161
|
+
return text || void 0;
|
|
162
|
+
}
|
|
163
|
+
function parseLanguage(langRef) {
|
|
164
|
+
if (!langRef) return void 0;
|
|
165
|
+
const match = langRef.match(/\/languages\/(\w+)$/);
|
|
166
|
+
if (!match) return void 0;
|
|
167
|
+
const code = match[1].toLowerCase();
|
|
168
|
+
const languageMap = {
|
|
169
|
+
eng: "en",
|
|
170
|
+
spa: "es",
|
|
171
|
+
fre: "fr",
|
|
172
|
+
fra: "fr",
|
|
173
|
+
ger: "de",
|
|
174
|
+
deu: "de",
|
|
175
|
+
ita: "it",
|
|
176
|
+
por: "pt",
|
|
177
|
+
rus: "ru",
|
|
178
|
+
jpn: "ja",
|
|
179
|
+
chi: "zh",
|
|
180
|
+
zho: "zh",
|
|
181
|
+
kor: "ko",
|
|
182
|
+
ara: "ar",
|
|
183
|
+
hin: "hi",
|
|
184
|
+
pol: "pl",
|
|
185
|
+
tur: "tr",
|
|
186
|
+
dut: "nl",
|
|
187
|
+
nld: "nl",
|
|
188
|
+
swe: "sv",
|
|
189
|
+
nor: "no",
|
|
190
|
+
dan: "da",
|
|
191
|
+
fin: "fi",
|
|
192
|
+
cze: "cs",
|
|
193
|
+
ces: "cs",
|
|
194
|
+
gre: "el",
|
|
195
|
+
ell: "el",
|
|
196
|
+
heb: "he",
|
|
197
|
+
hun: "hu",
|
|
198
|
+
rom: "ro",
|
|
199
|
+
ron: "ro",
|
|
200
|
+
tha: "th",
|
|
201
|
+
vie: "vi",
|
|
202
|
+
ind: "id",
|
|
203
|
+
mal: "ms",
|
|
204
|
+
msa: "ms",
|
|
205
|
+
ukr: "uk",
|
|
206
|
+
cat: "ca",
|
|
207
|
+
lat: "la"
|
|
208
|
+
};
|
|
209
|
+
return languageMap[code] || code;
|
|
210
|
+
}
|
|
211
|
+
function extractOlid(key) {
|
|
212
|
+
return key.replace(/^\/(?:works|books|authors)\//, "");
|
|
213
|
+
}
|
|
214
|
+
function buildOpenLibraryUrl(key) {
|
|
215
|
+
return `${BASE_URL}${key.startsWith("/") ? key : `/${key}`}`;
|
|
216
|
+
}
|
|
217
|
+
function clearCache() {
|
|
218
|
+
cache.clear();
|
|
219
|
+
}
|
|
220
|
+
var BASE_URL, COVERS_BASE_URL, CACHE_TTL_MS, cache, SEARCH_FIELDS;
|
|
221
|
+
var init_api = __esm({
|
|
222
|
+
"src/api.ts"() {
|
|
223
|
+
"use strict";
|
|
224
|
+
BASE_URL = "https://openlibrary.org";
|
|
225
|
+
COVERS_BASE_URL = "https://covers.openlibrary.org";
|
|
226
|
+
CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
227
|
+
cache = /* @__PURE__ */ new Map();
|
|
228
|
+
SEARCH_FIELDS = [
|
|
229
|
+
"key",
|
|
230
|
+
"title",
|
|
231
|
+
"subtitle",
|
|
232
|
+
"author_name",
|
|
233
|
+
"author_key",
|
|
234
|
+
"first_publish_year",
|
|
235
|
+
"publish_year",
|
|
236
|
+
"publisher",
|
|
237
|
+
"isbn",
|
|
238
|
+
"number_of_pages_median",
|
|
239
|
+
"cover_i",
|
|
240
|
+
"cover_edition_key",
|
|
241
|
+
"edition_count",
|
|
242
|
+
"language",
|
|
243
|
+
"subject",
|
|
244
|
+
"ratings_average",
|
|
245
|
+
"ratings_count"
|
|
246
|
+
].join(",");
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ../sdk-typescript/dist/types/rpc.js
|
|
251
|
+
var JSON_RPC_ERROR_CODES = {
|
|
252
|
+
/** Invalid JSON was received */
|
|
253
|
+
PARSE_ERROR: -32700,
|
|
254
|
+
/** The JSON sent is not a valid Request object */
|
|
255
|
+
INVALID_REQUEST: -32600,
|
|
256
|
+
/** The method does not exist / is not available */
|
|
257
|
+
METHOD_NOT_FOUND: -32601,
|
|
258
|
+
/** Invalid method parameter(s) */
|
|
259
|
+
INVALID_PARAMS: -32602,
|
|
260
|
+
/** Internal JSON-RPC error */
|
|
261
|
+
INTERNAL_ERROR: -32603
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// ../sdk-typescript/dist/errors.js
|
|
265
|
+
var PluginError = class extends Error {
|
|
266
|
+
data;
|
|
267
|
+
constructor(message, data) {
|
|
268
|
+
super(message);
|
|
269
|
+
this.name = this.constructor.name;
|
|
270
|
+
this.data = data;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Convert to JSON-RPC error format
|
|
274
|
+
*/
|
|
275
|
+
toJsonRpcError() {
|
|
276
|
+
return {
|
|
277
|
+
code: this.code,
|
|
278
|
+
message: this.message,
|
|
279
|
+
data: this.data
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ../sdk-typescript/dist/logger.js
|
|
285
|
+
var LOG_LEVELS = {
|
|
286
|
+
debug: 0,
|
|
287
|
+
info: 1,
|
|
288
|
+
warn: 2,
|
|
289
|
+
error: 3
|
|
290
|
+
};
|
|
291
|
+
var Logger = class {
|
|
292
|
+
name;
|
|
293
|
+
minLevel;
|
|
294
|
+
timestamps;
|
|
295
|
+
constructor(options) {
|
|
296
|
+
this.name = options.name;
|
|
297
|
+
this.minLevel = LOG_LEVELS[options.level ?? "info"];
|
|
298
|
+
this.timestamps = options.timestamps ?? true;
|
|
299
|
+
}
|
|
300
|
+
shouldLog(level) {
|
|
301
|
+
return LOG_LEVELS[level] >= this.minLevel;
|
|
302
|
+
}
|
|
303
|
+
format(level, message, data) {
|
|
304
|
+
const parts = [];
|
|
305
|
+
if (this.timestamps) {
|
|
306
|
+
parts.push((/* @__PURE__ */ new Date()).toISOString());
|
|
307
|
+
}
|
|
308
|
+
parts.push(`[${level.toUpperCase()}]`);
|
|
309
|
+
parts.push(`[${this.name}]`);
|
|
310
|
+
parts.push(message);
|
|
311
|
+
if (data !== void 0) {
|
|
312
|
+
if (data instanceof Error) {
|
|
313
|
+
parts.push(`- ${data.message}`);
|
|
314
|
+
if (data.stack) {
|
|
315
|
+
parts.push(`
|
|
316
|
+
${data.stack}`);
|
|
317
|
+
}
|
|
318
|
+
} else if (typeof data === "object") {
|
|
319
|
+
parts.push(`- ${JSON.stringify(data)}`);
|
|
320
|
+
} else {
|
|
321
|
+
parts.push(`- ${String(data)}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return parts.join(" ");
|
|
325
|
+
}
|
|
326
|
+
log(level, message, data) {
|
|
327
|
+
if (this.shouldLog(level)) {
|
|
328
|
+
process.stderr.write(`${this.format(level, message, data)}
|
|
329
|
+
`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
debug(message, data) {
|
|
333
|
+
this.log("debug", message, data);
|
|
334
|
+
}
|
|
335
|
+
info(message, data) {
|
|
336
|
+
this.log("info", message, data);
|
|
337
|
+
}
|
|
338
|
+
warn(message, data) {
|
|
339
|
+
this.log("warn", message, data);
|
|
340
|
+
}
|
|
341
|
+
error(message, data) {
|
|
342
|
+
this.log("error", message, data);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function createLogger(options) {
|
|
346
|
+
return new Logger(options);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ../sdk-typescript/dist/server.js
|
|
350
|
+
import { createInterface } from "node:readline";
|
|
351
|
+
function validateStringFields(params, fields) {
|
|
352
|
+
if (params === null || params === void 0) {
|
|
353
|
+
return { field: "params", message: "params is required" };
|
|
354
|
+
}
|
|
355
|
+
if (typeof params !== "object") {
|
|
356
|
+
return { field: "params", message: "params must be an object" };
|
|
357
|
+
}
|
|
358
|
+
const obj = params;
|
|
359
|
+
for (const field of fields) {
|
|
360
|
+
const value = obj[field];
|
|
361
|
+
if (value === void 0 || value === null) {
|
|
362
|
+
return { field, message: `${field} is required` };
|
|
363
|
+
}
|
|
364
|
+
if (typeof value !== "string") {
|
|
365
|
+
return { field, message: `${field} must be a string` };
|
|
366
|
+
}
|
|
367
|
+
if (value.trim() === "") {
|
|
368
|
+
return { field, message: `${field} cannot be empty` };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function validateSearchParams(params) {
|
|
374
|
+
return validateStringFields(params, ["query"]);
|
|
375
|
+
}
|
|
376
|
+
function validateGetParams(params) {
|
|
377
|
+
return validateStringFields(params, ["externalId"]);
|
|
378
|
+
}
|
|
379
|
+
function validateMatchParams(params) {
|
|
380
|
+
return validateStringFields(params, ["title"]);
|
|
381
|
+
}
|
|
382
|
+
function invalidParamsError(id, error) {
|
|
383
|
+
return {
|
|
384
|
+
jsonrpc: "2.0",
|
|
385
|
+
id,
|
|
386
|
+
error: {
|
|
387
|
+
code: JSON_RPC_ERROR_CODES.INVALID_PARAMS,
|
|
388
|
+
message: `Invalid params: ${error.message}`,
|
|
389
|
+
data: { field: error.field }
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function createMetadataPlugin(options) {
|
|
394
|
+
const { manifest: manifest2, provider, onInitialize, logLevel = "info" } = options;
|
|
395
|
+
const logger2 = createLogger({ name: manifest2.name, level: logLevel });
|
|
396
|
+
logger2.info(`Starting plugin: ${manifest2.displayName} v${manifest2.version}`);
|
|
397
|
+
const rl = createInterface({
|
|
398
|
+
input: process.stdin,
|
|
399
|
+
terminal: false
|
|
400
|
+
});
|
|
401
|
+
rl.on("line", (line) => {
|
|
402
|
+
void handleLine(line, manifest2, provider, onInitialize, logger2);
|
|
403
|
+
});
|
|
404
|
+
rl.on("close", () => {
|
|
405
|
+
logger2.info("stdin closed, shutting down");
|
|
406
|
+
process.exit(0);
|
|
407
|
+
});
|
|
408
|
+
process.on("uncaughtException", (error) => {
|
|
409
|
+
logger2.error("Uncaught exception", error);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
});
|
|
412
|
+
process.on("unhandledRejection", (reason) => {
|
|
413
|
+
logger2.error("Unhandled rejection", reason);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async function handleLine(line, manifest2, provider, onInitialize, logger2) {
|
|
417
|
+
const trimmed = line.trim();
|
|
418
|
+
if (!trimmed)
|
|
419
|
+
return;
|
|
420
|
+
let id = null;
|
|
421
|
+
try {
|
|
422
|
+
const request = JSON.parse(trimmed);
|
|
423
|
+
id = request.id;
|
|
424
|
+
logger2.debug(`Received request: ${request.method}`, { id: request.id });
|
|
425
|
+
const response = await handleRequest(request, manifest2, provider, onInitialize, logger2);
|
|
426
|
+
if (response !== null) {
|
|
427
|
+
writeResponse(response);
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (error instanceof SyntaxError) {
|
|
431
|
+
writeResponse({
|
|
432
|
+
jsonrpc: "2.0",
|
|
433
|
+
id: null,
|
|
434
|
+
error: {
|
|
435
|
+
code: JSON_RPC_ERROR_CODES.PARSE_ERROR,
|
|
436
|
+
message: "Parse error: invalid JSON"
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
} else if (error instanceof PluginError) {
|
|
440
|
+
writeResponse({
|
|
441
|
+
jsonrpc: "2.0",
|
|
442
|
+
id,
|
|
443
|
+
error: error.toJsonRpcError()
|
|
444
|
+
});
|
|
445
|
+
} else {
|
|
446
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
447
|
+
logger2.error("Request failed", error);
|
|
448
|
+
writeResponse({
|
|
449
|
+
jsonrpc: "2.0",
|
|
450
|
+
id,
|
|
451
|
+
error: {
|
|
452
|
+
code: JSON_RPC_ERROR_CODES.INTERNAL_ERROR,
|
|
453
|
+
message
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async function handleRequest(request, manifest2, provider, onInitialize, logger2) {
|
|
460
|
+
const { method, params, id } = request;
|
|
461
|
+
switch (method) {
|
|
462
|
+
case "initialize":
|
|
463
|
+
if (onInitialize) {
|
|
464
|
+
await onInitialize(params);
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
jsonrpc: "2.0",
|
|
468
|
+
id,
|
|
469
|
+
result: manifest2
|
|
470
|
+
};
|
|
471
|
+
case "ping":
|
|
472
|
+
return {
|
|
473
|
+
jsonrpc: "2.0",
|
|
474
|
+
id,
|
|
475
|
+
result: "pong"
|
|
476
|
+
};
|
|
477
|
+
case "shutdown": {
|
|
478
|
+
logger2.info("Shutdown requested");
|
|
479
|
+
const response = {
|
|
480
|
+
jsonrpc: "2.0",
|
|
481
|
+
id,
|
|
482
|
+
result: null
|
|
483
|
+
};
|
|
484
|
+
process.stdout.write(`${JSON.stringify(response)}
|
|
485
|
+
`, () => {
|
|
486
|
+
process.exit(0);
|
|
487
|
+
});
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
// Series metadata methods (scoped by content type)
|
|
491
|
+
case "metadata/series/search": {
|
|
492
|
+
const validationError = validateSearchParams(params);
|
|
493
|
+
if (validationError) {
|
|
494
|
+
return invalidParamsError(id, validationError);
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
jsonrpc: "2.0",
|
|
498
|
+
id,
|
|
499
|
+
result: await provider.search(params)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
case "metadata/series/get": {
|
|
503
|
+
const validationError = validateGetParams(params);
|
|
504
|
+
if (validationError) {
|
|
505
|
+
return invalidParamsError(id, validationError);
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
jsonrpc: "2.0",
|
|
509
|
+
id,
|
|
510
|
+
result: await provider.get(params)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case "metadata/series/match": {
|
|
514
|
+
if (!provider.match) {
|
|
515
|
+
return {
|
|
516
|
+
jsonrpc: "2.0",
|
|
517
|
+
id,
|
|
518
|
+
error: {
|
|
519
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
520
|
+
message: "This plugin does not support match"
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const validationError = validateMatchParams(params);
|
|
525
|
+
if (validationError) {
|
|
526
|
+
return invalidParamsError(id, validationError);
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
jsonrpc: "2.0",
|
|
530
|
+
id,
|
|
531
|
+
result: await provider.match(params)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// Future: book metadata methods
|
|
535
|
+
// case "metadata/book/search":
|
|
536
|
+
// case "metadata/book/get":
|
|
537
|
+
// case "metadata/book/match":
|
|
538
|
+
default:
|
|
539
|
+
return {
|
|
540
|
+
jsonrpc: "2.0",
|
|
541
|
+
id,
|
|
542
|
+
error: {
|
|
543
|
+
code: JSON_RPC_ERROR_CODES.METHOD_NOT_FOUND,
|
|
544
|
+
message: `Method not found: ${method}`
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function writeResponse(response) {
|
|
550
|
+
process.stdout.write(`${JSON.stringify(response)}
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/index.ts
|
|
555
|
+
init_api();
|
|
556
|
+
|
|
557
|
+
// package.json
|
|
558
|
+
var package_default = {
|
|
559
|
+
name: "@ashdev/codex-plugin-metadata-openlibrary",
|
|
560
|
+
version: "1.0.0",
|
|
561
|
+
description: "Open Library metadata plugin for Codex - fetches book metadata by ISBN or title search",
|
|
562
|
+
main: "dist/index.js",
|
|
563
|
+
bin: "dist/index.js",
|
|
564
|
+
type: "module",
|
|
565
|
+
files: [
|
|
566
|
+
"dist",
|
|
567
|
+
"README.md"
|
|
568
|
+
],
|
|
569
|
+
repository: {
|
|
570
|
+
type: "git",
|
|
571
|
+
url: "https://github.com/AshDevFr/codex.git",
|
|
572
|
+
directory: "plugins/metadata-openlibrary"
|
|
573
|
+
},
|
|
574
|
+
scripts: {
|
|
575
|
+
build: "esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'",
|
|
576
|
+
dev: "npm run build -- --watch",
|
|
577
|
+
clean: "rm -rf dist",
|
|
578
|
+
start: "node dist/index.js",
|
|
579
|
+
lint: "biome check .",
|
|
580
|
+
"lint:fix": "biome check --write .",
|
|
581
|
+
typecheck: "tsc --noEmit",
|
|
582
|
+
test: "vitest run",
|
|
583
|
+
"test:watch": "vitest",
|
|
584
|
+
prepublishOnly: "npm run lint && npm run build"
|
|
585
|
+
},
|
|
586
|
+
keywords: [
|
|
587
|
+
"codex",
|
|
588
|
+
"plugin",
|
|
589
|
+
"openlibrary",
|
|
590
|
+
"metadata",
|
|
591
|
+
"books",
|
|
592
|
+
"isbn"
|
|
593
|
+
],
|
|
594
|
+
author: "Codex",
|
|
595
|
+
license: "MIT",
|
|
596
|
+
engines: {
|
|
597
|
+
node: ">=22.0.0"
|
|
598
|
+
},
|
|
599
|
+
dependencies: {
|
|
600
|
+
"@ashdev/codex-plugin-sdk": "file:../sdk-typescript"
|
|
601
|
+
},
|
|
602
|
+
devDependencies: {
|
|
603
|
+
"@biomejs/biome": "^2.3.13",
|
|
604
|
+
"@types/node": "^22.0.0",
|
|
605
|
+
esbuild: "^0.24.0",
|
|
606
|
+
typescript: "^5.7.0",
|
|
607
|
+
vitest: "^3.0.0"
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
// src/manifest.ts
|
|
612
|
+
var DEFAULT_MAX_RESULTS = 10;
|
|
613
|
+
var manifest = {
|
|
614
|
+
name: "metadata-openlibrary",
|
|
615
|
+
displayName: "Open Library",
|
|
616
|
+
version: package_default.version,
|
|
617
|
+
description: "Fetches book metadata from Open Library (openlibrary.org). Supports ISBN lookup and title search for EPUBs, PDFs, and other book formats.",
|
|
618
|
+
author: "Codex",
|
|
619
|
+
homepage: "https://openlibrary.org",
|
|
620
|
+
protocolVersion: "1.0",
|
|
621
|
+
capabilities: {
|
|
622
|
+
// Book metadata provider only (not series)
|
|
623
|
+
metadataProvider: ["book"]
|
|
624
|
+
},
|
|
625
|
+
configSchema: {
|
|
626
|
+
description: "Configuration options for the Open Library plugin",
|
|
627
|
+
fields: [
|
|
628
|
+
{
|
|
629
|
+
key: "maxResults",
|
|
630
|
+
label: "Maximum Results",
|
|
631
|
+
description: "Maximum number of results to return for search queries (1-50)",
|
|
632
|
+
type: "number",
|
|
633
|
+
required: false,
|
|
634
|
+
default: DEFAULT_MAX_RESULTS,
|
|
635
|
+
example: 20
|
|
636
|
+
}
|
|
637
|
+
]
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// src/mapper.ts
|
|
642
|
+
init_api();
|
|
643
|
+
function mapSearchDocToSearchResult(doc) {
|
|
644
|
+
const year = doc.first_publish_year;
|
|
645
|
+
const coverUrl = doc.cover_i ? getCoverUrlById(doc.cover_i, "M") : void 0;
|
|
646
|
+
let relevanceScore = 0.5;
|
|
647
|
+
if (doc.author_name?.length) relevanceScore += 0.1;
|
|
648
|
+
if (doc.isbn?.length) relevanceScore += 0.15;
|
|
649
|
+
if (doc.cover_i) relevanceScore += 0.1;
|
|
650
|
+
if (doc.first_publish_year) relevanceScore += 0.05;
|
|
651
|
+
if (doc.subject?.length) relevanceScore += 0.05;
|
|
652
|
+
if (doc.ratings_count && doc.ratings_count > 0) relevanceScore += 0.05;
|
|
653
|
+
return {
|
|
654
|
+
externalId: doc.key,
|
|
655
|
+
// Work key, e.g., "/works/OL45883W"
|
|
656
|
+
title: doc.title,
|
|
657
|
+
alternateTitles: doc.subtitle ? [doc.subtitle] : [],
|
|
658
|
+
year,
|
|
659
|
+
coverUrl,
|
|
660
|
+
relevanceScore: Math.min(1, relevanceScore),
|
|
661
|
+
preview: {
|
|
662
|
+
genres: doc.subject?.slice(0, 5) || [],
|
|
663
|
+
rating: doc.ratings_average ? Math.round(doc.ratings_average * 2) / 2 : void 0,
|
|
664
|
+
authors: doc.author_name?.slice(0, 3) || [],
|
|
665
|
+
description: doc.publisher?.length ? `Published by ${doc.publisher[0]}` : void 0
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async function resolveAuthors(authorRefs) {
|
|
670
|
+
if (!authorRefs?.length) return [];
|
|
671
|
+
const authors = [];
|
|
672
|
+
for (const ref of authorRefs) {
|
|
673
|
+
const key = ref.author?.key || ref.key;
|
|
674
|
+
if (!key) continue;
|
|
675
|
+
const authorData = await getAuthor(key);
|
|
676
|
+
if (authorData) {
|
|
677
|
+
authors.push({
|
|
678
|
+
name: authorData.name,
|
|
679
|
+
key,
|
|
680
|
+
sortName: authorData.personal_name || void 0
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return authors;
|
|
685
|
+
}
|
|
686
|
+
function mapToBookAuthors(authors) {
|
|
687
|
+
return authors.map((author) => ({
|
|
688
|
+
name: author.name,
|
|
689
|
+
role: "author",
|
|
690
|
+
sortName: author.sortName
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
function buildCoverUrls(isbn, coverId) {
|
|
694
|
+
const covers = [];
|
|
695
|
+
if (isbn) {
|
|
696
|
+
covers.push({
|
|
697
|
+
url: getCoverUrlByIsbn(isbn, "S"),
|
|
698
|
+
size: "small"
|
|
699
|
+
});
|
|
700
|
+
covers.push({
|
|
701
|
+
url: getCoverUrlByIsbn(isbn, "M"),
|
|
702
|
+
size: "medium"
|
|
703
|
+
});
|
|
704
|
+
covers.push({
|
|
705
|
+
url: getCoverUrlByIsbn(isbn, "L"),
|
|
706
|
+
size: "large"
|
|
707
|
+
});
|
|
708
|
+
} else if (coverId) {
|
|
709
|
+
covers.push({
|
|
710
|
+
url: getCoverUrlById(coverId, "S"),
|
|
711
|
+
size: "small"
|
|
712
|
+
});
|
|
713
|
+
covers.push({
|
|
714
|
+
url: getCoverUrlById(coverId, "M"),
|
|
715
|
+
size: "medium"
|
|
716
|
+
});
|
|
717
|
+
covers.push({
|
|
718
|
+
url: getCoverUrlById(coverId, "L"),
|
|
719
|
+
size: "large"
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return covers;
|
|
723
|
+
}
|
|
724
|
+
function buildExternalLinks(editionKey, workKey) {
|
|
725
|
+
const links = [
|
|
726
|
+
{
|
|
727
|
+
url: buildOpenLibraryUrl(editionKey),
|
|
728
|
+
label: "Open Library (Edition)",
|
|
729
|
+
linkType: "provider"
|
|
730
|
+
}
|
|
731
|
+
];
|
|
732
|
+
if (workKey) {
|
|
733
|
+
links.push({
|
|
734
|
+
url: buildOpenLibraryUrl(workKey),
|
|
735
|
+
label: "Open Library (Work)",
|
|
736
|
+
linkType: "provider"
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return links;
|
|
740
|
+
}
|
|
741
|
+
function collectIsbns(edition) {
|
|
742
|
+
const isbns = [];
|
|
743
|
+
if (edition.isbn_13?.length) {
|
|
744
|
+
isbns.push(...edition.isbn_13);
|
|
745
|
+
}
|
|
746
|
+
if (edition.isbn_10?.length) {
|
|
747
|
+
isbns.push(...edition.isbn_10);
|
|
748
|
+
}
|
|
749
|
+
return [...new Set(isbns)];
|
|
750
|
+
}
|
|
751
|
+
async function mapEditionToBookMetadata(edition, workData) {
|
|
752
|
+
const authorRefs = edition.authors || workData?.authors;
|
|
753
|
+
const authors = await resolveAuthors(authorRefs);
|
|
754
|
+
const isbns = collectIsbns(edition);
|
|
755
|
+
const primaryIsbn = isbns[0];
|
|
756
|
+
const coverId = edition.covers?.[0] || workData?.covers?.[0];
|
|
757
|
+
const description = parseDescription(edition.description) || parseDescription(workData?.description);
|
|
758
|
+
const subjects = [...edition.subjects || [], ...workData?.subjects || []];
|
|
759
|
+
const uniqueSubjects = [...new Set(subjects)];
|
|
760
|
+
const year = parseYear(edition.publish_date);
|
|
761
|
+
const originalYear = parseYear(workData?.first_publish_date);
|
|
762
|
+
const language = parseLanguage(edition.languages?.[0]?.key);
|
|
763
|
+
const externalRatings = [];
|
|
764
|
+
const workKey = edition.works?.[0]?.key || workData?.key;
|
|
765
|
+
const externalId = workKey || edition.key;
|
|
766
|
+
return {
|
|
767
|
+
externalId,
|
|
768
|
+
externalUrl: buildOpenLibraryUrl(externalId),
|
|
769
|
+
// Core fields
|
|
770
|
+
title: edition.title,
|
|
771
|
+
subtitle: edition.subtitle || workData?.subtitle,
|
|
772
|
+
alternateTitles: [],
|
|
773
|
+
summary: description,
|
|
774
|
+
bookType: detectBookType(edition),
|
|
775
|
+
// Book-specific fields
|
|
776
|
+
pageCount: edition.number_of_pages,
|
|
777
|
+
year,
|
|
778
|
+
// ISBN
|
|
779
|
+
isbn: primaryIsbn,
|
|
780
|
+
isbns,
|
|
781
|
+
// Edition info
|
|
782
|
+
edition: edition.edition_name,
|
|
783
|
+
originalTitle: workData?.title !== edition.title ? workData?.title : void 0,
|
|
784
|
+
originalYear,
|
|
785
|
+
language,
|
|
786
|
+
// Taxonomy
|
|
787
|
+
genres: [],
|
|
788
|
+
// Open Library doesn't have genres, just subjects
|
|
789
|
+
tags: [],
|
|
790
|
+
subjects: uniqueSubjects.slice(0, 20),
|
|
791
|
+
// Limit to 20 subjects
|
|
792
|
+
// Credits
|
|
793
|
+
authors: mapToBookAuthors(authors),
|
|
794
|
+
artists: [],
|
|
795
|
+
// Open Library doesn't track artists separately
|
|
796
|
+
publisher: edition.publishers?.[0],
|
|
797
|
+
// Media
|
|
798
|
+
coverUrl: primaryIsbn ? getCoverUrlByIsbn(primaryIsbn, "L") : coverId ? getCoverUrlById(coverId, "L") : void 0,
|
|
799
|
+
covers: buildCoverUrls(primaryIsbn, coverId),
|
|
800
|
+
// Rating
|
|
801
|
+
externalRatings,
|
|
802
|
+
awards: [],
|
|
803
|
+
// Links
|
|
804
|
+
externalLinks: buildExternalLinks(edition.key, workKey)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
function detectBookType(edition) {
|
|
808
|
+
const format = edition.physical_format?.toLowerCase();
|
|
809
|
+
if (format) {
|
|
810
|
+
if (format.includes("comic") || format.includes("graphic novel")) {
|
|
811
|
+
return "graphic_novel";
|
|
812
|
+
}
|
|
813
|
+
if (format.includes("manga")) {
|
|
814
|
+
return "manga";
|
|
815
|
+
}
|
|
816
|
+
if (format.includes("magazine") || format.includes("periodical")) {
|
|
817
|
+
return "magazine";
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const subjects = (edition.subjects || []).join(" ").toLowerCase();
|
|
821
|
+
if (subjects.includes("graphic novel") || subjects.includes("comics")) {
|
|
822
|
+
return "graphic_novel";
|
|
823
|
+
}
|
|
824
|
+
if (subjects.includes("manga")) {
|
|
825
|
+
return "manga";
|
|
826
|
+
}
|
|
827
|
+
return "novel";
|
|
828
|
+
}
|
|
829
|
+
async function getFullBookMetadata(editionOrWorkKey, isbn) {
|
|
830
|
+
if (isbn) {
|
|
831
|
+
const { getEditionByIsbn: getEditionByIsbn2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
832
|
+
const edition = await getEditionByIsbn2(isbn);
|
|
833
|
+
if (edition) {
|
|
834
|
+
const workKey = edition.works?.[0]?.key;
|
|
835
|
+
const workData = workKey ? await getWork(workKey) : null;
|
|
836
|
+
return mapEditionToBookMetadata(edition, workData);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (editionOrWorkKey.includes("/works/")) {
|
|
840
|
+
const workData = await getWork(editionOrWorkKey);
|
|
841
|
+
if (!workData) return null;
|
|
842
|
+
const editions = await getWorkEditions(editionOrWorkKey, 5);
|
|
843
|
+
if (editions.length > 0) {
|
|
844
|
+
const editionWithIsbn = editions.find((e) => e.isbn_13?.length || e.isbn_10?.length);
|
|
845
|
+
const edition = editionWithIsbn || editions[0];
|
|
846
|
+
return mapEditionToBookMetadata(edition, workData);
|
|
847
|
+
}
|
|
848
|
+
const authors = await resolveAuthors(workData.authors);
|
|
849
|
+
const coverId = workData.covers?.[0];
|
|
850
|
+
return {
|
|
851
|
+
externalId: workData.key,
|
|
852
|
+
externalUrl: buildOpenLibraryUrl(workData.key),
|
|
853
|
+
title: workData.title,
|
|
854
|
+
subtitle: workData.subtitle,
|
|
855
|
+
alternateTitles: [],
|
|
856
|
+
summary: parseDescription(workData.description),
|
|
857
|
+
isbns: [],
|
|
858
|
+
genres: [],
|
|
859
|
+
tags: [],
|
|
860
|
+
subjects: workData.subjects?.slice(0, 20) || [],
|
|
861
|
+
authors: mapToBookAuthors(authors),
|
|
862
|
+
artists: [],
|
|
863
|
+
coverUrl: coverId ? getCoverUrlById(coverId, "L") : void 0,
|
|
864
|
+
covers: coverId ? [
|
|
865
|
+
{ url: getCoverUrlById(coverId, "S"), size: "small" },
|
|
866
|
+
{ url: getCoverUrlById(coverId, "M"), size: "medium" },
|
|
867
|
+
{ url: getCoverUrlById(coverId, "L"), size: "large" }
|
|
868
|
+
] : [],
|
|
869
|
+
externalRatings: [],
|
|
870
|
+
awards: [],
|
|
871
|
+
externalLinks: [
|
|
872
|
+
{
|
|
873
|
+
url: buildOpenLibraryUrl(workData.key),
|
|
874
|
+
label: "Open Library",
|
|
875
|
+
linkType: "provider"
|
|
876
|
+
}
|
|
877
|
+
]
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
const url = `https://openlibrary.org${editionOrWorkKey}.json`;
|
|
881
|
+
try {
|
|
882
|
+
const response = await fetch(url, {
|
|
883
|
+
headers: {
|
|
884
|
+
"User-Agent": "Codex/1.0 (https://github.com/AshDevFr/codex; codex-plugin)",
|
|
885
|
+
Accept: "application/json"
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
if (response.ok) {
|
|
889
|
+
const edition = await response.json();
|
|
890
|
+
const workKey = edition.works?.[0]?.key;
|
|
891
|
+
const workData = workKey ? await getWork(workKey) : null;
|
|
892
|
+
return mapEditionToBookMetadata(edition, workData);
|
|
893
|
+
}
|
|
894
|
+
} catch {
|
|
895
|
+
}
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/index.ts
|
|
900
|
+
var logger = createLogger({ name: "openlibrary", level: "info" });
|
|
901
|
+
var config = {
|
|
902
|
+
maxResults: DEFAULT_MAX_RESULTS
|
|
903
|
+
};
|
|
904
|
+
var bookProvider = {
|
|
905
|
+
/**
|
|
906
|
+
* Search for books by ISBN or title/author query
|
|
907
|
+
*
|
|
908
|
+
* If ISBN is provided, it takes priority for direct lookup.
|
|
909
|
+
* Otherwise, falls back to title/author search.
|
|
910
|
+
*/
|
|
911
|
+
async search(params) {
|
|
912
|
+
const { isbn, query, author, limit } = params;
|
|
913
|
+
const maxResults = Math.min(limit || config.maxResults, 50);
|
|
914
|
+
if (isbn && isValidIsbn(isbn)) {
|
|
915
|
+
const edition = await getEditionByIsbn(isbn);
|
|
916
|
+
if (edition) {
|
|
917
|
+
const workKey = edition.works?.[0]?.key;
|
|
918
|
+
const workData = workKey ? await getWork(workKey) : null;
|
|
919
|
+
const metadata = await mapEditionToBookMetadata(edition, workData);
|
|
920
|
+
return {
|
|
921
|
+
results: [
|
|
922
|
+
{
|
|
923
|
+
externalId: metadata.externalId,
|
|
924
|
+
title: metadata.title || "Unknown",
|
|
925
|
+
alternateTitles: metadata.subtitle ? [metadata.subtitle] : [],
|
|
926
|
+
year: metadata.year,
|
|
927
|
+
coverUrl: metadata.coverUrl,
|
|
928
|
+
relevanceScore: 1,
|
|
929
|
+
// Perfect match by ISBN
|
|
930
|
+
preview: {
|
|
931
|
+
genres: metadata.subjects.slice(0, 5),
|
|
932
|
+
authors: metadata.authors.map((a) => a.name)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
]
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
if (!query) {
|
|
939
|
+
return { results: [] };
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (!query) {
|
|
943
|
+
return { results: [] };
|
|
944
|
+
}
|
|
945
|
+
const searchResponse = await searchBooks(query, {
|
|
946
|
+
author,
|
|
947
|
+
limit: maxResults
|
|
948
|
+
});
|
|
949
|
+
if (!searchResponse?.docs?.length) {
|
|
950
|
+
return { results: [] };
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
results: searchResponse.docs.map(mapSearchDocToSearchResult)
|
|
954
|
+
};
|
|
955
|
+
},
|
|
956
|
+
/**
|
|
957
|
+
* Get full book metadata by external ID
|
|
958
|
+
*
|
|
959
|
+
* The external ID can be:
|
|
960
|
+
* - A work key: "/works/OL45883W"
|
|
961
|
+
* - An edition key: "/books/OL7353617M"
|
|
962
|
+
*/
|
|
963
|
+
async get(params) {
|
|
964
|
+
const { externalId } = params;
|
|
965
|
+
const metadata = await getFullBookMetadata(externalId);
|
|
966
|
+
if (metadata) {
|
|
967
|
+
return metadata;
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
externalId,
|
|
971
|
+
externalUrl: `https://openlibrary.org${externalId.startsWith("/") ? externalId : `/${externalId}`}`,
|
|
972
|
+
alternateTitles: [],
|
|
973
|
+
isbns: [],
|
|
974
|
+
genres: [],
|
|
975
|
+
tags: [],
|
|
976
|
+
subjects: [],
|
|
977
|
+
authors: [],
|
|
978
|
+
artists: [],
|
|
979
|
+
covers: [],
|
|
980
|
+
externalRatings: [],
|
|
981
|
+
awards: [],
|
|
982
|
+
externalLinks: [
|
|
983
|
+
{
|
|
984
|
+
url: `https://openlibrary.org${externalId.startsWith("/") ? externalId : `/${externalId}`}`,
|
|
985
|
+
label: "Open Library",
|
|
986
|
+
linkType: "provider"
|
|
987
|
+
}
|
|
988
|
+
]
|
|
989
|
+
};
|
|
990
|
+
},
|
|
991
|
+
/**
|
|
992
|
+
* Auto-match a book using available identifiers
|
|
993
|
+
*
|
|
994
|
+
* Match priority:
|
|
995
|
+
* 1. ISBN (if provided) - highest confidence
|
|
996
|
+
* 2. Title + author search - lower confidence
|
|
997
|
+
*/
|
|
998
|
+
async match(params) {
|
|
999
|
+
const { title, authors, isbn, year } = params;
|
|
1000
|
+
if (isbn && isValidIsbn(isbn)) {
|
|
1001
|
+
const edition = await getEditionByIsbn(isbn);
|
|
1002
|
+
if (edition) {
|
|
1003
|
+
const workKey = edition.works?.[0]?.key;
|
|
1004
|
+
const workData = workKey ? await getWork(workKey) : null;
|
|
1005
|
+
const metadata = await mapEditionToBookMetadata(edition, workData);
|
|
1006
|
+
return {
|
|
1007
|
+
match: {
|
|
1008
|
+
externalId: metadata.externalId,
|
|
1009
|
+
title: metadata.title || "Unknown",
|
|
1010
|
+
alternateTitles: metadata.subtitle ? [metadata.subtitle] : [],
|
|
1011
|
+
year: metadata.year,
|
|
1012
|
+
coverUrl: metadata.coverUrl,
|
|
1013
|
+
relevanceScore: 1,
|
|
1014
|
+
preview: {
|
|
1015
|
+
genres: metadata.subjects.slice(0, 5),
|
|
1016
|
+
authors: metadata.authors.map((a) => a.name)
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
confidence: 0.99,
|
|
1020
|
+
// Very high confidence for ISBN match
|
|
1021
|
+
alternatives: []
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const searchQuery = authors?.length ? `${title} ${authors[0]}` : title;
|
|
1026
|
+
const searchResponse = await searchBooks(searchQuery, {
|
|
1027
|
+
limit: 5
|
|
1028
|
+
});
|
|
1029
|
+
if (!searchResponse?.docs?.length) {
|
|
1030
|
+
return {
|
|
1031
|
+
match: null,
|
|
1032
|
+
confidence: 0,
|
|
1033
|
+
alternatives: []
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const results = searchResponse.docs.map(mapSearchDocToSearchResult);
|
|
1037
|
+
const bestMatch = results[0];
|
|
1038
|
+
let confidence = bestMatch.relevanceScore || 0.5;
|
|
1039
|
+
const normalizedTitle = title.toLowerCase().trim();
|
|
1040
|
+
const normalizedMatchTitle = bestMatch.title.toLowerCase().trim();
|
|
1041
|
+
if (normalizedTitle === normalizedMatchTitle) {
|
|
1042
|
+
confidence = Math.min(1, confidence + 0.3);
|
|
1043
|
+
} else if (normalizedMatchTitle.includes(normalizedTitle) || normalizedTitle.includes(normalizedMatchTitle)) {
|
|
1044
|
+
confidence = Math.min(1, confidence + 0.15);
|
|
1045
|
+
}
|
|
1046
|
+
if (year && bestMatch.year === year) {
|
|
1047
|
+
confidence = Math.min(1, confidence + 0.1);
|
|
1048
|
+
}
|
|
1049
|
+
confidence = Math.min(confidence, 0.85);
|
|
1050
|
+
return {
|
|
1051
|
+
match: bestMatch,
|
|
1052
|
+
confidence,
|
|
1053
|
+
alternatives: results.slice(1)
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
createMetadataPlugin({
|
|
1058
|
+
manifest,
|
|
1059
|
+
bookProvider,
|
|
1060
|
+
logLevel: "info",
|
|
1061
|
+
onInitialize(params) {
|
|
1062
|
+
const maxResults = params.config?.maxResults;
|
|
1063
|
+
if (maxResults !== void 0) {
|
|
1064
|
+
config.maxResults = Math.min(Math.max(1, maxResults), 50);
|
|
1065
|
+
}
|
|
1066
|
+
logger.info(`Plugin initialized (maxResults: ${config.maxResults})`);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
logger.info("Open Library plugin started");
|
|
1070
|
+
//# sourceMappingURL=index.js.map
|