commit 1d307908c6539cba0e539877266207d2fff2d4e0 Author: szbk Date: Mon Jan 20 23:58:11 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..838db45 --- /dev/null +++ b/.npmignore @@ -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/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..b9034d5 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,81 @@ +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 { + echo "List directory - Before removing reports" + sh 'ls -la' + } + } + stage('Remove reports') { + steps { + echo "Remove reports files" + sh 'rm -rf reports' + } + } + stage('List directory - After removing reports') { + steps { + echo "List directory - After removing reports" + sh 'ls -la' + } + } + stage('Check for package.json changes') { + steps { + echo "Check for package.json changes" + script { + // Check if package.json has changed + def hasChanges = sh(script: 'git diff --name-only HEAD~1 | grep "package.json"', returnStatus: true) == 0 + if (hasChanges) { + echo "package.json changed, running npm install" + currentBuild.result = 'SUCCESS' + sh 'npm install' + } else { + echo "package.json not changed, skipping npm install" + currentBuild.result = 'SUCCESS' + } + } + } + } + stage('Run Tests') { + steps { + echo "Runing Test.." + sh 'npm test' + } + } + stage('Publish Test Results') { + steps { + echo "Publish Test Results" + junit 'reports/test-results.xml' + echo "Send test result slack 🚚" + } + } + } + post { + always { + slackSend( + channel: '#jenkins', + tokenCredentialId: 'slack-token', + message: "Job 📦 '${env.JOB_NAME} [${env.BUILD_NUMBER}]' başarısız oldu. 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' + ) + } + } +} \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..cdbd45e --- /dev/null +++ b/Readme.md @@ -0,0 +1,87 @@ +# Unoffical Amazon Book Search + +This library allows you to retrieve book information from Amazon using only the ISBN number, without needing an API key. The library analyzes Amazon HTML structure and returns book details in JSON format. It works asynchronously (based on Promises). + +At the same time, you can recreate book descriptions using Gemini AI with this library. Remember that you need an API key for the Gemini service to do this! + +## ✨ Features + +- 📚 Retrieves book information from Amazon 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. +- 🤖 Book descriptions created with Gemini AI! + +## 🎯 Requirements + +- Node.js (v14 veya üzeri) +- NPM +- Internet connection +- Curiosity +- Preferred Gemini AI API Key! + +## 📦 Installation + +```bash +npm install szbk-amazon-book-search +``` +## 🚀 Usage + +The following example shows how to use the API: + +```javascript +const AmazonBookSearch = require("szbk-amazon-book-search"); + +(async () => { + try { + const BookSearch = new AmazonBookSearch("en"); + const bookDetails = await BookSearch.getBookDetails("0593724283"); + // const bookDetails = await BookSearch.getBookDetails("0593724283", "Gemini API Key"); + + console.log(bookDetails); + } catch (error) { + console.error("Error:", error.message); + } +})(); + + +// Example Output: +{ + title: 'The Desert Spear: Book Two of The Demon Cycle', + thumbImage: 'https://m.media-amazon.com/images/I/51VC3BV9KBL._SY445_SX342_.jpg', + authorName: 'Peter V. Brett', + description: 'INTERNATIONAL BESTSELLER • “[Peter V. Brett] confirms his place among epic fantasy’s pantheon of greats amid the likes of George R. R. Martin, Steven Erikson, and Robert Jordan.”—Fantasy Book Critic The second volume in the internationally bestselling Demon Cycle series continues the epic tale of humanity’s last stand against an army of demons—and reveals a new contender for the role of savior. The sun is setting on humanity. The night now belongs to voracious demons that prey upon a dwindling population forced to cower behind half-forgotten symbols of power. Legends tell of a Deliverer: a general who once bound all mankind into a single force that defeated the demons. But is the return of the Deliverer just another myth? Out of the sands rides Ahmann Jardir, who has forged the desert tribes of Krasia into a demon-killing army. He has proclaimed himself Shar’Dama Ka, the Deliverer, and carries ancient weapons—a spear and a crown—that give credence to his claim. But the Northerners claim their own Deliverer: the Warded Man, a dark, forbidding figure. Once, the Shar’Dama Ka and the Warded Man were friends. Now they are fierce adversaries. Yet as old allegiances are tested and fresh alliances forged, all are unaware of the appearance of a new breed of demon, more intelligent—and deadly—than any that have come before. Includes a bonus Demon Cycle novella, Brayan’s Gold, and a Krasian dictionaryDon’t miss any of the thrilling novels in Peter V. Brett’s Demon Cycle THE WARDED MAN • THE DESERT SPEAR • THE DAYLIGHT WAR • THE SKULL THRONE • THE CORE', + page: 864, + publisher: 'Del Rey', + isbn: '0593724283', + date: 'November 7, 2023', + rate: '4.5' +} +``` + +## 📂 Project Structure +```javascript +szbk-amazon-search-book +├── config +│ └── index.js # Configuration file +├── lib +│ ├── index.js # AmazonBookSearch class +│ └── module.js # Data processing and parsing operations +├── test +│ └── index.js # Integration tests +├── index.js # Entry point +├── package.json # Dependencies and scripts +└── README.md # Documentation +``` + +## 🧪 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..c0c5e5b --- /dev/null +++ b/config/index.js @@ -0,0 +1,16 @@ +require("dotenv").config(); + +const config = { + en_base_url: "https://www.amazon.com/s?k=", + en_detail_url: "https://www.amazon.com/dp/", + tr_base_url: "https://www.amazon.com.tr/s?k=", + tr_detail_url: "https://www.amazon.com.tr/dp/", + 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", + }, + // Amazon'a 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..4fd7bdf --- /dev/null +++ b/lib/index.js @@ -0,0 +1,85 @@ +const modules = require("./module"); +const config = require("../config"); +const axios = require("axios"); + +class AmazonBookSearch { + constructor(location) { + if (location !== "tr" && location !== "en") { + throw new Error("Yanlış konum!"); + } + + this.url = location === "tr" ? config.tr_base_url : config.en_base_url; + + const fetchBookId = async (isbn) => { + const headers = config.headers; + try { + const url = encodeURI(this.url + isbn); + const response = await axios.get(url, { headers }); + if (!response.data) { + throw new Error("Kitap bilgisi bulunamadı!"); + } + const bookId = modules.extractBookId(response.data); + + return bookId; + } catch (error) { + throw new Error("Hata: " + error.message); + } + }; + + this.getBookDetails = async (isbn, geminiApiKey) => { + + const headers = config.headers; + try { + const bookId = await fetchBookId(isbn); + const url = encodeURI(location == "tr" ? config.tr_detail_url + bookId : config.en_detail_url + bookId); + + 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, + geminiApiKey, + location + ); + resolve(details); + } catch (error) { + reject(error); + } + }, config.fetchTimeout); + }); + } catch (error) { + throw new Error("Hata: " + error.message); + } + }; + } +} + +module.exports = AmazonBookSearch; + +// Gemini API Key girilirse, amazon'da bulunan kitap açıklaması gemini tarafından yeniden oluşturulur. +// (async () => { +// try { +// const BookSearch = new AmazonBookSearch("tr"); +// const bookDetails = await BookSearch.getBookDetails("9786257746168", "AIzaSyAY15XJcK1VIxzMRe48dyDEeNPqeqhQt2I"); +// console.log(bookDetails); +// } catch (error) { +// console.log(error.message); +// } +// })(); + +// // +// (async () => { +// try { +// const BookSearch = new AmazonBookSearch("en"); +// const bookDetails = await BookSearch.getBookDetails("0593724283"); +// 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..7005f96 --- /dev/null +++ b/lib/module.js @@ -0,0 +1,106 @@ +const { GoogleGenerativeAI } = require("@google/generative-ai"); +const cheerio = require("cheerio"); + +const geminiDescriptionEdit = async (description, geminiApiKey, location) => { + if (geminiApiKey != undefined) { + const genAI = new GoogleGenerativeAI(geminiApiKey); + const model = genAI.getGenerativeModel({ model: "gemini-pro" }); + let prompt; + if (location == "en") { + prompt = + description + + " based on the text above, provide a detailed book description of similar length. Do not add any comments to the text, remain completely faithful to it. Only provide the book description, without using a title like book description."; + console.log(prompt); + + } else { + prompt = + description + + " yukarıdaki metine bağlı kalarak, benzer uzunlukta bir kitap tanımı ver. Metine hiçbir yorum katma, metine tamamen sadık kal. Sadece kitap tanımını ver, Kitap Tanımı şeklinde bir başlık kullanma"; + } + const result = await model.generateContent(prompt); + const response = await result.response; + return response.text(); + } else { + return description; + } +}; + +const extractBookId = (html) => { + const $ = cheerio.load(html); + let bookId = null; + + $("[data-csa-c-item-id]").each((index, element) => { + const itemId = $(element).attr("data-csa-c-item-id"); + if (!itemId.startsWith("amzn1.asin.1.B")) { + bookId = itemId + .replace(/^amzn1\.asin(\.amzn1)?\./, "") + .replace(/^1\./, ""); + return false; // Exit loop after finding the first valid bookId + } + }); + + return bookId; +}; +const extractBookPage = ($) => { + const text = $("div.rpi-attribute-value span").text(); // İçeriği al + const numberMatch = text.match(/\d+/); // Yalnızca rakamları al + return numberMatch ? parseInt(numberMatch[0], 10) : null; // Sayıyı döndür +}; + +const extractBookPublisher = ($) => { + const publisherParentDiv = $(".rpi-icon.book_details-publisher").parent(); + const publisherDiv = publisherParentDiv.next(); + const spanInsidePublisherSibling = publisherDiv.find("span"); + return spanInsidePublisherSibling.text(); +}; + +const extractBookDate = ($) => { + const dateParentDiv = $(".rpi-icon.book_details-publication_date").parent(); + const dateDiv = dateParentDiv.next(); + const spanInsideDateSibling = dateDiv.find("span"); + return spanInsideDateSibling.text(); +}; + +const extractBookDetails = async (html, isbn, geminiApiKey, location) => { + const extractedText = html + .match( + /]*class="a-expander-content a-expander-partial-collapse-content"[^>]*>(.*?)<\/div>/s + )?.[1] + ?.replace(/<[^>]+>/g, "") + .trim(); + + // Sonucu yazdır + const $ = cheerio.load(html); + const title = $("#imgTagWrapperId img").attr("alt"); + const thumbImage = $("#imgTagWrapperId img").attr("src"); + const authorName = $(".author a").text(); + + const descriptionRaw = $("#bookDescription_feature_div .a-expander-content") + .text() + .trim(); + + const description = await geminiDescriptionEdit( + descriptionRaw, + geminiApiKey, + location + ); + const page = extractBookPage($); + const publisher = extractBookPublisher($); + const date = extractBookDate($); + const ratingText = $('i[data-hook="average-star-rating"] .a-icon-alt').text(); + const rate = ratingText.match(/[\d.,]+/)[0].replace(",", "."); + + return { + title, + thumbImage, + authorName, + description, + page, + publisher, + isbn, + date, + rate + }; +}; + +module.exports = { extractBookId, extractBookDetails }; diff --git a/mocha-report-config.json b/mocha-report-config.json new file mode 100644 index 0000000..99678bb --- /dev/null +++ b/mocha-report-config.json @@ -0,0 +1,11 @@ +{ + "reporterEnabled": "spec, mocha-junit-reporter", + "mochaJunitReporterReporterOptions": { + "mochaFile": "./reports/test-results.xml", + "testsuitesTitle": "📦 Amazon 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..d1f5231 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "szbk-amazon-book-search", + "version": "1.1.0", + "description": "Allows searching for books on Amazon. To print the book description with gemini-ai you need an api key.", + "main": "index.js", + "scripts": { + "test": "mocha --reporter mocha-multi-reporters --reporter-options configFile=mocha-report-config.json" + }, + "repository": { + "type": "git", + "url": "" + }, + + "author": "szbk", + "keywords": [ + "amazon", + "book", + "isbn", + "search", + "library", + "api" + ], + "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" + } +} \ No newline at end of file diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..a489f37 --- /dev/null +++ b/test/index.js @@ -0,0 +1,194 @@ +const { expect } = require("chai"); + +const AmazonBookSearch = require("../index"); + +describe("📦 Amazon Book Search (Turkish) Integration Test", () => { + let bookSearch; + const isbn = "9944824453"; + const geminiApiKey = "AIzaSyAY15XJcK1VIxzMRe48dyDEeNPqeqhQt2I"; + const timeoutDuration = 30000; + + beforeEach(() => { + bookSearch = new AmazonBookSearch("tr"); + }); + + 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: İblis Döngüsü 1" 🚀', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("title"); + expect(bookDetails.title).to.equal("Dövmeli Adam: İblis Döngüsü 1"); + done(); + }) + .catch(done); + }); + + it('Checks if the book thumb image is not empty 🌄', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("thumbImage"); + expect(bookDetails.thumbImage).to.be.a("string"); + done(); + }) + .catch(done); + }); + + it('Is the book\'s publication date "1 Temmuz 2016" ⏰', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("date"); + expect(bookDetails.date).to.equal("1 Temmuz 2016"); + done(); + }) + .catch(done); + }); + + it('Is the page count "638" 📋', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("page"); + expect(bookDetails.page).to.equal(638); + done(); + }) + .catch(done); + }); + + it('Is the publisher "Epsilon Yayınları" 📖', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("publisher"); + expect(bookDetails.publisher).to.equal("Epsilon Yayınları"); + done(); + }) + .catch(done); + }); + + it('Is the Gemini API test working for the book description 🤖', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn, geminiApiKey) + .then((bookDetails) => { + expect(bookDetails).to.have.property("description"); + expect(bookDetails.description).to.be.a("string"); + done(); + }) + .catch(done); + }); +}); + +describe("📦 Amazon Book Search (English) Integration Test", () => { + let bookSearch; + const isbn = "0593724283"; + const timeoutDuration = 30000; + const geminiApiKey = "AIzaSyAY15XJcK1VIxzMRe48dyDEeNPqeqhQt2I"; + + beforeEach(() => { + bookSearch = new AmazonBookSearch("en"); + }); + + it('Is the ISBN "0593724283" 🔥', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("isbn"); + expect(bookDetails.isbn).to.equal("0593724283"); + done(); + }) + .catch(done); + }); + + it('Is the book title "The Desert Spear: Book Two of The Demon Cycle" 🚀', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("title"); + expect(bookDetails.title).to.equal("The Desert Spear: Book Two of The Demon Cycle"); + done(); + }) + .catch(done); + }); + + it('Checks if the book thumb image is not empty 🌄', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("thumbImage"); + expect(bookDetails.thumbImage).to.be.a("string"); + done(); + }) + .catch(done); + }); + + it('Is the book\'s publication date "November 7, 2023" ⏰', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("date"); + expect(bookDetails.date).to.equal("November 7, 2023"); + done(); + }) + .catch(done); + }); + + it('Is the page count "864" 📋', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("page"); + expect(bookDetails.page).to.equal(864); + done(); + }) + .catch(done); + }); + + it('Is the publisher "Del Rey" 📖', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn) + .then((bookDetails) => { + expect(bookDetails).to.have.property("publisher"); + expect(bookDetails.publisher).to.equal("Del Rey"); + done(); + }) + .catch(done); + }); + + it('Is the Gemini API test working for the book description 🤖', function (done) { + this.timeout(timeoutDuration); + bookSearch + .getBookDetails(isbn, geminiApiKey) + .then((bookDetails) => { + expect(bookDetails).to.have.property("description"); + expect(bookDetails.description).to.be.a("string"); + done(); + }) + .catch(done); + }); +}); +