openLibry Scraper
JavaScriptPublicby zx-apiAug 23, 2026, 05:18 AMExpires: Never136 views
1// OpenLibraryScraper.js
2
3class OpenLibraryScraper {
4 constructor(options = {}) {
5 this.baseURL = options.baseURL || 'https://openlibrary.org';
6 this.coverURL = options.coverURL || 'https://covers.openlibrary.org';
7 this.archiveURL = options.archiveURL || 'https://archive.org';
8 this.userAgent = options.userAgent || 'OpenLibraryScraper/1.0 megawatihamil@gmail.com';
9 this.baseDelay = options.baseDelay || 1000;
10 this.maxRetries = options.maxRetries || 3;
11 this.timeout = options.timeout || 15000;
12 this.lastRequestTime = 0;
13 }
14
15 _sleep(ms) {
16 return new Promise(resolve => setTimeout(resolve, ms));
17 }
18
19 async _request(path, options = {}) {
20 const now = Date.now();
21 const elapsed = now - this.lastRequestTime;
22 if (elapsed < this.baseDelay) {
23 await this._sleep(this.baseDelay - elapsed);
24 }
25
26 const url = path.startsWith('http') ? path : `${this.baseURL}${path}`;
27
28 for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
29 try {
30 const controller = new AbortController();
31 const timeoutId = setTimeout(() => controller.abort(), this.timeout);
32
33 const response = await fetch(url, {
34 ...options,
35 headers: {
36 'User-Agent': this.userAgent,
37 'Accept': 'application/json',
38 ...options.headers,
39 },
40 signal: controller.signal,
41 });
42
43 clearTimeout(timeoutId);
44 this.lastRequestTime = Date.now();
45
46 if (response.status === 429 || response.status >= 500) {
47 if (attempt === this.maxRetries) {
48 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
49 }
50 const backoff = this.baseDelay * Math.pow(2, attempt);
51 console.warn(`โณ Retry ${attempt}/${this.maxRetries} (HTTP ${response.status}) - tunggu ${backoff}ms`);
52 await this._sleep(backoff);
53 continue;
54 }
55
56 if (!response.ok) {
57 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
58 }
59
60 const text = await response.text();
61 try {
62 return JSON.parse(text);
63 } catch {
64 throw new Error(`Response bukan JSON valid: ${text.substring(0, 100)}`);
65 }
66
67 } catch (error) {
68 if (attempt === this.maxRetries) throw error;
69 const backoff = this.baseDelay * Math.pow(2, attempt);
70 console.warn(`โณ Retry ${attempt}/${this.maxRetries}: ${error.message}`);
71 await this._sleep(backoff);
72 }
73 }
74 }
75
76 _normalizeBook(doc) {
77 if (!doc) return null;
78
79 // --- Extract Authors ---
80 let authors = [];
81 let authorKeys = [];
82
83 if (Array.isArray(doc.author_name)) {
84 authors = doc.author_name;
85 authorKeys = (doc.author_key || []).map(k =>
86 k.startsWith('/authors/') ? k.split('/').pop() : k
87 );
88 } else if (Array.isArray(doc.authors)) {
89 authors = doc.authors.map(a => {
90 if (a.name) return a.name;
91 if (a.author?.name) return a.author.name;
92 return null;
93 }).filter(Boolean);
94
95 authorKeys = doc.authors.map(a => {
96 const rawKey = a.key || a.author?.key;
97 if (!rawKey) return null;
98 return rawKey.startsWith('/authors/') ? rawKey.split('/').pop() : rawKey;
99 }).filter(Boolean);
100 }
101
102 let coverId = doc.cover_i || doc.cover_id || null;
103 if (!coverId && Array.isArray(doc.covers) && doc.covers.length > 0) {
104 coverId = doc.covers[0];
105 }
106 if (!coverId && Array.isArray(doc.photos) && doc.photos.length > 0) {
107 coverId = doc.photos[0];
108 }
109
110 const isbn10 = doc.isbn_10 || [];
111 const isbn13 = doc.isbn_13 || [];
112
113 let description = doc.description;
114 if (typeof description === 'object' && description !== null) {
115 description = description.value;
116 }
117
118 let olid = null;
119 if (doc.key) {
120 olid = doc.key.split('/').pop();
121 }
122
123
124 let readUrl = null;
125 if (doc.key) {
126 readUrl = `${this.baseURL}${doc.key}`;
127 }
128
129 let borrowUrl = null;
130 let canBorrow = false;
131
132 // Identifier Internet Archive (ia / ocaid)
133 const iaIdentifier = doc.ia?.[0] || doc.ocaid || doc.identifier?.ia?.[0] || null;
134 if (iaIdentifier) {
135 borrowUrl = `${this.archiveURL}/details/${iaIdentifier}`;
136 canBorrow = true;
137 }
138
139 if (doc.availability) {
140 canBorrow = doc.availability.is_readable || doc.availability.is_browseable || false;
141 if (doc.availability.identifier && !borrowUrl) {
142 borrowUrl = `${this.archiveURL}/details/${doc.availability.identifier}`;
143 }
144 }
145
146 let previewUrl = null;
147 if (olid && doc.key?.startsWith('/books/')) {
148 previewUrl = `${this.baseURL}/books/${olid}/-/preview`;
149 }
150
151 let borrowDirectUrl = null;
152 if (canBorrow && iaIdentifier) {
153 borrowDirectUrl = `${this.baseURL}/borrow/ia/${iaIdentifier}`;
154 }
155
156 return {
157 key: doc.key,
158 olid,
159 title: doc.title,
160 subtitle: doc.subtitle || null,
161 authors,
162 authorKeys,
163 firstPublishYear: doc.first_publish_year || null,
164 publishDate: doc.publish_date || null,
165 publishers: doc.publishers || doc.publish || [],
166 languages: (doc.languages || []).map(l =>
167 typeof l === 'string' ? l : (l.key || '').split('/').pop()
168 ),
169 subjects: doc.subjects || [],
170 isbn10,
171 isbn13,
172 isbn: isbn13[0] || isbn10[0] || null,
173 pages: doc.number_of_pages || null,
174 coverId,
175 coverUrl: coverId ? `${this.coverURL}/b/id/${coverId}-L.jpg` : null,
176 description,
177 editionsCount: doc.edition_count || null,
178 readUrl,
179 borrowUrl,
180 borrowDirectUrl,
181 previewUrl,
182 canBorrow,
183 iaIdentifier,
184 raw: doc,
185 };
186 }
187
188 /**
189 * Resolve author name dari author key
190 * @private
191 */
192 async _resolveAuthorNames(authorKeys) {
193 if (!authorKeys || authorKeys.length === 0) return [];
194
195 const names = [];
196 for (const key of authorKeys) {
197 try {
198 const data = await this._request(`/authors/${key}.json`);
199 names.push(data.name || 'Unknown');
200 } catch {
201 names.push('Unknown');
202 }
203 }
204 return names;
205 }
206
207 async search(query, options = {}) {
208 const params = new URLSearchParams();
209 params.set('q', query);
210 params.set('page', String(options.page || 1));
211 params.set('limit', String(options.limit || 25));
212
213 if (options.fields) params.set('fields', options.fields);
214 if (options.author) params.set('author', options.author);
215 if (options.title) params.set('title', options.title);
216 if (options.subject) params.set('subject', options.subject);
217 if (options.language) params.set('language', options.language);
218 if (options.sort) params.set('sort', options.sort);
219
220 const data = await this._request(`/search.json?${params.toString()}`);
221
222 return {
223 totalResults: data.numFound || 0,
224 start: data.start,
225 page: options.page || 1,
226 books: (data.docs || []).map(doc => this._normalizeBook(doc)),
227 };
228 }
229
230 async searchAuthor(query, options = {}) {
231 const params = new URLSearchParams({ q: query });
232 params.set('page', String(options.page || 1));
233 params.set('limit', String(options.limit || 25));
234
235 const data = await this._request(`/search/authors.json?${params.toString()}`);
236
237 return {
238 totalResults: data.numFound || 0,
239 authors: (data.docs || []).map(a => ({
240 key: a.key,
241 name: a.name,
242 birthDate: a.birth_date || null,
243 deathDate: a.death_date || null,
244 topWork: a.top_work || null,
245 workCount: a.work_count || 0,
246 })),
247 };
248 }
249
250 async getByISBN(isbn) {
251 const clean = isbn.replace(/[-\s]/g, '');
252 const data = await this._request(`/isbn/${clean}.json`);
253 const book = this._normalizeBook(data);
254
255 if (book && book.authors.length === 0 && book.authorKeys.length > 0) {
256 book.authors = await this._resolveAuthorNames(book.authorKeys);
257 }
258
259 return book;
260 }
261
262 async getByOLID(olid) {
263 const id = olid.startsWith('OL') ? olid : `OL${olid}`;
264 const data = await this._request(`/books/${id}.json`);
265 const book = this._normalizeBook(data);
266
267 if (book && book.authors.length === 0 && book.authorKeys.length > 0) {
268 book.authors = await this._resolveAuthorNames(book.authorKeys);
269 }
270
271 return book;
272 }
273
274 async getByOCLC(oclc) {
275 const data = await this._request(`/books/oclc/${oclc}.json`);
276 return this._normalizeBook(data);
277 }
278
279 async getWork(workId) {
280 const id = workId.startsWith('OL')
281 ? (workId.endsWith('W') ? workId : `${workId}W`)
282 : `OL${workId}W`;
283 const data = await this._request(`/works/${id}.json`);
284 const book = this._normalizeBook(data);
285
286 if (book && book.authors.length === 0 && book.authorKeys.length > 0) {
287 book.authors = await this._resolveAuthorNames(book.authorKeys);
288 }
289
290 return book;
291 }
292
293 async getEditions(workId, options = {}) {
294 const id = workId.startsWith('OL')
295 ? (workId.endsWith('W') ? workId : `${workId}W`)
296 : `OL${workId}W`;
297 const params = new URLSearchParams({
298 limit: String(options.limit || 50),
299 });
300 const data = await this._request(`/works/${id}/editions.json?${params.toString()}`);
301 return {
302 total: data.size || 0,
303 editions: (data.entries || []).map(e => this._normalizeBook(e)),
304 };
305 }
306
307 async getAuthor(authorId) {
308 const id = authorId.startsWith('OL')
309 ? (authorId.endsWith('A') ? authorId : `${authorId}A`)
310 : `OL${authorId}A`;
311 const data = await this._request(`/authors/${id}.json`);
312
313 let bio = data.bio;
314 if (typeof bio === 'object' && bio !== null) {
315 bio = bio.value;
316 }
317
318 return {
319 key: data.key,
320 olid: data.key?.split('/').pop(),
321 name: data.name,
322 birthDate: data.birth_date || null,
323 deathDate: data.death_date || null,
324 bio,
325 wikipedia: data.wikipedia || null,
326 photos: data.photos || [],
327 links: data.links || [],
328 profileUrl: `${this.baseURL}${data.key}`,
329 };
330 }
331
332 async getAuthorWorks(authorId, options = {}) {
333 const id = authorId.startsWith('OL')
334 ? (authorId.endsWith('A') ? authorId : `${authorId}A`)
335 : `OL${authorId}A`;
336 const params = new URLSearchParams({
337 limit: String(options.limit || 50),
338 offset: String(options.offset || 0),
339 });
340 const data = await this._request(`/authors/${id}/works.json?${params.toString()}`);
341
342 const works = (data.entries || []).map(w => {
343 const authors = (w.authors || []).map(a => {
344 if (a.name) return a.name;
345 if (a.author?.name) return a.author.name;
346 return null;
347 }).filter(Boolean);
348
349 const coverId = w.cover_id || null;
350
351 return {
352 key: w.key,
353 title: w.title,
354 authors,
355 firstPublishYear: w.first_publish_year || null,
356 coverId,
357 coverUrl: coverId ? `${this.coverURL}/b/id/${coverId}-L.jpg` : null,
358 editionCount: w.edition_count || 0,
359 readUrl: `${this.baseURL}${w.key}`,
360 };
361 });
362
363 return {
364 total: data.size || 0,
365 works,
366 };
367 }
368
369 async getSubject(subject, options = {}) {
370 const params = new URLSearchParams({
371 limit: String(options.limit || 25),
372 offset: String(options.offset || 0),
373 });
374 if (options.details) params.set('details', 'true');
375
376 const cleanSubject = subject.toLowerCase().replace(/\s+/g, '_');
377 const data = await this._request(`/subjects/${cleanSubject}.json?${params.toString()}`);
378
379 return {
380 name: data.name,
381 workCount: data.work_count || 0,
382 subjectUrl: `${this.baseURL}/subjects/${cleanSubject}`,
383 books: (data.works || []).map(w => ({
384 key: w.key,
385 title: w.title,
386 authors: (w.authors || []).map(a => a.name).filter(Boolean),
387 coverId: w.cover_id || null,
388 coverUrl: w.cover_id ? `${this.coverURL}/b/id/${w.cover_id}-L.jpg` : null,
389 firstPublishYear: w.first_publish_year || null,
390 editionCount: w.edition_count || 0,
391 readUrl: `${this.baseURL}${w.key}`,
392 })),
393 };
394 }
395
396 async getSubjectList(type = 'subject') {
397 const data = await this._request(`/subjects.json?limit=100`);
398 return data.subjects || [];
399 }
400
401 async getRecentAdditions(limit = 50) {
402 const data = await this._request(`/recentadditions.json?limit=${limit}`);
403 return {
404 total: data.numFound || 0,
405 books: (data.docs || []).map(doc => this._normalizeBook(doc)),
406 };
407 }
408
409 async getTrendingBooks(timeframe = 'daily', options = {}) {
410 const validTimeframes = ['daily', 'monthly', 'yearly', 'alltime'];
411 if (!validTimeframes.includes(timeframe)) timeframe = 'daily';
412
413 const data = await this._request(`/trending/${timeframe}.json`);
414 const works = Array.isArray(data) ? data : (data.works || data.docs || []);
415
416 let books = works.map(w => {
417 const coverId = w.cover_id || null;
418 return {
419 key: w.key,
420 title: w.title,
421 authors: (w.authors || []).map(a => a.name || a.author?.name).filter(Boolean),
422 coverId,
423 coverUrl: coverId ? `${this.coverURL}/b/id/${coverId}-L.jpg` : null,
424 editionCount: w.edition_count || 0,
425 readUrl: w.key ? `${this.baseURL}${w.key}` : null,
426 borrowUrl: null,
427 canBorrow: false,
428 };
429 });
430
431 if (options.fetchDetails) {
432 console.warn(`โ ๏ธ Fetching details untuk ${books.length} buku trending...`);
433 for (let i = 0; i < books.length; i++) {
434 const bookKey = books[i].key;
435 if (!bookKey) continue;
436
437 try {
438 const workId = bookKey.split('/').pop();
439 const detail = await this.getWork(workId);
440 if (detail) {
441 books[i].authors = detail.authors;
442 books[i].coverId = detail.coverId;
443 books[i].coverUrl = detail.coverUrl;
444 books[i].borrowUrl = detail.borrowUrl;
445 books[i].canBorrow = detail.canBorrow;
446 books[i].iaIdentifier = detail.iaIdentifier;
447 }
448 } catch (err) {
449 // Abaikan error per buku
450 }
451 }
452 }
453
454 return {
455 timeframe,
456 books,
457 };
458 }
459
460 getCoverUrl(id, type = 'id', size = 'L') {
461 const validSizes = ['S', 'M', 'L'];
462 const validTypes = ['isbn', 'olid', 'oclc', 'id'];
463 if (!validSizes.includes(size)) size = 'L';
464 if (!validTypes.includes(type)) type = 'id';
465 return `${this.coverURL}/b/${type}/${id}-${size}.jpg`;
466 }
467
468 /**
469 * Dapatkan SEMUA link baca buku dari berbagai sumber
470 * Termasuk: Internet Archive, Amazon, Perpustakaan, dll
471 * @param {string} identifier - OLID (OL22597282M) atau ISBN atau IA identifier
472 * @param {string} type - "olid" | "isbn" | "oclc" | "ia"
473 * @returns {Promise<Object>} Semua link baca yang tersedia
474 */
475 async getReadLinks(identifier, type = 'olid') {
476 const links = {
477 openLibrary: null,
478 internetArchive: null,
479 preview: null,
480 external: [],
481 };
482
483 try {
484 let book = null;
485
486 if (type === 'isbn') {
487 book = await this.getByISBN(identifier);
488 } else if (type === 'oclc') {
489 book = await this.getByOCLC(identifier);
490 } else if (type === 'olid') {
491 book = await this.getByOLID(identifier);
492 } else if (type === 'ia') {
493 // Langsung IA identifier
494 links.internetArchive = `${this.archiveURL}/details/${identifier}`;
495 links.openLibrary = `${this.baseURL}/ia:${identifier}`;
496 }
497
498 if (book) {
499 links.openLibrary = book.readUrl;
500 links.internetArchive = book.borrowUrl;
501 links.preview = book.previewUrl;
502
503 if (book.raw) {
504 // 1. Link dari field `links`
505 if (Array.isArray(book.raw.links)) {
506 book.raw.links.forEach(link => {
507 links.external.push({
508 title: link.title || 'External Link',
509 url: link.url,
510 });
511 });
512 }
513
514 if (book.raw.identifiers) {
515 const ids = book.raw.identifiers;
516 if (ids.goodreads?.length > 0) {
517 links.external.push({
518 title: 'Goodreads',
519 url: `https://www.goodreads.com/book/show/${ids.goodreads[0]}`,
520 });
521 }
522 if (ids.librarything?.length > 0) {
523 links.external.push({
524 title: 'LibraryThing',
525 url: `https://www.librarything.com/work/${ids.librarything[0]}`,
526 });
527 }
528 if (ids.wikidata?.length > 0) {
529 links.external.push({
530 title: 'Wikidata',
531 url: `https://www.wikidata.org/wiki/${ids.wikidata[0]}`,
532 });
533 }
534 if (ids.amazon?.length > 0) {
535 links.external.push({
536 title: 'Amazon',
537 url: `https://www.amazon.com/dp/${ids.amazon[0]}`,
538 });
539 }
540 }
541
542 if (book.isbn) {
543 links.external.push({
544 title: 'WorldCat (Perpustakaan Terdekat)',
545 url: `https://www.worldcat.org/isbn/${book.isbn}`,
546 });
547 links.external.push({
548 title: 'Google Books',
549 url: `https://books.google.com/books?vid=ISBN${book.isbn}`,
550 });
551 links.external.push({
552 title: 'Open Library (by ISBN)',
553 url: `${this.baseURL}/isbn/${book.isbn}`,
554 });
555 }
556 }
557 }
558
559 const seen = new Set();
560 links.external = links.external.filter(link => {
561 if (seen.has(link.url)) return false;
562 seen.add(link.url);
563 return true;
564 });
565
566 return links;
567 } catch (error) {
568 return {
569 error: error.message,
570 openLibrary: null,
571 internetArchive: null,
572 preview: null,
573 external: [],
574 };
575 }
576 }
577
578 async getBorrowableEditions(workId) {
579 const editions = await this.getEditions(workId, { limit: 100 });
580 return editions.editions.filter(ed => ed.canBorrow && ed.borrowUrl);
581 }
582
583 async advancedSearch(filters = {}) {
584 const params = new URLSearchParams();
585
586 if (filters.query) params.set('q', filters.query);
587 if (filters.title) params.set('title', filters.title);
588 if (filters.author) params.set('author', filters.author);
589 if (filters.subject) params.set('subject', filters.subject);
590 if (filters.publisher) params.set('publisher', filters.publisher);
591 if (filters.isbn) params.set('isbn', filters.isbn);
592
593 if (filters.language) params.set('language', filters.language);
594
595 if (filters.yearFrom) {
596 params.set('first_publish_year', `${filters.yearFrom}`);
597 }
598
599 params.set('page', String(filters.page || 1));
600 params.set('limit', String(filters.limit || 25));
601
602 if (filters.sort) params.set('sort', filters.sort);
603
604 const data = await this._request(`/search.json?${params.toString()}`);
605 let books = (data.docs || []).map(doc => this._normalizeBook(doc));
606
607 if (filters.yearFrom && filters.yearTo) {
608 books = books.filter(b =>
609 b.firstPublishYear !== null &&
610 b.firstPublishYear >= filters.yearFrom &&
611 b.firstPublishYear <= filters.yearTo
612 );
613 }
614
615 return {
616 totalResults: data.numFound || 0,
617 books,
618 };
619 }
620
621 async hasISBN(isbn) {
622 try {
623 const book = await this.getByISBN(isbn);
624 return !!book;
625 } catch {
626 return false;
627 }
628 }
629
630 async batchSearch(queries, options = {}) {
631 const results = {};
632 for (const query of queries) {
633 try {
634 results[query] = await this.search(query, { limit: options.limit || 5 });
635 } catch (err) {
636 results[query] = { error: err.message };
637 }
638 }
639 return results;
640 }
641
642 /**
643 * Cek apakah buku bisa dibaca gratis di Internet Archive
644 * @param {string} isbn atau OLID
645 * @param {string} type - "isbn" | "olid"
646 * @returns {Promise<Object>}
647 */
648 async canReadOnline(identifier, type = 'isbn') {
649 const book = type === 'isbn'
650 ? await this.getByISBN(identifier)
651 : await this.getByOLID(identifier);
652
653 if (!book) {
654 return { readable: false, reason: 'Book not found' };
655 }
656
657 if (book.canBorrow && book.borrowUrl) {
658 return {
659 readable: true,
660 title: book.title,
661 borrowUrl: book.borrowUrl,
662 previewUrl: book.previewUrl,
663 readUrl: book.readUrl,
664 iaIdentifier: book.iaIdentifier,
665 };
666 }
667
668 return {
669 readable: false,
670 title: book.title,
671 readUrl: book.readUrl,
672 reason: 'Not available for online reading',
673 };
674 }
675}
676
677
678if (typeof module !== 'undefined' && module.exports) {
679 module.exports = OpenLibraryScraper;
680}
681
682const scraper = new OpenLibraryScraper({
683 userAgent: 'MyBookApp/1.0 MegawatiHamil@gmail.com', // โ ๏ธ GANTI Email Atau Biarin Pake Gmail Ini
684 baseDelay: 1500,
685});
686
687async function OpenLibryRun() {
688 try {
689 console.log('\n๐ SEARCH DENGAN READ URL...');
690 const search = await scraper.search('Dune Frank Herbert', { limit: 3 });
691 search.books.forEach((book, i) => {
692 console.log(`\n${i + 1}. ${book.title} (${book.firstPublishYear})`);
693 console.log(` ๐ค Penulis: ${book.authors.join(', ')}`);
694 console.log(` ๐ Baca di OL: ${book.readUrl}`);
695 console.log(` ๐ฅ Borrow/Read Online: ${book.borrowUrl || 'Tidak tersedia'}`);
696 console.log(` โ
Bisa dibaca: ${book.canBorrow ? 'Ya' : 'Tidak'}`);
697 if (book.coverUrl) console.log(` ๐ผ๏ธ Cover: ${book.coverUrl}`);
698 });
699
700 console.log('\n๐ DETAIL BUKU (ISBN 9780441172719)...');
701 const book = await scraper.getByISBN('9780441172719');
702 if (book) {
703 console.log(`Judul: ${book.title}`);
704 console.log(`Penulis: ${book.authors.join(', ')}`);
705 console.log('\n๐ SEMUA URL BACA:');
706 console.log(` ๐ Halaman OL: ${book.readUrl}`);
707 console.log(` ๐ฅ Internet Archive: ${book.borrowUrl || 'Tidak ada'}`);
708 console.log(` ๐ฅ Borrow Direct: ${book.borrowDirectUrl || 'Tidak ada'}`);
709 console.log(` ๐๏ธ Preview: ${book.previewUrl || 'Tidak ada'}`);
710 console.log(` โ
Can Borrow: ${book.canBorrow}`);
711 console.log(` ๐ IA Identifier: ${book.iaIdentifier || 'Tidak ada'}`);
712 }
713
714 console.log('\n๐ ALL READ LINKS (ISBN 9780441172719)...');
715 const readLinks = await scraper.getReadLinks('9780441172719', 'isbn');
716 console.log('๐ Open Library:', readLinks.openLibrary);
717 console.log('๐ฅ Internet Archive:', readLinks.internetArchive);
718 console.log('๐๏ธ Preview:', readLinks.preview);
719 console.log('๐ External Links:');
720 readLinks.external.forEach(link => {
721 console.log(` โข ${link.title}: ${link.url}`);
722 });
723
724 console.log('\nโ
CEK KETERBACAAN...');
725 const readable = await scraper.canReadOnline('9780441172719', 'isbn');
726 console.log(`Readability: ${JSON.stringify(readable, null, 2)}`);
727
728 console.log('\n๐ EDISI YANG BISA DIPINJAM (Dune)...');
729 const borrowable = await scraper.getBorrowableEditions('OL893414W');
730 console.log(`Ditemukan ${borrowable.length} edisi yang bisa dibaca online`);
731 borrowable.slice(0, 5).forEach((ed, i) => {
732 console.log(`\n${i + 1}. ${ed.title} (${ed.publishDate})`);
733 console.log(` ๐ฅ Baca: ${ed.borrowUrl}`);
734 });
735
736 console.log('\n๐ BUKU KLASIK (biasanya bisa dibaca gratis)...');
737 const classics = await scraper.search('Pride and Prejudice Jane Austen', { limit: 1 });
738 if (classics.books[0]) {
739 const cls = classics.books[0];
740 console.log(`\n${cls.title}`);
741 console.log(`๐ Baca di OL: ${cls.readUrl}`);
742 console.log(`๐ฅ Baca di Archive: ${cls.borrowUrl || 'Tidak tersedia'}`);
743
744 const borrowEds = await scraper.getBorrowableEditions(cls.olid);
745 if (borrowEds.length > 0) {
746 console.log(`\nโ
${borrowEds.length} edisi tersedia untuk dibaca:`);
747 borrowEds.slice(0, 3).forEach(ed => {
748 console.log(` โข ${ed.title} (${ed.publishDate}) โ ${ed.borrowUrl}`);
749 });
750 }
751 }
752
753 } catch (error) {
754 console.error('โ Error:', error.message);
755 }
756}
757
758OpenLibryRun();758 linesยท27,983 charsยท27.4 KB
WwrapยทFfullscreen