const axios = require('axios'); const express = require('express'); const https = require('https'); const fs = require('fs'); const path = require('path'); const sqlite3 = require('sqlite3').verbose(); const bodyParser = require('body-parser'); const app = express(); const PORT = process.env.PORT || 3000; // SSL certificate paths const privateKey = fs.readFileSync(path.join(__dirname, 'server.key'), 'utf8'); const certificate = fs.readFileSync(path.join(__dirname, 'server.cert'), 'utf8'); const credentials = { key: privateKey, cert: certificate }; // Middleware to parse JSON bodies app.use(express.json()); // Use built-in body-parser for JSON const { Sequelize, DataTypes } = require('sequelize'); // Initialize Sequelize const sequelize = new Sequelize({ dialect: 'sqlite', storage: './books.db', }); // Test the connection sequelize.authenticate() .then(() => { console.log('Connection has been established successfully.'); }) .catch(err => { console.error('Unable to connect to the database:', err); }); // TODO: Add physical location of book to database const Book = sequelize.define('Book', { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, isbn: { type: DataTypes.TEXT, allowNull: false, unique: true, }, title: { type: DataTypes.TEXT, allowNull: false, }, authors: { type: DataTypes.TEXT, }, publishedDate: { type: DataTypes.TEXT, }, description: { type: DataTypes.TEXT, }, url: { type: DataTypes.TEXT, }, number_of_pages: { type: DataTypes.INTEGER, }, identifiers: { type: DataTypes.TEXT, }, publishers: { type: DataTypes.TEXT, }, subjects: { type: DataTypes.TEXT, }, notes: { type: DataTypes.TEXT, }, cover_small: { type: DataTypes.TEXT, }, cover_medium: { type: DataTypes.TEXT, }, cover_large: { type: DataTypes.TEXT, }, location_id: { type: DataTypes.INTEGER, references: { model: Location, key: 'id' } } }, { tableName: 'books', timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` }); const Location = sequelize.define('Location', { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true, }, name: { type: DataTypes.TEXT, allowNull: false, }, shelf: { type: DataTypes.TEXT, allowNull: false, } }, { tableName: 'locations', timestamps: false, // If your table doesn't have `createdAt` and `updatedAt` }); // Set up the SQLite database const db = new sqlite3.Database('./books.db', (err) => { if (err) { console.error('Could not connect to database', err); } else { console.log('Connected to database'); db.run(` CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT, title TEXT, authors TEXT, publishedDate TEXT, description TEXT, url TEXT, number_of_pages INTEGER, identifiers TEXT, publishers TEXT, subjects TEXT, notes TEXT, cover_small TEXT, cover_medium TEXT, cover_large TEXT ) `); } }); // Serve static files from the 'public' directory app.use(express.static(path.join(__dirname, 'public'))); // Endpoint to fetch book details by ISBN app.get('/book/:isbn', async (req, res) => { const { isbn } = req.params; console.log(`Fetching book data for ISBN: ${isbn}`); try { // First, check if the book is in the local database const book = await Book.findOne({ where: { isbn } }); if (book) { console.log('Book found in the local database'); res.json({ source: 'local', data: book }); return; } // If not found locally, try Google Books const apiKey = 'AIzaSyCQikthZ5TlkFTcKTG8n171dRafosK2Mg8'; const googleBooksResponse = await axios.get(`https://www.googleapis.com/books/v1/volumes?q=${isbn}&key=${apiKey}`); if (googleBooksResponse.status === 429) { console.log('Rate limit exceeded for Google Books'); res.status(429).json({ error: 'Rate limit exceeded' }); return; } if (googleBooksResponse.status === 408) { console.log('Request Timeout for Google Books'); res.status(408).json({ error: 'Request Timeout' }); return; } if (googleBooksResponse.data.items && googleBooksResponse.data.items.length > 0) { const googleBookData = googleBooksResponse.data.items[0]; console.log('Book data found in Google Books'); res.json({ source: 'external', data: formatGoogleBooksData(googleBookData) }); return; } else { console.log('Book not found in Google Books'); } // If not found in Google Books, try Open Library const openLibraryResponse = await axios.get(`https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`); const bookData = openLibraryResponse.data[`ISBN:${isbn}`]; if (openLibraryResponse.status === 429) { console.log('Rate limit exceeded for Open Library'); res.status(429).json({ error: 'Rate limit exceeded' }); return; } if (openLibraryResponse.status === 408) { console.log('Request Timeout for Open Library'); res.status(408).json({ error: 'Request Timeout' }); return; } if (bookData) { console.log('Book data found in Open Library'); res.json({ source: 'external', data: formatOpenLibraryData(bookData) }); return; } else { console.log('Book not found in Open Library'); } // If not found in Google Books or Open Library, try the Internet Archive const archiveResponse = await axios.get(`https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`); const archiveData = archiveResponse.data[`ISBN:${isbn}`]; if (archiveData) { console.log('Book data found in the Internet Archive'); res.json({ source: 'external', data: formatArchiveData(archiveData) }); } else { console.log('Book not found in the Internet Archive'); res.status(404).json({ error: 'Book not found' }); } } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to fetch book data' }); } }); app.get('/search-title', async (req, res) => { const { title, internalOnly=false } = req.query; console.log(`Searching for books by title or related fields: ${title}`); try { // First, search in the local database across all relevant fields const localBooks = await Book.findAll({ where: { [Sequelize.Op.or]: [ { title: { [Sequelize.Op.like]: `%${title}%` } }, { authors: { [Sequelize.Op.like]: `%${title}%` } }, { publishers: { [Sequelize.Op.like]: `%${title}%` } }, { description: { [Sequelize.Op.like]: `%${title}%` } }, { subjects: { [Sequelize.Op.like]: `%${title}%` } } ] } }); if (localBooks.length > 0) { console.log('Books found in the local database'); res.json({ source: 'local', results: localBooks }); return; } if (internalOnly) { res.status(404).json({ error: 'No books found with that title or related fields.' }); return; } // If no results found locally, proceed to search external sources const openLibraryResponse = await axios.get(`https://openlibrary.org/search.json?q=${encodeURIComponent(title)}&limit=7`); const searchResults = openLibraryResponse.data.docs; if (searchResults.length > 0) { console.log('Books found by title in external sources'); const bookData = searchResults.map(result => ({ title: result.title, authors: result.author_name || [], publish_date: result.first_publish_year, isbn: result.isbn ? result.isbn[0] : '', publisher: result.publisher ? result.publisher[0] : '', key: result.key // Unique key to fetch more detailed data later if needed })); res.json({ source: 'external', results: bookData }); } else { res.status(404).json({ error: 'No books found with that title or related fields.' }); } } catch (error) { console.error(error); res.status(500).json({ error: 'Failed to search for book by title or related fields' }); } }); // Endpoint to store book in the database app.post('/store-book', async (req, res) => { try { const book = await Book.create(req.body); res.json({ success: true, book }); } catch (error) { console.error('Failed to store book:', error); res.status(500).json({ error: 'Failed to store book in database' }); } }); app.put('/book/:isbn', async (req, res) => { try { const { isbn } = req.params; const book = await Book.findOne({ where: { isbn } }); if (book) { await book.update(req.body); res.json({ success: true, message: 'Book updated successfully' }); } else { res.status(404).json({ error: 'Book not found' }); } } catch (error) { console.error('Failed to update book:', error); res.status(500).json({ error: 'Failed to update book in database' }); } }); app.delete('/book/:id', async (req, res) => { try { const { id } = req.params; const book = await Book.findByPk(id); if (book) { await book.destroy(); res.json({ success: true, message: 'Book deleted successfully' }); } else { res.status(404).json({ error: 'Book not found' }); } } catch (error) { console.error('Failed to delete book:', error); res.status(500).json({ error: 'Failed to delete book from database' }); } }); // TODO: Function to checkout a book // TODO: Function to return a book function formatOpenLibraryData(data) { return { isbn: data.identifiers.isbn_13 ? data.identifiers.isbn_13[0] : '', title: data.title, authors: data.authors ? data.authors.map(author => author.name) : [], publishedDate: data.publish_date, description: data.excerpts ? data.excerpts[0].text : 'No description available', url: data.url, number_of_pages: data.number_of_pages || null, identifiers: JSON.stringify(data.identifiers), // Store as JSON string publishers: data.publishers ? data.publishers.map(pub => pub.name).join(', ') : '', subjects: data.subjects ? data.subjects.map(sub => sub.name).join(', ') : '', notes: data.notes || '', cover_small: data.cover ? data.cover.small : '', cover_medium: data.cover ? data.cover.medium : '', cover_large: data.cover ? data.cover.large : '' }; } function formatArchiveData(data) { return { isbn: data.isbn ? data.isbn[0] : '', title: data.title, authors: data.contributor ? [data.contributor] : [], publishedDate: data.date ? new Date(data.date).getFullYear() : '', description: data.description ? data.description.join(' ') : 'No description available', url: `https://archive.org/details/${data.identifier}`, number_of_pages: data.imagecount || null, identifiers: JSON.stringify({ archive_identifier: data.identifier, oclc: data['external-identifier'] ? data['external-identifier'].filter(id => id.includes('urn:oclc')).map(id => id.split(':')[2]) : [] }), publishers: data.publisher || '', subjects: data.subject ? data.subject.join(', ') : '', notes: '', // Archive data does not provide specific notes like Open Library cover_small: '', // Placeholder, as cover image URLs need to be constructed manually cover_medium: '', // Same as above cover_large: '' // Same as above }; } function formatGoogleBooksData(data) { return { isbn: data.volumeInfo.industryIdentifiers ? data.volumeInfo.industryIdentifiers.find(id => id.type === 'ISBN_13').identifier : '', title: data.volumeInfo.title, authors: data.volumeInfo.authors || [], publishedDate: data.volumeInfo.publishedDate, description: data.volumeInfo.description || 'No description available', url: data.volumeInfo.previewLink, number_of_pages: data.volumeInfo.pageCount || null, identifiers: JSON.stringify(data.volumeInfo.industryIdentifiers), publishers: data.volumeInfo.publisher || '', subjects: data.volumeInfo.categories || [], notes: '', // Placeholder for additional notes cover_small: data.volumeInfo.imageLinks.smallThumbnail || '', cover_medium: data.volumeInfo.imageLinks.thumbnail || '', cover_large: data.volumeInfo.imageLinks.small || '' }; } const httpsServer = https.createServer(credentials, app); sequelize.sync({ alter: true }).then(() => { console.log('Database & tables synced!'); }); httpsServer.listen(PORT, () => { console.log(`HTTPS Server running on https://localhost:${PORT}`); });