commit 1a01c5fabc856af58eeda13eab6ab799eb855daa Author: szbk Date: Sat Jan 18 21:26:13 2025 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..838db45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +reports/ +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +node_modules/ +public/src/thumbs +public/avatars/ +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +package-lock.json +.env +.env-mongo +.vscode/ +settings.json +Procfile +dump.rdb +error.log +output.log +.env.development +.env.production diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..bb8de46 --- /dev/null +++ b/.npmignore @@ -0,0 +1,32 @@ +# Jenkins ve test raporlarını dahil et +Jenkinsfile +mocha-report-config.json +reports/ + +# Geçici dosyalar ve günlükler +*.log +*.tmp + +# Node.js bağımlılıkları +node_modules/ + +# Derleme veya yapım çıktıları +dist/ +build/ + +# Test ve kapsama raporları +coverage/ + +# Yapılandırma dosyaları (sadece geliştirme için) +.env +.eslintrc.js +.babelrc + +# Projeye özel dosyalar +.secret +.vscode/ +.idea/ + +# Diğer geçici dosyalar +*.swp +*.bak diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..5d4dc56 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,75 @@ +pipeline { + agent any + environment { + BUILD_TIMESTAMP = new java.text.SimpleDateFormat('yyyy-MM-dd HH:mm:ss').format(new java.util.Date()) + } + + tools { + nodejs('22.13.0') + } + triggers { + cron('H */6 * * *') // Her 6 saatte bir çalıştır + } + stages { + stage('List directory - Before removing reports') { + steps { + sh ''' + echo "List directory - Before removing reports" + ls -la + ''' + } + } + stage('Remove reports') { + steps { + sh 'rm -rf reports' + } + } + stage('List directory - After removing reports') { + steps { + sh 'ls -la' + } + } + stage('Install Dependencies') { + steps { + sh 'npm install' + } + } + stage('Run Tests') { + steps { + sh 'npm test' + } + } + stage('Publish Test Results') { + steps { + junit 'reports/test-results.xml' + } + } + } + post { + always { + slackSend( + channel: '#jenkins', + tokenCredentialId: 'slack-token', + message: """ + \n 🧠 Goodreads Book Search Integration Test: + \n - Is the ISBN '9944824453' 🔥 + \n - Is the book title 'Dövmeli Adam' 🚀 + \n - Is the book's publication date '1 September 2008' ⏰ + \n - Is the page count '640' 📋 + \n Build Zamanı: ${env.BUILD_TIMESTAMP} + \n Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' tamamlandı. + \n Detaylar: ${env.BUILD_URL} + """, + color: currentBuild.result == 'SUCCESS' ? 'good' : 'danger' + ) + } + failure { + slackSend( + channel: '#jenkins', + tokenCredentialId: 'slack-token', + message: "Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' başarısız oldu. Detaylar: ${env.BUILD_URL}", + color: 'danger' + ) + } + } +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..8b3df4e --- /dev/null +++ b/Readme.md @@ -0,0 +1,88 @@ +# Unoffical Goodreads Book Search API + +This library allows you to retrieve book information from Goodreads using only the ISBN number, without needing an API key. The library analyzes Goodreads' HTML structure and returns book details in JSON format. It works asynchronously (based on Promises). + +## ✨ Features + +- 📚 Retrieves book information from Goodreads using the ISBN number. +- 🖼️ Returns detailed information such as book cover, title, author, genre, and publication date. +- 🕒 Customizable delay between requests. +- ✅ Tests integrated with Mocha and Chai. +- 🌐 Web data is fetched and parsed using Axios and Cheerio. + +## 🎯 Requirements + +- Node.js (v14 veya üzeri) +- NPM +- Internet connection +- Curiosity + +## 📦 Installation + +```bash +npm install wisecolt-goodreads-search-api +``` +## 🚀 Usage + +The following example shows how to use the API: + +```javascript +const GoodreadsBookSearch = require("wisecolt-goodreads-search-api"); + +(async () => { + try { + const BookSearch = new GoodreadsBookSearch(); + const bookDetails = await BookSearch.getBookDetails("9944824453"); + + console.log(bookDetails); + } catch (error) { + console.error("Hata:", error.message); + } +})(); + + +// Example Output: +{ + title: 'Dövmeli Adam', + thumbImage: 'https: //res.cloudinary.com/path/to/image.jpg', + authorName: { + author: { name: 'Peter V. Brett', profileLink: '/author/show/12345' + }, + translators: ['Çevirmen Adı 1', 'Çevirmen Adı 2' + ] + }, + description: 'Kitap açıklaması burada yer alır.', + page: '640', + isbn: '9944824453', + date: '01 Sept 2008', + rate: '4.3', + genres: ['Fantasy', 'Adventure', 'Action' + ] +} +``` + +## 📂 Project Structure +```javascript +wisecolt-goodreads-search-api +├── config +│ └── index.js # Konfigürasyon dosyası +├── lib +│ ├── index.js # GoodreadsBookSearch sınıfı +│ └── module.js # Veri işleme ve parse işlemleri +├── test +│ └── index.js # Entegrasyon testleri +├── index.js # Giriş noktası +├── package.json # Bağımlılıklar ve betikler +└── README.md # Dokümantasyon +``` + +## 🧪 Tests +To run the tests, follow these steps: +1. Install test dependencies: + ```javascript + npm install + ``` +2. Run the tests: + ```javascript + npm test + ``` \ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..58b6b8e --- /dev/null +++ b/config/index.js @@ -0,0 +1,13 @@ +require("dotenv").config(); + +const config = { + goodreadsBaseUrl: "https://www.goodreads.com/search?q=", + headers: { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.105 Safari/537.36", + }, + // Goodreads'e istek atarken, iki istek arasındaki zaman farkıdır + fetchTimeout: 2000, +}; + +module.exports = config; diff --git a/index.js b/index.js new file mode 100644 index 0000000..4cc88b3 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib'); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..601fb39 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,45 @@ +const modules = require("./module"); +const config = require("../config"); +const axios = require("axios"); + +class GoodreadsBookSearch { + + getBookDetails = async (isbn) => { + const headers = config.headers; + const url = encodeURI(config.goodreadsBaseUrl + isbn); + try { + return new Promise((resolve, reject) => { + setTimeout(async () => { + try { + const response = await axios.get(url, { headers }); + if (!response.data) { + throw new Error("Detay bilgisi bulunamadı!"); + } + const details = await modules.extractBookDetails( + response.data, + isbn, + ); + resolve(details); + } catch (error) { + reject(error); + } + }, config.fetchTimeout); + }); + } catch (error) { + throw new Error("Hata: " + error.message); + } + }; + +} + +module.exports = GoodreadsBookSearch; + +// (async () => { +// try { +// const BookSearch = new GoodreadsBookSearch(); +// const bookDetails = await BookSearch.getBookDetails("6059141005"); +// // console.log(bookDetails); +// } catch (error) { +// console.log(error.message); +// } +// })(); diff --git a/lib/module.js b/lib/module.js new file mode 100644 index 0000000..e651b5f --- /dev/null +++ b/lib/module.js @@ -0,0 +1,94 @@ +const cheerio = require("cheerio"); + + +const extractBookPage = ($) => { + // "pagesFormat" test ID'sine sahip p etiketindeki sayıyı çıkar + const pagesText = $('[data-testid="pagesFormat"]').text(); + + // Metinden yalnızca rakamı almak için regex kullan + const match = pagesText.match(/\d+/); // Sadece sayıları bulur + + return match ? match[0] : null; // Sayıyı döndür +}; + +const extractBookDate = ($) => { + + const publicationDateText = $('p[data-testid="publicationInfo"]').text().replace('First published ', '').trim(); + const publicationDate = new Date(publicationDateText); + const formattedDate = publicationDate.toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric' + }); + + return formattedDate; +}; + +const extractAuthorAndTranslators = ($) => { + // Yazar ve çevirmen isimlerini Set'lere ekle + const authorsSet = new Set(); + const translatorsSet = new Set(); + const authorLink = $('a.ContributorLink').attr('href'); + + $('.ContributorLink').each((index, element) => { + const name = $(element).find('.ContributorLink__name').text().trim(); + const role = $(element).find('.ContributorLink__role').text().trim(); + + if (role.toLowerCase().includes('translator')) { + translatorsSet.add(name); // Çevirmenleri ekle + } else { + authorsSet.add({ name, profileLink: authorLink }); // Yazarları ekle + } + }); + + const authors = Array.from(authorsSet); + const translators = Array.from(translatorsSet); + + const result = {}; + + if (authors.length > 0) { + result.author = authors[0]; // İlk yazar + } + + if (translators.length > 0) { + result.translators = translators; // Çevirmenlerin tamamı + } + + return result; +} +function extractGenreList($) { + + const genres = []; + $('span.BookPageMetadataSection__genreButton').each((index, element) => { + const genre = $(element).find('.Button__labelItem').text().trim(); + genres.push(genre); + }); + + return genres; +} + +const extractBookDetails = async (html, isbn, gemini_api_key) => { + const $ = cheerio.load(html); + const title = $('h1[data-testid="bookTitle"]').text(); // OK + const thumbImage = $('meta[property="og:image"]').attr('content'); // OK + const authorName = extractAuthorAndTranslators($); // OK + const description = $('.BookPageMetadataSection__description .Formatted').text().trim(); //OK + const page = extractBookPage($); + const rate = $('.RatingStatistics__rating').first().text().trim(); + const date = extractBookDate($); + const genres = extractGenreList($); + + return { + title, + thumbImage, + authorName, + description, + page, + isbn, + date, + rate, + genres + }; +}; + +module.exports = { extractBookDetails }; \ No newline at end of file diff --git a/mocha-report-config.json b/mocha-report-config.json new file mode 100644 index 0000000..874de92 --- /dev/null +++ b/mocha-report-config.json @@ -0,0 +1,11 @@ +{ + "reporterEnabled": "spec, mocha-junit-reporter", + "mochaJunitReporterReporterOptions": { + "mochaFile": "./reports/test-results.xml", + "testsuitesTitle": "🧠 Goodreads Book Search Integration Test", + "suiteTitleSeparatedBy": ": ", + "properties": { + "environment": "development" + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..65df8b1 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "wisecolt-goodreads-search-book", + "version": "1.1.0", + "description": "Allows searching for books on Goodreads. To print the book description with gemini-ai you need an api key.", + "repository": { + "type": "git", + "url": "https://github.com/szbk/goodreads-book-search.git" + }, + "keywords": [ + "goodreads", + "book", + "isbn", + "search", + "library", + "api" + ], + "main": "index.js", + "scripts": { + "test": "mocha --reporter mocha-multi-reporters --reporter-options configFile=mocha-report-config.json" + }, + "author": "wisecolt", + "license": "MIT", + "dependencies": { + "@google/generative-ai": "^0.8.0", + "axios": "^1.6.8", + "cheerio": "^1.0.0-rc.12", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "chai": "^5.1.2", + "mocha": "^11.0.1", + "mocha-junit-reporter": "^2.2.1", + "mocha-multi-reporters": "^1.5.1" + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..2f6a0b5 --- /dev/null +++ b/test/index.js @@ -0,0 +1,57 @@ +const { expect } = require("chai"); + +const GoodreadsBookSearch = require("../index"); + +describe("🧠 Goodreads Book Search Integration Test", () => { + let bookSearch; + const isbn = "9944824453"; + const timeoutDuration = 30000; + + beforeEach(() => { + bookSearch = new GoodreadsBookSearch(); + }); + + it('Is the ISBN "9944824453" 🔥', function (done) { + this.timeout(timeoutDuration); + bookSearch.getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("isbn"); + expect(bookDetails.isbn).to.equal("9944824453"); + done(); + }) + .catch(done); + }); + + it('Is the book title "Dövmeli Adam" 🚀', function (done) { + this.timeout(timeoutDuration); + bookSearch.getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("title"); + expect(bookDetails.title).to.equal("Dövmeli Adam"); + done(); + }) + .catch(done); + }); + + it('Is the book\'s publication date "1 September 2008" ⏰', function (done) { + this.timeout(timeoutDuration); + bookSearch.getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("date"); + expect(bookDetails.date).to.equal("01 Sept 2008"); + done(); + }) + .catch(done); + }); + + it('Is the page count "640" 📋', function (done) { + this.timeout(timeoutDuration); + bookSearch.getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("page"); + expect(bookDetails.page).to.equal("640"); + done(); + }) + .catch(done); + }); +}); \ No newline at end of file