audible-api
Version:
A Node.js API for searching the audible website
420 lines (331 loc) • 15.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = getAudibleBook;
var _cheerio = require("cheerio");
var _dirtyJson = _interopRequireDefault(require("dirty-json"));
var _nodeFetch = _interopRequireDefault(require("node-fetch"));
var _url = require("url");
var _audibleSearchFields = require("./data/audible-search-fields");
var _language = require("./utils/language");
var _string = require("./utils/string");
var _time = require("./utils/time");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
/**
* Get extended information about the author from their Audible URL
*
* @param author - An initial object of the creator to extend (reuired url)
* @returns An extended object of information about the author
*/
function parseAuthorInfo(_x) {
return _parseAuthorInfo.apply(this, arguments);
}
function _parseAuthorInfo() {
_parseAuthorInfo = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(author) {
var res, body, page, ldJsonList, newAuthor, main, authorJson, personJson;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.prev = 0;
if (author.url) {
_context.next = 3;
break;
}
return _context.abrupt("return", author);
case 3:
_context.next = 5;
return (0, _nodeFetch["default"])(author.url);
case 5:
res = _context.sent;
_context.next = 8;
return res.text();
case 8:
body = _context.sent;
page = (0, _cheerio.load)(body);
ldJsonList = [];
page('script[type="application/ld+json"]').each(function (i, elSel) {
// Use dirty-json because the description section has line breaks instead of \n characters like it should
var jsonObj = _dirtyJson["default"].parse(page(elSel).text());
ldJsonList.push(jsonObj);
});
ldJsonList = ldJsonList.flat();
newAuthor = _objectSpread({}, author); // Get Author's Images
main = ".adbl-main";
newAuthor.thumbnailImageUrl = page("img.author-image-outline", main).attr("src"); // parse useful ld+json from the dom of the authors's page
authorJson = ldJsonList.find(function (jsonItem) {
return jsonItem["@type"] === "MusicGroup";
});
if (authorJson && typeof authorJson !== "string") {
// Get author's name
newAuthor.name = authorJson.name || newAuthor.name; // Get author's bio
newAuthor.bio = authorJson.description || newAuthor.bio; // Get author's amazon ID
newAuthor.id = authorJson.url.split("/").pop(); // Get clean author url
newAuthor.url = authorJson.url || newAuthor.url;
}
personJson = ldJsonList.find(function (jsonItem) {
return jsonItem["@type"] === "Person";
});
if (personJson && typeof personJson !== "string") {
newAuthor.imageUrl = personJson.image;
}
return _context.abrupt("return", newAuthor);
case 23:
_context.prev = 23;
_context.t0 = _context["catch"](0);
return _context.abrupt("return", author);
case 26:
case "end":
return _context.stop();
}
}
}, _callee, null, [[0, 23]]);
}));
return _parseAuthorInfo.apply(this, arguments);
}
/**
* Get all Audible details about an Audiobook from its ASIN
*
* @param asin - Amazon Standard Identification Number, Amazon's unique ID that they assign to all of their products
* @param opts - The optional arguments
* @returns The parsed book data
*/
function getAudibleBook(_x2) {
return _getAudibleBook.apply(this, arguments);
}
function _getAudibleBook() {
_getAudibleBook = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2(asin) {
var opts,
site,
getAuthors,
_ref,
baseUrl,
bookUrl,
res,
body,
page,
main,
book,
series,
seriesArr,
copyright,
ldJsonList,
breadcrumbJson,
itemListElement,
newGenres,
productJson,
bookJson,
language,
_ref2,
ratingValue,
ratingCount,
_ref3,
lowPrice,
highPrice,
priceCurrency,
durationStr,
_durationStr$match,
_durationStr$match2,
hours,
minutes,
_args2 = arguments;
return regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
opts = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : {};
_context2.prev = 1;
site = opts.site || "us";
getAuthors = opts.getAuthors || false;
_ref = _audibleSearchFields.siteCountries[site] || _audibleSearchFields.siteCountries.us, baseUrl = _ref.url;
bookUrl = "".concat(baseUrl, "/pd/").concat(asin, "?ipRedirectOverride=true");
_context2.next = 8;
return (0, _nodeFetch["default"])(bookUrl);
case 8:
res = _context2.sent;
_context2.next = 11;
return res.text();
case 11:
body = _context2.sent;
page = (0, _cheerio.load)(body);
main = 'div[role="main"]'; // Get URL
book = {
url: page('link[rel="canonical"]').attr("href"),
authors: [],
narrators: []
}; // Get Authors
page(".authorLabel a", main).each(function (i, elSel) {
var el = page(elSel);
var newAuthor = {
name: el.text().trim()
};
var authorUrlPath = el.attr("href");
if (authorUrlPath) {
newAuthor.url = (0, _string.cleanUrl)(new _url.URL(authorUrlPath, baseUrl).href);
}
book.authors.push(newAuthor);
}); // Get extended author information
if (!getAuthors) {
_context2.next = 20;
break;
}
_context2.next = 19;
return Promise.all(book.authors.map(function (author) {
return parseAuthorInfo(author);
}));
case 19:
book.authors = _context2.sent;
case 20:
// Get narrators
page(".narratorLabel a", main).each(function (i, elSel) {
var el = page(elSel);
var newNarrator = {
name: el.text().trim()
};
var narratorUrlPath = el.attr("href");
if (narratorUrlPath) {
newNarrator.url = (0, _string.cleanNarratorUrl)(new _url.URL(narratorUrlPath, baseUrl).href);
}
book.narrators.push(newNarrator);
}); // Get series and series part
try {
series = [];
page(".seriesLabel a", main).each(function (i, elSel) {
var el = page(elSel);
var newSeries = {
name: el.text().trim()
};
var seriesUrlPath = el.attr("href");
if (seriesUrlPath) {
newSeries.url = (0, _string.cleanUrl)(new _url.URL(seriesUrlPath, baseUrl).href);
}
series.push(newSeries);
});
seriesArr = page(".seriesLabel").text().replace(/\n/g, "").trim() // remove the beginning text from the series
.replace("Series: ", "").split(", ").map(function (item) {
return item.trim();
});
seriesArr.forEach(function (seriesStr, i) {
if (seriesStr.includes("Book")) {
var seriesPart = Number(seriesStr.replace("Book ", ""));
if (seriesPart) {
var matchingBookIndex = series.findIndex(function (item) {
return item.name === seriesArr[i - 1];
});
if (matchingBookIndex > -1) {
series[matchingBookIndex].part = seriesPart;
}
}
}
});
book.series = series;
} catch (err) {// console.warn(`ERROR PARSING AUDIBLE SERIES'\n${err.stack}`);
} // Get Copyright Info string and year
try {
copyright = page(".productPublisherSummary .bc-section > .bc-box:last-child").eq(0).text().trim();
if (copyright && copyright.includes("©")) {
book.copyright = copyright.replace(/\s+/g, " ");
book.copyrightYear = (0, _string.getCopyrightYear)(copyright);
}
} catch (err) {
console.warn("Error parsing copyright", err);
}
ldJsonList = [];
page('script[type="application/ld+json"]').each(function (i, elSel) {
var jsonObj;
try {
jsonObj = JSON.parse(page(elSel).text());
} catch (err) {
jsonObj = _dirtyJson["default"].parse(page(elSel).text());
}
ldJsonList.push(jsonObj);
});
ldJsonList = ldJsonList.flat(); // parse useful ld+json from the dom of the page
// "@type": "BreadcrumbList"
breadcrumbJson = ldJsonList.find(function (jsonItem) {
return jsonItem["@type"] === "BreadcrumbList";
}); // Get Genres
if (breadcrumbJson) {
itemListElement = breadcrumbJson.itemListElement;
newGenres = [];
itemListElement.slice(1).forEach(function (breadcrumb) {
if (breadcrumb) {
var item = breadcrumb.item;
if (typeof item !== "string") {
newGenres.push({
name: item.name,
url: "".concat(baseUrl).concat(item["@id"])
});
}
}
});
book.genres = newGenres;
} // "@type": "Product"
productJson = ldJsonList.find(function (jsonItem) {
return jsonItem["@type"] === "Product";
});
if (productJson) {
// Get ASIN
book.asin = productJson.productID; // Get SKU
book.sku = productJson.sku;
} // "@type": "Audiobook"
bookJson = ldJsonList.find(function (jsonItem) {
return jsonItem["@type"] === "Audiobook";
});
if (bookJson) {
// Get title
book.title = bookJson.name; // Get clean title, without series part ("Book N") or "(Unabridged)"
book.cleanTitle = (0, _string.cleanTitle)(bookJson.name); // Get Publisher
book.publisher = bookJson.publisher; // Get language and language codes
language = (0, _language.getLanguageByName)(bookJson.inLanguage);
if (language) {
book.language = language;
} // Get full description (without any html)
book.description = (0, _string.cleanDescription)(bookJson.description); // Get the date the book was published
book.datePublished = new Date(bookJson.datePublished); // Get user rating
if (bookJson.aggregateRating) {
_ref2 = bookJson.aggregateRating, ratingValue = _ref2.ratingValue, ratingCount = _ref2.ratingCount;
book.rating = {
value: parseFloat(ratingValue),
count: Number(ratingCount)
};
} // Get pricing
if (bookJson.offers) {
_ref3 = bookJson.offers, lowPrice = _ref3.lowPrice, highPrice = _ref3.highPrice, priceCurrency = _ref3.priceCurrency;
book.price = {
low: Number(lowPrice),
high: Number(highPrice),
currency: priceCurrency
};
} // Get Abridgement
book.isAbridged = bookJson.abridged === "true"; // Get duration in seconds
durationStr = bookJson.duration;
if (durationStr) {
hours = Number(((_durationStr$match = durationStr.match(/\d+(?=H)/)) === null || _durationStr$match === void 0 ? void 0 : _durationStr$match[0]) || 0);
minutes = Number(((_durationStr$match2 = durationStr.match(/\d+(?=M)/)) === null || _durationStr$match2 === void 0 ? void 0 : _durationStr$match2[0]) || 0);
book.duration = hours * _time.SECONDS_IN_HOUR + minutes * _time.SECONDS_IN_MINUTE;
} // Get cover image URL
book.coverUrl = bookJson.image;
}
return _context2.abrupt("return", book);
case 35:
_context2.prev = 35;
_context2.t0 = _context2["catch"](1);
console.error("ERROR PARSING AUDIBLE BOOK FROM ASIN: ".concat(asin));
throw _context2.t0;
case 39:
case "end":
return _context2.stop();
}
}
}, _callee2, null, [[1, 35]]);
}));
return _getAudibleBook.apply(this, arguments);
}
//# sourceMappingURL=get-audible-book.js.map