Implement Google Books API integration for book updates and enhance library management features
- Added a new endpoint in `index.js` to update book data using the Google Books API, allowing for real-time updates based on ISBN. - Introduced a `rebuild_book_entries` function in `libraryManager.py` to facilitate user-driven updates of book entries from the Google Books API. - Enhanced error handling and user prompts for better interaction during book updates. - Updated `public/index.html` and `public/script.js` to improve the user interface and support new functionalities. - Modified styles in `public/styles.css` to enhance the layout and usability of the checkout button and location prompt.
This commit is contained in:
parent
370ad7c107
commit
f4f227b24d
@ -1,4 +1,6 @@
|
|||||||
const { Book, Location, Checkout, User, Sequelize, Op } = require('./models');
|
const { Book, Location, Checkout, User, Sequelize, Op } = require('./models');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
// Fetch book from the local database by ISBN
|
// Fetch book from the local database by ISBN
|
||||||
const fetchBookFromLocalDatabase = async (isbn) => {
|
const fetchBookFromLocalDatabase = async (isbn) => {
|
||||||
return await Book.findOne({ where: { isbn } });
|
return await Book.findOne({ where: { isbn } });
|
||||||
|
|||||||
53
index.js
53
index.js
@ -275,14 +275,15 @@ async function refetchBookData(startIndex = 0) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response && error.response.status === 429) {
|
if (error.response && error.response.status === 429) {
|
||||||
console.error('Rate limit exceeded. Pausing requests.');
|
console.error('Rate limit exceeded. Pausing requests.');
|
||||||
// Implement a delay or exit the loop if necessary
|
// Implement a delay
|
||||||
break;
|
await delay(10000); // Delay for 10 seconds
|
||||||
|
index--; // Retry the current book
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error fetching data for ISBN: ${isbn}`, error.message);
|
console.error(`Error fetching data for ISBN: ${isbn}`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Delay to handle rate limits
|
// Delay between requests to respect rate limits
|
||||||
await delay(1000); // Adjust the delay as needed
|
await delay(1000); // Adjust the delay as needed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,3 +697,49 @@ app.get('/api/books-on-loan', async (req, res) => {
|
|||||||
res.status(500).send('Internal Server Error');
|
res.status(500).send('Internal Server Error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/book/update/:isbn', async (req, res) => {
|
||||||
|
const { isbn } = req.params;
|
||||||
|
const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch data from Google Books API
|
||||||
|
const url = `https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}&key=${googleBooksApiKey}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
|
||||||
|
if (response.data.totalItems > 0) {
|
||||||
|
const volumeInfo = response.data.items[0].volumeInfo;
|
||||||
|
|
||||||
|
// Extract data from the API response
|
||||||
|
const updatedData = {
|
||||||
|
title: volumeInfo.title || null,
|
||||||
|
authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : null,
|
||||||
|
publishedDate: volumeInfo.publishedDate || null,
|
||||||
|
description: volumeInfo.description || null,
|
||||||
|
number_of_pages: volumeInfo.pageCount || null,
|
||||||
|
publishers: volumeInfo.publisher || null,
|
||||||
|
subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : null,
|
||||||
|
cover_small: volumeInfo.imageLinks ? volumeInfo.imageLinks.smallThumbnail : null,
|
||||||
|
cover_medium: volumeInfo.imageLinks ? volumeInfo.imageLinks.thumbnail : null,
|
||||||
|
cover_large: volumeInfo.imageLinks ? volumeInfo.imageLinks.large : null,
|
||||||
|
// Add more fields as needed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the book entry in the database
|
||||||
|
const book = await Book.findOne({ where: { isbn } });
|
||||||
|
|
||||||
|
if (book) {
|
||||||
|
await book.update(updatedData);
|
||||||
|
console.log(`Book data for ISBN ${isbn} updated.`);
|
||||||
|
res.json({ success: true, book: updatedData });
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Book not found in local database' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'No data found in Google Books API' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating book data:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update book data' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -117,6 +117,99 @@ def list_books_on_loan():
|
|||||||
else:
|
else:
|
||||||
console.print("Failed to fetch books on loan.", style="bold red")
|
console.print("Failed to fetch books on loan.", style="bold red")
|
||||||
|
|
||||||
|
def rebuild_book_entries():
|
||||||
|
isbn = Prompt.ask("Enter ISBN of the book to rebuild")
|
||||||
|
response = requests.get(f"{API_BASE_URL}/book/confirm/{isbn}", verify=False)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
book_data = response.json()
|
||||||
|
console.print("Current Book Data from Local Database:", style="bold blue")
|
||||||
|
console.print(f"Title: {book_data['data']['title']}")
|
||||||
|
console.print(f"Authors: {book_data['data']['authors']}")
|
||||||
|
console.print(f"Published Date: {book_data['data'].get('publishedDate', 'N/A')}")
|
||||||
|
console.print(f"Description: {book_data['data'].get('description', 'N/A')}")
|
||||||
|
|
||||||
|
# Allow the user to select fields to search with
|
||||||
|
console.print("\nSelect fields to search the Google Books API with:")
|
||||||
|
use_title = Prompt.ask("Use Title? (yes/no)", choices=["yes", "no"], default="yes") == "yes"
|
||||||
|
use_authors = Prompt.ask("Use Authors? (yes/no)", choices=["yes", "no"], default="yes") == "yes"
|
||||||
|
use_isbn = Prompt.ask("Use ISBN? (yes/no)", choices=["yes", "no"], default="yes") == "yes"
|
||||||
|
|
||||||
|
# Build search query based on selected fields
|
||||||
|
query_components = []
|
||||||
|
if use_title:
|
||||||
|
query_components.append(f'intitle:{book_data["data"]["title"]}')
|
||||||
|
if use_authors:
|
||||||
|
query_components.append(f'inauthor:{book_data["data"]["authors"]}')
|
||||||
|
if use_isbn:
|
||||||
|
query_components.append(f'isbn:{book_data["data"]["isbn"]}')
|
||||||
|
query = '+'.join(query_components)
|
||||||
|
|
||||||
|
# Query the Google Books API directly
|
||||||
|
google_response = requests.get(f"https://www.googleapis.com/books/v1/volumes?q={query}")
|
||||||
|
if google_response.status_code == 200:
|
||||||
|
google_data = google_response.json()
|
||||||
|
if google_data["totalItems"] > 0:
|
||||||
|
# Take the first result
|
||||||
|
volumes = google_data["items"]
|
||||||
|
console.print("\nMultiple volumes found. Please select one:")
|
||||||
|
for i, item in enumerate(volumes):
|
||||||
|
volume_info = item["volumeInfo"]
|
||||||
|
title = volume_info.get('title', 'N/A')
|
||||||
|
authors = ', '.join(volume_info.get('authors', []))
|
||||||
|
published_date = volume_info.get('publishedDate', 'N/A')
|
||||||
|
|
||||||
|
# Limit the length of the title and authors to 32 characters
|
||||||
|
if len(title) > 32:
|
||||||
|
title = title[:29] + '...'
|
||||||
|
if len(authors) > 32:
|
||||||
|
authors = authors[:29] + '...'
|
||||||
|
|
||||||
|
console.print(f"{i + 1}. Title: {title}, Authors: {authors}, Published Date: {published_date}")
|
||||||
|
|
||||||
|
selected_index = int(Prompt.ask("Enter the number of the volume you want to select")) - 1
|
||||||
|
volume_info = volumes[selected_index]["volumeInfo"]
|
||||||
|
console.print("\nBook Data Found from Google Books API:", style="bold green")
|
||||||
|
console.print(f"Title: {volume_info.get('title', 'N/A')}")
|
||||||
|
console.print(f"Authors: {', '.join(volume_info.get('authors', []))}")
|
||||||
|
console.print(f"Published Date: {volume_info.get('publishedDate', 'N/A')}")
|
||||||
|
console.print(f"Description: {volume_info.get('description', 'N/A')}")
|
||||||
|
# isbn (use 13, then 10 if 13 is not present) ISBN: [{'type': 'ISBN_10', 'identifier': '0521274559'}, {'type': 'ISBN_13', 'identifier': '9780521274555'}]
|
||||||
|
isbn = volume_info.get('industryIdentifiers', [])
|
||||||
|
isbn = next((identifier['identifier'] for identifier in isbn if identifier['type'] == 'ISBN_13'), None)
|
||||||
|
if not isbn:
|
||||||
|
isbn = next((identifier['identifier'] for identifier in isbn if identifier['type'] == 'ISBN_10'), None)
|
||||||
|
console.print(f"ISBN: {isbn}")
|
||||||
|
|
||||||
|
# Ask the user if they want to update the entry
|
||||||
|
update = Prompt.ask("Do you want to update this book entry? (yes/no)", choices=["yes", "no"], default="no")
|
||||||
|
if update == "yes":
|
||||||
|
data = {
|
||||||
|
"title": volume_info.get('title'),
|
||||||
|
"authors": ', '.join(volume_info.get('authors', [])),
|
||||||
|
"publishedDate": volume_info.get('publishedDate'),
|
||||||
|
"description": volume_info.get('description'),
|
||||||
|
"isbn": isbn
|
||||||
|
}
|
||||||
|
update_response = requests.put(
|
||||||
|
f"{API_BASE_URL}/book/{book_data['data']['isbn']}",
|
||||||
|
json=data,
|
||||||
|
verify=False,
|
||||||
|
auth=HTTPBasicAuth(USERNAME, PASSWORD)
|
||||||
|
)
|
||||||
|
if update_response.status_code == 200:
|
||||||
|
console.print("Book entry updated successfully.", style="bold green")
|
||||||
|
else:
|
||||||
|
console.print("Failed to update book entry.", style="bold red")
|
||||||
|
print(update_response.json())
|
||||||
|
print(update_response.status_code)
|
||||||
|
else:
|
||||||
|
console.print("No data found on Google Books API.", style="bold red")
|
||||||
|
else:
|
||||||
|
console.print("Failed to fetch data from Google Books API.", style="bold red")
|
||||||
|
else:
|
||||||
|
console.print("Book not found in local database.", style="bold red")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
while True:
|
while True:
|
||||||
console.print("\n[bold]Admin Console[/bold]")
|
console.print("\n[bold]Admin Console[/bold]")
|
||||||
@ -126,8 +219,9 @@ def main():
|
|||||||
console.print("4. Change Book Status")
|
console.print("4. Change Book Status")
|
||||||
console.print("5. Search Books")
|
console.print("5. Search Books")
|
||||||
console.print("6. List Books on Loan")
|
console.print("6. List Books on Loan")
|
||||||
console.print("7. Exit")
|
console.print("7. Rebuild Book Entries")
|
||||||
choice = Prompt.ask("Choose an option", choices=["1", "2", "3", "4", "5", "6", "7"], default="7")
|
console.print("8. Exit")
|
||||||
|
choice = Prompt.ask("Choose an option", choices=["1", "2", "3", "4", "5", "6", "7", "8"], default="8")
|
||||||
|
|
||||||
if choice == "1":
|
if choice == "1":
|
||||||
list_books()
|
list_books()
|
||||||
@ -142,6 +236,8 @@ def main():
|
|||||||
elif choice == "6":
|
elif choice == "6":
|
||||||
list_books_on_loan()
|
list_books_on_loan()
|
||||||
elif choice == "7":
|
elif choice == "7":
|
||||||
|
rebuild_book_entries()
|
||||||
|
elif choice == "8":
|
||||||
console.print("Exiting...", style="bold yellow")
|
console.print("Exiting...", style="bold yellow")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Ramsey Library</title>
|
<title>Ramsey Library</title>
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<button id="dark-mode-toggle">🌙</button>
|
<button id="dark-mode-toggle">🌙</button>
|
||||||
<h1>Ramsey Library</h1>
|
<h1>Ramsey Library</h1>
|
||||||
@ -32,7 +34,7 @@
|
|||||||
// Fetch all books when the page loads
|
// Fetch all books when the page loads
|
||||||
window.onload = fetchBooks('');
|
window.onload = fetchBooks('');
|
||||||
|
|
||||||
document.getElementById('search-bar').addEventListener('input', function() {
|
document.getElementById('search-bar').addEventListener('input', function () {
|
||||||
fetchBooks(this.value);
|
fetchBooks(this.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,60 +76,60 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility function to get a cookie by name
|
// Utility function to get a cookie by name
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
const parts = value.split(`; ${name}=`);
|
const parts = value.split(`; ${name}=`);
|
||||||
if (parts.length === 2) return parts.pop().split(';').shift();
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to set a cookie
|
|
||||||
function setCookie(name, value, days) {
|
|
||||||
const date = new Date();
|
|
||||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
||||||
const expires = `expires=${date.toUTCString()}`;
|
|
||||||
document.cookie = `${name}=${value}; ${expires}; path=/`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client-side function to request checkout
|
|
||||||
function requestCheckout(isbn) {
|
|
||||||
let email = getCookie('lastEmail') || '';
|
|
||||||
let name = getCookie('lastName') || '';
|
|
||||||
|
|
||||||
// Prompt the user for email and name, pre-filling with existing values if available
|
|
||||||
email = prompt('Please enter your email:', email);
|
|
||||||
name = prompt('Please enter your name:', name);
|
|
||||||
|
|
||||||
if (!email || !name) {
|
|
||||||
alert('Email and name are required to proceed with checkout.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the email and name in cookies for future use
|
|
||||||
setCookie('lastEmail', email, 7); // Store for 7 days
|
|
||||||
setCookie('lastName', name, 7); // Store for 7 days
|
|
||||||
|
|
||||||
fetch(`/api/checkout/${isbn}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, name })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.success) {
|
|
||||||
alert('Checkout request sent successfully.');
|
|
||||||
} else {
|
|
||||||
alert(data.error || 'Failed to send checkout request.');
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.catch(error => {
|
// Utility function to set a cookie
|
||||||
console.error('Error:', error);
|
function setCookie(name, value, days) {
|
||||||
alert('An error occurred while sending the checkout request.');
|
const date = new Date();
|
||||||
});
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
}
|
const expires = `expires=${date.toUTCString()}`;
|
||||||
|
document.cookie = `${name}=${value}; ${expires}; path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side function to request checkout
|
||||||
|
function requestCheckout(isbn) {
|
||||||
|
let email = getCookie('lastEmail') || '';
|
||||||
|
let name = getCookie('lastName') || '';
|
||||||
|
|
||||||
|
// Prompt the user for email and name, pre-filling with existing values if available
|
||||||
|
email = prompt('Please enter your email:', email);
|
||||||
|
name = prompt('Please enter your name:', name);
|
||||||
|
|
||||||
|
if (!email || !name) {
|
||||||
|
alert('Email and name are required to proceed with checkout.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the email and name in cookies for future use
|
||||||
|
setCookie('lastEmail', email, 7); // Store for 7 days
|
||||||
|
setCookie('lastName', name, 7); // Store for 7 days
|
||||||
|
|
||||||
|
fetch(`/api/checkout/${isbn}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, name })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('Checkout request sent successfully.');
|
||||||
|
} else {
|
||||||
|
alert(data.error || 'Failed to send checkout request.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('An error occurred while sending the checkout request.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function sortTable(columnIndex) {
|
function sortTable(columnIndex) {
|
||||||
const table = document.getElementById('book-table');
|
const table = document.getElementById('book-table');
|
||||||
@ -171,4 +173,5 @@ function requestCheckout(isbn) {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
@ -259,7 +259,8 @@ function requestCheckout(isbn) {
|
|||||||
fetch(`/api/checkout/${isbn}`, {
|
fetch(`/api/checkout/${isbn}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + btoa(username + ':' + password)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
|||||||
@ -197,4 +197,49 @@ body.dark-mode #book-table-body tr:hover {
|
|||||||
background-color: #444;
|
background-color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Center the checkout button and make it fill the cell */
|
||||||
|
#book-table td button {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box; /* Ensure padding and border are included in the element's total width and height */
|
||||||
|
}
|
||||||
|
|
||||||
/* Add more styles as needed for other elements */
|
/* Add more styles as needed for other elements */
|
||||||
|
|
||||||
|
#location-prompt {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #000;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-prompt label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-prompt input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-prompt select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#location-prompt button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user