/** @module routes/v1 */
// Register router
const { authorize } = require('../../utils.js');
const fs = require('fs');
const express = require('express');
const router = express.Router();
const BookController = require('../../controllers/BookController.js');
const EditionController = require('../../controllers/EditionController.js');
const SpineImageController = require('../../controllers/SpineImageController.js');
const AuthorController = require('../../controllers/AuthorController.js');
const TokenController = require('../../controllers/TokenController.js');
const { ISBNFactory, ISBN } = require('../../classes/books/ISBN.js');
const { Edition, EditionFactory } = require('../../classes/books/Edition.js');
const { SpineImageFactory } = require('../../classes/books/SpineImage.js');
const { BookFactory } = require('../../classes/books/Book.js');
const { AuthorFactory } = require('../../classes/books/Author.js');
const { TokenFactory } = require('../../classes/users/Token.js');
/** Route for adding a new edition */
router.post('/contribute/edition/add', async (req, res) => {
if (!await authorize(['contribute.edition'], req, res)) {
return;
}
let isbnString = req.headers.isbn;
if (!isbnString) {
res.status(400).send({ message: 'Missing ISBN' });
return;
}
const isbn = await new ISBNFactory().setISBN(isbnString).create();
if (!isbn) {
res.status(400).send({ message: 'Invalid ISBN' });
return;
}
if (!req.busboy) {
res.status(400).send({ message: 'Missing spine image' });
return;
}
const spineImageFilepath = generateSpineImagePath();
let fstream;
let streamClosed = false;
req.pipe(req.busboy);
req.busboy.on('file', (fieldname, file, filename) => {
fstream = fs.createWriteStream(spineImageFilepath);
file.pipe(fstream);
file.on('close', () => {
streamClosed = true;
});
});
const user = await (await new TokenFactory().load(await new TokenController().byTokenString(req.headers['authorization'])).create()).getUser();
const editionDbRecord = await new EditionController().byISBN(isbn);
let edition;
if (editionDbRecord) {
edition = await new EditionFactory().load(editionDbRecord).create();
} else {
edition = await addEdition(isbn);
await user.addContribution("ADD_EDITION_AUTOMATIC", edition.id);
}
const spineImageFilename = spineImageFilepath.split('/').pop();
const newSpineImage = await new SpineImageFactory().load(await new SpineImageController().insert(edition.id, spineImageFilename)).create();
if (fstream) {
while (!streamClosed) {
await new Promise(resolve => setTimeout(resolve, 100));
}
await user.addContribution("ADD_SPINE_IMAGE", newSpineImage.id);
await user.changeGoalProgressByTrackName('Contributor', 'spine images added', 1);
await user.changeGoalProgressByTrackName('Contributor', 'points earned', (await user.getContributionType('ADD_SPINE_IMAGE')).point_value);
res.status(200).send({ message: 'Added edition successfully' });
} else {
res.status(500).send({ message: 'Failed to save spine image' });
}
});
module.exports = router;
// Functions
/**
* Adds an edition to the database using OpenLibrary API if it doesn't exist in our database
*
* @param {ISBN} isbn
* @returns {Promise<Edition>} edition
*/
async function addEdition(isbn) {
const bookInfo = await getOLBookInfo(isbn);
if (!bookInfo) {
return;
}
const title = bookInfo.title;
const subtitle = bookInfo.subtitle;
const publishDate = parseDate(bookInfo.publish_date);
const bookController = new BookController();
const existingBookDbRecord = await bookController.byTitle(title);
let existingBook;
if (!existingBookDbRecord) {
existingBook = await new BookFactory().load(await bookController.insert(title, subtitle, publishDate)).create();
} else {
existingBook = await new BookFactory().load(existingBookDbRecord).create();
}
const authors = [];
if (bookInfo.authors) {
for (const authorKeyObj of bookInfo.authors) {
const authorInfo = await getOLAuthorInfo(authorKeyObj.key.replace('/authors/', ''));
authors.push({
name: authorInfo.name,
personal_name: authorInfo.personal_name,
});
}
const authorController = new AuthorController();
for (const author of authors) {
const existingAuthorDbRecord = await authorController.byName(author.name);
let existingAuthor;
if (!existingAuthorDbRecord) {
existingAuthor = await new AuthorFactory().load(await authorController.insert(author.name, author.personal_name)).create();
} else {
existingAuthor = await new AuthorFactory().load(existingAuthorDbRecord).create();
}
await existingAuthor.linkToBook(existingBook);
}
}
return await new EditionFactory().load(await new EditionController().insert(existingBook.id, isbn.getISBN10(), isbn.getISBN(), bookInfo.key)).create();
}
/**
* Get book info from OpenLibrary API using ISBN
*
* @param {ISBN} isbn
* @returns {Promise<object>}
*/
async function getOLBookInfo(isbn) {
return (await fetch(`https://openlibrary.org/isbn/${isbn.getISBN()}.json`, { redirect: 'follow' })).json();
}
/**
* Get author info from OpenLibrary API using author key
*
* @param {string} authorKey
* @returns {Promise<object>}
*/
async function getOLAuthorInfo(authorKey) {
return (await fetch(`https://openlibrary.org/authors/${authorKey}.json`, { redirect: 'follow' })).json();
}
/**
* Generates a new unique spine image path using the current date and a random number
*
* @returns {string} path
*/
function generateSpineImagePath() {
const date = new Date();
const random = Math.floor(Math.random() * 1000);
return process.env.STORAGE_PATH + '/spine-images/' + date.getTime() + '-' + random + '.jpg';
}
/**
* Parses a date string into a date object
*
* @param {any} date_str
* @returns {date} parsedDate
*/
function parseDate(date_str) {
if (!date_str) {
return null;
}
const date = new Date(date_str);
if (isNaN(date.getTime())) {
return null;
}
return date;
}