From d2b88f4e5955ff05845b6f8a52eabf19708f4b75 Mon Sep 17 00:00:00 2001 From: knight Date: Sat, 17 May 2025 15:49:26 -0400 Subject: [PATCH] feat: add book metadata editor with Google Books and OpenLibrary integration --- bookHelpers.js | 8 +- books.db | Bin 753664 -> 815104 bytes index.js | 183 ++++++++++++++- populate_locations.js | 53 +++++ public/edit_books.html | 59 +++++ public/edit_books.js | 405 ++++++++++++++++++++++++++++++++++ public/google_api_tester.html | 22 ++ public/google_api_tester.js | 30 +++ public/scanner.html | 4 +- public/script.js | 137 ++++++++++-- public/style.css | 110 +++++++++ 11 files changed, 981 insertions(+), 30 deletions(-) create mode 100644 populate_locations.js create mode 100644 public/edit_books.html create mode 100644 public/edit_books.js create mode 100644 public/google_api_tester.html create mode 100644 public/google_api_tester.js create mode 100644 public/style.css diff --git a/bookHelpers.js b/bookHelpers.js index 3c0304f..c6701f8 100644 --- a/bookHelpers.js +++ b/bookHelpers.js @@ -3,7 +3,13 @@ const axios = require('axios'); // Fetch book from the local database by ISBN const fetchBookFromLocalDatabase = async (isbn) => { - return await Book.findOne({ where: { isbn } }); + return await Book.findOne({ + where: { isbn }, + include: [{ + model: Location, + as: 'Location' + }] + }); }; // Fetch book from Google Books API diff --git a/books.db b/books.db index 2ce00de73c0b8fada27744cb34a34378563daf8d..907d07cdda39918391ca3f3792632a6337c945bf 100644 GIT binary patch delta 75818 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 H0s8*{R)=V+ delta 1062 zcmXAnU1%It6vyYz+>iO#55IGS!~(| zMcf-f4L-PKN-hzR6|GxAaJ978_#i=2Da3~@_@GZNjrgL-Qhbr7#&f0v_s6;Co;l}# z&b{^S{(Aks#$bOmVp-O=u%3lQ&h9PS(a6{7XVRVOe-u2{+oq7Pa(|yvM`8D>Is$wp zo7{E(?x)lk*xS`3z_NM>SX2)J_o_1R4mAuM%|6|A&s3;(f_=Bz0rb?Jz+tr=IHa}# z$JGGvUeyoWuYBN>N&p8H1s+fYxZ@2#at83sJmBfqvM4HtQ(*3U4iF@OXd5z}v;xk^qDP&MR)7CO_{o@UD8`UG+ERQpu+D z6j>(PP5C*S9?~DBJTZT#{6m{8>&0olrVowS<7W9dKN+-W4aGMRDP~WxP6Mqr`eKeg zw0daOu~Z$m9c;Jhkr+C?t-=k@(>wvss3bx#UJS`SOc2*ZSmtL*|6*U>u&ZBV( zoqTq&cQaZ_0;0@Qolw{wvxDb7Jy3W4xfsXXHDb~Gmxr@ee8i63vF%RbTJRmn{K3|p>!NDj z)ckc { }, attributes: ['isbn', 'title', 'cover_small', 'cover_medium', 'cover_large', 'authors'], }); + // CORRECTED DEBUG LOGGING ON SERVER + if (books && books.length > 0) { + // Log a specific book if its ID is known, e.g., ID 207 for Artemis Fowl + const specificBookForDebug = books.find(b => b.id === 207); + if (specificBookForDebug) { + console.log("Server-side specific book from /api/books (ID 207):", JSON.stringify(specificBookForDebug, null, 2)); + } else { + console.log("Server-side book ID 207 not found in /api/books results, logging first book instead:", JSON.stringify(books[0], null, 2)); + } + } res.json(books); } catch (error) { console.error('Failed to fetch books with images:', error); @@ -301,23 +311,25 @@ function delay(ms) { // Confirm book is in the library by ISBN app.get('/book/confirm/:isbn', async (req, res) => { const { isbn } = req.params; - console.log(`Fetching book data for ISBN: ${isbn}`); + console.log(`Cascade: /book/confirm/:isbn - Confirming ISBN: ${isbn}`); try { // Check if the book is in the local database const localBook = await fetchBookFromLocalDatabase(isbn); if (localBook) { - console.log('Book found in the local database'); + console.log('Cascade: /book/confirm/:isbn - Book found in local database.'); return res.json({ source: 'local', data: localBook }); + } else { + console.log('Cascade: /book/confirm/:isbn - Book NOT found in local database.'); + return res.status(404).json({ error: 'Book not found in local library' }); } } catch (error) { - console.error(error); - return res.status(500).json({ error: 'Failed to fetch book data' }); + console.error('Cascade: /book/confirm/:isbn - Error during lookup:', error); + return res.status(500).json({ error: 'Failed to confirm book in local library' }); } }); - app.get('/search-title', async (req, res) => { const { title, internalOnly = false } = req.query; console.log(`Searching for books by title or related fields: ${title}`); @@ -393,9 +405,14 @@ app.delete('/book/:id', async (req, res) => { } }); -app.get('/locations', async (req, res) => { +app.get('/api/locations', async (req, res) => { try { - const locations = await Location.findAll(); + const locations = await Location.findAll({ + order: [ + ['name', 'ASC'], + ['shelf', 'ASC'] + ] + }); res.json(locations); } catch (error) { console.error('Failed to fetch locations:', error); @@ -657,6 +674,156 @@ function requestCheckout(isbn) { }); } +// API endpoint to get all books +app.get('/api/books', async (req, res) => { + try { + const books = await Book.findAll({ + include: [{ + model: Location, + as: 'Location' // Corrected alias based on model definition + }], + order: [['title', 'ASC']] + }); + res.json(books.map(book => ({ + id: book.id, + title: book.title, + authors: book.authors, + isbn: book.isbn, + publishedDate: book.publishedDate, + description: book.description, + cover_small: book.cover_small, + number_of_pages: book.number_of_pages, + publishers: book.publishers, + subjects: book.subjects, + location_id: book.location_id, + Location: book.Location ? { id: book.Location.id, name: book.Location.name, room: book.Location.room, shelf: book.Location.shelf } : null + }))); + } catch (error) { + console.error('Failed to fetch books:', error); + res.status(500).json({ error: 'Failed to fetch books' }); + } +}); + +// API endpoint to add a new book +app.post('/api/books', async (req, res) => { + try { + const { + title, + authors, + publishedDate, + description, + isbn, + number_of_pages, + publishers, + subjects, + cover_small, + cover_medium, + cover_large, + location_id + } = req.body; + + // Basic validation (e.g., title is required) + if (!title) { + return res.status(400).json({ error: 'Title is required' }); + } + + const newBookData = { + title, + authors: Array.isArray(authors) ? authors.join(', ') : authors, + publishedDate, + description, + isbn, + number_of_pages, + publishers: Array.isArray(publishers) ? publishers.join(', ') : publishers, + subjects: Array.isArray(subjects) ? subjects.join(', ') : subjects, + cover_small, + cover_medium, + cover_large, + location_id: location_id === '' ? null : location_id // Handle empty string as null + }; + + const book = await Book.create(newBookData); + + // Fetch the newly created book with its location to send back in the response + const createdBookWithLocation = await Book.findByPk(book.id, { + include: [{ + model: Location, + as: 'Location' + }] + }); + + res.status(201).json({ success: true, book: createdBookWithLocation }); + } catch (error) { + console.error('Failed to add new book:', error); + if (error.name === 'SequelizeUniqueConstraintError') { + let errorMessage = 'Failed to add new book due to a conflict.'; + // Assuming 'isbn' is a unique field in your Book model + if (error.fields && typeof error.fields === 'object' && 'isbn' in error.fields) { + errorMessage = 'A book with this ISBN already exists. Please use the edit feature if you want to update it.'; + } + return res.status(409).json({ error: errorMessage }); + } + res.status(500).json({ error: 'Failed to add new book' }); + } +}); + +// API endpoint to update a book's metadata by its primary key ID +app.put('/api/books/:id', async (req, res) => { + try { + const bookId = req.params.id; + const book = await Book.findByPk(bookId); + + if (!book) { + return res.status(404).json({ error: 'Book not found' }); + } + + const { + title, + authors, + publishedDate, + description, + isbn, + number_of_pages, + publishers, + subjects, + cover_small, + cover_medium, + cover_large, + location_id // Added to handle location updates + } = req.body; + + const updateData = {}; + if (title !== undefined) updateData.title = title; + if (authors !== undefined) updateData.authors = Array.isArray(authors) ? authors.join(', ') : authors; + if (publishedDate !== undefined) updateData.publishedDate = publishedDate; + if (description !== undefined) updateData.description = description; + if (isbn !== undefined) updateData.isbn = isbn === '' ? null : isbn; + if (number_of_pages !== undefined) updateData.number_of_pages = number_of_pages; + if (publishers !== undefined) updateData.publishers = Array.isArray(publishers) ? publishers.join(', ') : publishers; + if (subjects !== undefined) updateData.subjects = Array.isArray(subjects) ? subjects.join(', ') : subjects; + if (cover_small !== undefined) updateData.cover_small = cover_small; + if (cover_medium !== undefined) updateData.cover_medium = cover_medium; + if (cover_large !== undefined) updateData.cover_large = cover_large; + if (location_id !== undefined) updateData.location_id = location_id === '' ? null : location_id; // Handle empty string as null + + await book.update(updateData); + console.log(`Book data for ID ${bookId} updated successfully.`); + + // Fetch the updated book with its location to send back in the response + const updatedBookWithLocation = await Book.findByPk(bookId, { + include: [{ + model: Location, + as: 'Location' + }] + }); + res.json({ success: true, book: updatedBookWithLocation }); + + } catch (error) { + console.error(`Failed to update book with ID ${req.params.id}:`, error); + res.status(500).json({ error: 'Failed to update book' }); + } +}); + const httpsServer = https.createServer(credentials, app); @@ -698,7 +865,7 @@ app.get('/api/books-on-loan', async (req, res) => { } }); -app.post('/book/update/:isbn', async (req, res) => { +app.post('/book/update/:isbn', authMiddleware, async (req, res) => { // Added authMiddleware for consistency const { isbn } = req.params; const googleBooksApiKey = process.env.GOOGLE_BOOKS_API_KEY; diff --git a/populate_locations.js b/populate_locations.js new file mode 100644 index 0000000..eae6099 --- /dev/null +++ b/populate_locations.js @@ -0,0 +1,53 @@ + +import { sequelize, Location } from './models.js'; + +const locationsToCreate = []; + +// Cubes 1-10 +for (let i = 1; i <= 10; i++) { + locationsToCreate.push({ name: 'Cube', shelf: i.toString() }); +} + +// Office Shelves 1-6 +for (let i = 1; i <= 6; i++) { + locationsToCreate.push({ name: 'Office Shelf', shelf: i.toString() }); +} + +// Miscellaneous placements +locationsToCreate.push({ name: 'Upstairs', shelf: 'Misc' }); +locationsToCreate.push({ name: 'Downstairs', shelf: 'Misc' }); + +async function populateLocations() { + try { + // Optional: Sync database to ensure tables are created + await sequelize.sync(); // Often done in your main app setup + + // Check if locations already exist to avoid duplicates if script is run multiple times + // This is a simple check; more robust checks might be needed for production + const existingLocations = await Location.findAll(); + const newLocations = locationsToCreate.filter(ltc => + !existingLocations.some(el => el.name === ltc.name && el.shelf === ltc.shelf) + ); + + if (newLocations.length > 0) { + await Location.bulkCreate(newLocations); + console.log(`${newLocations.length} locations have been successfully added.`); + } else { + console.log('All specified locations already exist in the database. No new locations added.'); + } + + // Select all locations from the database + const [locationsFromQuery] = await sequelize.query('SELECT * FROM locations'); // sequelize.query returns [results, metadata] + console.log('All locations:', locationsFromQuery); + } catch (error) { + console.error('Error populating locations:', error); + } finally { + // Close the database connection if the script is standalone and not part of a larger app run + if (sequelize && typeof sequelize.close === 'function') { + await sequelize.close(); + console.log('Database connection closed.'); + } + } +} + +populateLocations(); diff --git a/public/edit_books.html b/public/edit_books.html new file mode 100644 index 0000000..dd2a86c --- /dev/null +++ b/public/edit_books.html @@ -0,0 +1,59 @@ + + + + + + Edit Book Metadata + + +

Edit Book Metadata

+ + +
+ +
+ + + + + + + diff --git a/public/edit_books.js b/public/edit_books.js new file mode 100644 index 0000000..8847a5b --- /dev/null +++ b/public/edit_books.js @@ -0,0 +1,405 @@ +let allLocations = []; // To store fetched locations globally - ADDED HERE +document.addEventListener('DOMContentLoaded', async () => { + await fetchLocations(); // Fetch locations on page load + const bookListContainer = document.getElementById('book-list-container'); + const modal = document.getElementById('edit-modal'); + const closeModalButton = modal.querySelector('.close-button'); + const modalBookTitle = document.getElementById('modal-book-title'); + const modalBookIdInput = document.getElementById('modal-book-id'); + + const modalSearchQueryInput = document.getElementById('modal-search-query'); + const modalSearchButton = document.getElementById('modal-search-button'); + const modalSearchResultsContainer = document.getElementById('modal-search-results'); + + const modalEditLocationSelect = document.getElementById('modal-edit-location'); + const modalEditFields = { + title: document.getElementById('modal-edit-title'), + authors: document.getElementById('modal-edit-authors'), + publishedDate: document.getElementById('modal-edit-publishedDate'), + isbn: document.getElementById('modal-edit-isbn'), + description: document.getElementById('modal-edit-description'), + pages: document.getElementById('modal-edit-pages'), + publishers: document.getElementById('modal-edit-publishers'), + subjects: document.getElementById('modal-edit-subjects'), + cover_small: document.getElementById('modal-edit-cover-small'), + cover_medium: document.getElementById('modal-edit-cover-medium') + }; + const modalSaveButton = document.getElementById('modal-save-button'); + + let currentBooks = []; // To store the fetched books globally + // let allLocations = []; // To store fetched locations globally - REMOVED FROM HERE + let currentlyEditingBookId = null; + + const addNewBookButton = document.getElementById('add-new-book-button'); + + addNewBookButton.addEventListener('click', () => { + currentlyEditingBookId = null; // Signal "add" mode + modalBookIdInput.value = ''; // Clear hidden book ID input + modalBookTitle.textContent = 'Add New Book'; // Update modal title for adding + + // Clear all form fields + Object.values(modalEditFields).forEach(field => field.value = ''); + modalEditLocationSelect.value = ''; // Reset location dropdown + modalSearchResultsContainer.innerHTML = ''; // Clear any previous search results + document.getElementById('modal-search-query').value = ''; // Clear search query too + + openModal(); // Open the modal for new entry + }); + + // Authentication has been disabled for book updates + + // --- Fetch and Display Books --- + async function fetchAndDisplayBooks() { + try { + const response = await fetch('/api/books'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + currentBooks = await response.json(); + renderBookList(currentBooks); + } catch (error) { + console.error('Error fetching books:', error); + bookListContainer.innerHTML = '

Error loading books. See console for details.

'; + } + } + + function renderBookList(books) { + bookListContainer.innerHTML = ''; // Clear existing list + if (books.length === 0) { + bookListContainer.innerHTML = '

No books found in the library.

'; + return; + } + + const ul = document.createElement('ul'); + ul.className = 'book-list'; // Add a class for styling if needed + books.forEach(book => { + const li = document.createElement('li'); + const locationName = book.Location ? `${book.Location.name}${book.Location.shelf ? ' (Shelf: ' + book.Location.shelf + ')' : ''}` : 'No Location'; // Reverted to book.Location (capital L) + li.innerHTML = ` +
+ Cover for ${book.title || 'N/A'} +
+

Title: ${book.title || 'N/A'}

+

Author(s): ${book.authors || 'N/A'}

+

Published: ${book.publishedDate || 'N/A'}

+

ISBN: ${book.isbn || 'N/A'}

+

Publisher(s): ${book.publishers || 'N/A'}

+

Location: ${locationName}

+
+ +
+ `; + ul.appendChild(li); + }); + bookListContainer.appendChild(ul); + + // Add event listeners to new edit buttons + document.querySelectorAll('.edit-metadata-button').forEach(button => { + button.addEventListener('click', handleEditMetadataClick); + }); + } + + // --- Modal Logic --- + function openModal() { + modal.style.display = 'block'; + } + + function closeModal() { + modal.style.display = 'none'; + modalSearchResultsContainer.innerHTML = ''; // Clear search results + // Clear form fields + Object.values(modalEditFields).forEach(input => input.value = ''); + modalSearchQueryInput.value = ''; + } + + closeModalButton.onclick = closeModal; + window.onclick = (event) => { + if (event.target == modal) { + closeModal(); + } + }; + + async function handleEditMetadataClick(event) { + event.preventDefault(); // Prevent default action + const bookId = event.target.dataset.bookId; + const bookToEdit = currentBooks.find(b => b.id.toString() === bookId); + + if (!bookToEdit) { + alert('Book not found!'); + return; + } + + modalBookTitle.textContent = bookToEdit.title; + modalBookIdInput.value = bookToEdit.id; + currentlyEditingBookId = bookToEdit.id; // Ensure this is set for edit mode + + // Pre-fill search query and existing data + modalSearchQueryInput.value = bookToEdit.title || ''; + if (bookToEdit.isbn) { + modalSearchQueryInput.value += ` ISBN: ${bookToEdit.isbn}`; + } + + modalEditFields.title.value = bookToEdit.title || ''; + modalEditFields.authors.value = bookToEdit.authors || ''; + modalEditFields.publishedDate.value = bookToEdit.publishedDate || ''; + modalEditFields.isbn.value = bookToEdit.isbn || ''; + modalEditFields.description.value = bookToEdit.description || ''; + modalEditFields.pages.value = bookToEdit.number_of_pages || ''; + modalEditFields.publishers.value = bookToEdit.publishers || ''; + modalEditFields.subjects.value = bookToEdit.subjects || ''; + modalEditFields.cover_small.value = bookToEdit.cover_small || ''; + modalEditFields.cover_medium.value = bookToEdit.cover_medium || ''; + + // Populate and set location dropdown + modalEditLocationSelect.innerHTML = ''; // Clear existing options but keep default + allLocations.forEach(loc => { + const option = document.createElement('option'); + option.value = loc.id; + option.textContent = `${loc.name}` + (loc.shelf ? ` (Shelf: ${loc.shelf})` : ''); + modalEditLocationSelect.appendChild(option); + }); + modalEditLocationSelect.value = bookToEdit.location_id || (bookToEdit.Location ? bookToEdit.Location.id : ''); + + openModal(); + } + + // --- Google Books API Search in Modal --- + modalSearchButton.addEventListener('click', handleModalSearch); + + async function handleModalSearch() { + let originalQuery = modalSearchQueryInput.value.trim(); + if (!originalQuery) { + alert('Please enter a search query.'); + return; + } + + let searchQuery = originalQuery; + const potentialIsbn = originalQuery.replace(/-/g, ''); + let isIsbnQuery = false; + + if (/^(\d{9}[\dX]|\d{13})$/.test(potentialIsbn)) { + searchQuery = potentialIsbn; // Use the cleaned ISBN for OpenLibrary + isIsbnQuery = true; + console.log(`Detected ISBN-like string "${originalQuery}", searching as ISBN "${searchQuery}"`); + } else if (originalQuery.toLowerCase().startsWith('isbn:')) { + searchQuery = originalQuery.substring(5).replace(/-/g, ''); // Remove 'isbn:' and hyphens + isIsbnQuery = true; + console.log(`Query "${originalQuery}" formatted for ISBN search, using "${searchQuery}"`); + } else { + console.log(`Performing general search for "${originalQuery}"`); + } + + modalSearchResultsContainer.innerHTML = '

Searching...

'; + const searchButton = document.getElementById('modal-search-button'); + if (searchButton) searchButton.disabled = true; + + let combinedResults = []; + + try { + const googleQuery = isIsbnQuery ? `isbn:${searchQuery}` : searchQuery; + const googleUrl = `https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(googleQuery)}&maxResults=5`; + const openLibFields = 'key,title,author_name,first_publish_year,isbn,cover_i,publisher,subject,number_of_pages_median,subtitle,first_sentence_value'; + const openLibUrl = `https://openlibrary.org/search.json?q=${encodeURIComponent(searchQuery)}&limit=5&fields=${openLibFields}`; + + console.log("Searching Google Books API with URL:", googleUrl); + console.log("Searching OpenLibrary API with URL:", openLibUrl); + + const [googleResult, openLibResult] = await Promise.allSettled([ + fetch(googleUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`Google Books API request failed: ${res.status} ${res.statusText}`))), + fetch(openLibUrl).then(res => res.ok ? res.json() : Promise.reject(new Error(`OpenLibrary API request failed: ${res.status} ${res.statusText}`))) + ]); + + if (googleResult.status === 'fulfilled' && googleResult.value.items) { + const googleBooks = googleResult.value.items.map(item => { + const volumeInfo = item.volumeInfo; + let isbn13 = '', isbn10 = ''; + if (volumeInfo.industryIdentifiers) { + volumeInfo.industryIdentifiers.forEach(id => { + if (id.type === 'ISBN_13') isbn13 = id.identifier; + if (id.type === 'ISBN_10') isbn10 = id.identifier; + }); + } + return { + title: volumeInfo.title, + authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : '', + publishedDate: volumeInfo.publishedDate, + isbn: isbn13 || isbn10, + description: volumeInfo.description, + pageCount: volumeInfo.pageCount, + publishers: volumeInfo.publisher ? (Array.isArray(volumeInfo.publisher) ? volumeInfo.publisher.join(', ') : volumeInfo.publisher) : '', + subjects: volumeInfo.categories ? volumeInfo.categories.join(', ') : '', + cover_small: volumeInfo.imageLinks?.smallThumbnail, + cover_medium: volumeInfo.imageLinks?.thumbnail, + source: 'Google Books', + rawData: volumeInfo + }; + }); + combinedResults.push(...googleBooks); + } else if (googleResult.status === 'rejected') { + console.error('Error fetching from Google Books:', googleResult.reason); + } + + if (openLibResult.status === 'fulfilled' && openLibResult.value.docs) { + const openLibBooks = openLibResult.value.docs.map(doc => { + return { + title: doc.title, + authors: doc.author_name ? doc.author_name.join(', ') : '', + publishedDate: doc.first_publish_year ? String(doc.first_publish_year) : '', + isbn: doc.isbn ? doc.isbn[0] : '', // Take first ISBN + description: doc.first_sentence_value || doc.subtitle || '', + pageCount: doc.number_of_pages_median, + publishers: doc.publisher ? doc.publisher.join(', ') : '', + subjects: doc.subject ? doc.subject.join(', ') : '', + cover_small: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-S.jpg` : '', + cover_medium: doc.cover_i ? `https://covers.openlibrary.org/b/id/${doc.cover_i}-M.jpg` : '', + source: 'OpenLibrary', + rawData: doc + }; + }); + combinedResults.push(...openLibBooks); + } + else if (openLibResult.status === 'rejected') { + console.error('Error fetching from OpenLibrary:', openLibResult.reason); + } + + displayModalSearchResults(combinedResults); + + } catch (error) { // Catch any unexpected errors during the setup or Promise.allSettled itself + console.error('Error in handleModalSearch:', error); + modalSearchResultsContainer.innerHTML = `

An unexpected error occurred during search: ${error.message}. Please check console.

`; + } finally { + if (searchButton) searchButton.disabled = false; + } + } + + function displayModalSearchResults(items) { + modalSearchResultsContainer.innerHTML = ''; + if (!items || items.length === 0) { + modalSearchResultsContainer.innerHTML = '

No results found from any source.

'; + return; + } + + const ul = document.createElement('ul'); + items.forEach(bookItem => { + const li = document.createElement('li'); + li.innerHTML = ` + ${bookItem.title || 'N/A'} by ${bookItem.authors || 'N/A'} (${bookItem.source}) +
+ Published: ${bookItem.publishedDate || 'N/A'}, ISBN: ${bookItem.isbn || 'N/A'} + + `; + li.querySelector('.select-book-result').addEventListener('click', () => { + populateFormWithSelectedBook(bookItem); + }); + ul.appendChild(li); + }); + modalSearchResultsContainer.appendChild(ul); + } + + function populateFormWithSelectedBook(bookItem) { + modalEditFields.title.value = bookItem.title || ''; + modalEditFields.authors.value = bookItem.authors || ''; + modalEditFields.publishedDate.value = bookItem.publishedDate || ''; + modalEditFields.isbn.value = bookItem.isbn || ''; + modalEditFields.description.value = bookItem.description || ''; + modalEditFields.pages.value = bookItem.pageCount || ''; + modalEditFields.publishers.value = bookItem.publishers || ''; + modalEditFields.subjects.value = bookItem.subjects || ''; + modalEditFields.cover_small.value = bookItem.cover_small || ''; + modalEditFields.cover_medium.value = bookItem.cover_medium || ''; + } + + // --- Save Changes --- + modalSaveButton.addEventListener('click', async () => { + const bookData = { + title: modalEditFields.title.value.trim(), + authors: modalEditFields.authors.value.trim(), + publishedDate: modalEditFields.publishedDate.value.trim(), + isbn: modalEditFields.isbn.value.trim(), + description: modalEditFields.description.value.trim(), + number_of_pages: modalEditFields.pages.value ? parseInt(modalEditFields.pages.value, 10) : null, + publishers: modalEditFields.publishers.value.trim(), + subjects: modalEditFields.subjects.value.trim(), + cover_small: modalEditFields.cover_small.value.trim(), + cover_medium: modalEditFields.cover_medium.value.trim(), + location_id: modalEditLocationSelect.value || null, + }; + + if (!bookData.title) { + alert('Title is required.'); + return; + } + + let url; + let method; + const headers = { + 'Content-Type': 'application/json' + }; + + if (currentlyEditingBookId) { // Edit mode + url = `/api/books/${currentlyEditingBookId}`; + method = 'PUT'; + // Basic Auth not currently implemented for PUT as per prior setup + } else { // Add mode + url = '/api/books'; + method = 'POST'; + // POST endpoint requires Basic Auth + const adminPassword = prompt("Enter admin password to add a new book:"); + if (adminPassword === null) { // User cancelled prompt + return; + } + headers['Authorization'] = 'Basic ' + btoa('admin:' + adminPassword); + } + + try { + const response = await fetch(url, { + method: method, + headers: headers, + body: JSON.stringify(bookData) + }); + + const responseBody = await response.text(); // Read body as text first to handle potential non-JSON errors + let result; + try { + result = JSON.parse(responseBody); + } catch (e) { + // If parsing fails, the response was not valid JSON + console.error('Failed to parse server response as JSON:', responseBody); + throw new Error(`Server returned non-JSON response (status: ${response.status}). Check console for details.`); + } + + if (!response.ok) { + const errorMessage = result && result.error ? result.error : `HTTP error! Status: ${response.status}`; + throw new Error(errorMessage); + } + + if (result.success) { + alert(`Book ${method === 'POST' ? 'added' : 'updated'} successfully!`); + closeModal(); + fetchAndDisplayBooks(); // Refresh the list + } else { + alert(`Failed to ${method === 'POST' ? 'add' : 'update'} book: ${result.error || 'Unknown server error'}`); + } + } catch (error) { + console.error(`Error ${method === 'POST' ? 'adding' : 'saving'} book data:`, error); + alert(`Error ${method === 'POST' ? 'adding' : 'saving'}: ${error.message}`); + } + }); + + // Initial load + fetchAndDisplayBooks(); +}); + +async function fetchLocations() { + try { + const response = await fetch('/api/locations'); + if (!response.ok) { + throw new Error(`Failed to fetch locations: ${response.status}`); + } + allLocations = await response.json(); + console.log('Locations fetched:', allLocations); + } catch (error) { + console.error('Error fetching locations:', error); + // Optionally, inform the user that locations could not be loaded + } +} diff --git a/public/google_api_tester.html b/public/google_api_tester.html new file mode 100644 index 0000000..9a48932 --- /dev/null +++ b/public/google_api_tester.html @@ -0,0 +1,22 @@ + + + + + + Google Books API Tester + + +

Google Books API Tester

+ +
+ + + +
+ +

API Response:

+

+
+    
+
+
diff --git a/public/google_api_tester.js b/public/google_api_tester.js
new file mode 100644
index 0000000..289d6fb
--- /dev/null
+++ b/public/google_api_tester.js
@@ -0,0 +1,30 @@
+document.addEventListener('DOMContentLoaded', () => {
+    const searchButton = document.getElementById('search-button');
+    const searchQueryInput = document.getElementById('search-query');
+    const apiResponseOutput = document.getElementById('api-response');
+
+    searchButton.addEventListener('click', async () => {
+        const query = searchQueryInput.value.trim();
+        if (!query) {
+            apiResponseOutput.textContent = 'Please enter a search query.';
+            return;
+        }
+
+        apiResponseOutput.textContent = 'Searching...';
+
+        try {
+            // Note: For extensive use, you should use an API key.
+            // Add &key=YOUR_API_KEY to the URL if you have one.
+            const response = await fetch(`https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(query)}`);
+            if (!response.ok) {
+                const errorText = await response.text();
+                throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
+            }
+            const data = await response.json();
+            apiResponseOutput.textContent = JSON.stringify(data, null, 2);
+        } catch (error) {
+            console.error('Error fetching from Google Books API:', error);
+            apiResponseOutput.textContent = `Error: ${error.message}`;
+        }
+    });
+});
diff --git a/public/scanner.html b/public/scanner.html
index 42397bb..6e45f34 100644
--- a/public/scanner.html
+++ b/public/scanner.html
@@ -13,7 +13,9 @@
         
         
         
-        s
+        
+        
+        
     
     
diff --git a/public/script.js b/public/script.js index 62b302d..a8beb17 100644 --- a/public/script.js +++ b/public/script.js @@ -50,9 +50,8 @@ function startScanner() { return; } - if (quaggaInitialized) { - Quagga.start(); - } else { + const initLogic = () => { + console.log("startScanner/initLogic: Proceeding with Quagga.init()."); Quagga.init({ inputStream: { name: "Live", @@ -61,34 +60,93 @@ function startScanner() { constraints: { deviceId: selectedDeviceId, facingMode: "environment", - // Remove advanced constraints if not needed + advanced: [{torch: document.getElementById('flash-toggle') ? document.getElementById('flash-toggle').checked : false}] }, }, decoder: { readers: ["ean_reader", "ean_8_reader"] } - }, function (err) { + }, function(err) { if (err) { - console.log(err); + console.error("Quagga.init() callback: Failed with error:", err); + quaggaInitialized = false; + alert('Error initializing scanner: ' + err.name + ' - ' + err.message); return; } - console.log("Initialization finished. Ready to start"); + console.log("Quagga.init() callback: Successful. Starting stream..."); Quagga.start(); + console.log("Quagga.init() callback: Quagga.start() called."); quaggaInitialized = true; - - // Set up the onDetected handler + Quagga.offDetected(processBarcode); // Remove before adding to prevent duplicates Quagga.onDetected(processBarcode); + console.log("Cascade: startScanner/initLogic - Quagga.onDetected(processBarcode) RE-ATTACHED."); + console.log("Quagga.init() callback: Stream started. onDetected handler active."); + + // Explicitly re-apply flash if toggle is on, after stream has started + const flashToggleEl = document.getElementById('flash-toggle'); + if (flashToggleEl && flashToggleEl.checked) { + console.log("Cascade: startScanner/initLogic - Flash toggle is checked. Attempting to re-apply torch constraint post-start."); + if (Quagga.CameraAccess && Quagga.CameraAccess.getActiveTrack()) { + const track = Quagga.CameraAccess.getActiveTrack(); + track.applyConstraints({ advanced: [{ torch: true }] }) + .then(() => { + console.log("Cascade: startScanner/initLogic - Torch constraint explicitly re-applied successfully post-start."); + }) + .catch(constraintErr => { + console.error("Cascade: startScanner/initLogic - Error explicitly re-applying torch constraint post-start:", constraintErr); + }); + } else { + console.warn("Cascade: startScanner/initLogic - Flash toggle checked, but cannot get active track to re-apply torch constraint post-start."); + } + } }); + }; + + if (quaggaInitialized) { + console.log("startScanner: Restart requested. Stopping current Quagga instance."); + try { + Quagga.stop(); + console.log("startScanner: Quagga.stop() called successfully during restart."); + } catch (e) { + console.warn("startScanner: Error during Quagga.stop(), proceeding with re-init attempt:", e); + } + quaggaInitialized = false; // Force re-init + + console.log("startScanner: Delaying for 100ms before re-initializing Quagga for restart."); + setTimeout(() => { + console.log("startScanner: Delay finished. Calling initLogic for restart."); + initLogic(); + }, 100); // 100ms delay + } else { + console.log("startScanner: Initial Quagga setup or forced re-init without prior true state."); + initLogic(); } } async function processBarcode(data) { - if (isScanning) return; // Prevent further scans while a scan is being processed + console.log("Cascade: processBarcode - Entered. Current isScanning state:", isScanning); + if (isScanning) { + console.log("Cascade: processBarcode - Exiting early because isScanning is true."); + return; // Prevent further scans while a scan is being processed + } isScanning = true; // Set the scanning flag + console.log("Cascade: processBarcode - Set isScanning to true."); const isbn = data.codeResult.code; - console.log("Detected ISBN:", isbn); - Quagga.stop(); // Stop the scanner once an ISBN is detected + console.log("Detected code:", isbn); + + // Validate ISBN length (10 or 13 digits) + if (!isbn || (isbn.length !== 10 && isbn.length !== 13)) { + console.log(`Cascade: processBarcode - Invalid code length: ${isbn.length}. Code: ${isbn}. Resuming scan.`); + // isScanning = false; // Reset isScanning to allow immediate re-scan by Quagga's internal loop if needed + // No need to explicitly set isScanning to false here if we don't stop Quagga. + // The initial check `if (isScanning)` at the top of processBarcode should handle concurrent calls. + // We want Quagga to continue, so we don't call Quagga.stop() or startScanner() here. + return; // Continue scanning + } + + console.log("Validated ISBN:", isbn); + Quagga.stop(); // Stop the scanner once a VALID ISBN is detected if (document.getElementById('confirm-mode').checked) { // Confirm book in library @@ -110,20 +168,25 @@ async function fetchBookInfo(isbn) { bookData.isbn2 = isbn; // Add the ISBN to the book data promptUserWithBook(bookData); } else { - console.log("No book data found. Restarting scanner..."); + console.log("Cascade: fetchBookInfo - No book data. Resetting isScanning."); + isScanning = false; // Reset flag BEFORE restarting scanner + console.log("Cascade: fetchBookInfo - isScanning is now false. Calling startScanner..."); startScanner(); // Restart the scanner if no book information is found } } catch (error) { console.error('Error fetching book data:', error); + console.log("Cascade: fetchBookInfo - Error fetching. Resetting isScanning."); + isScanning = false; // Reset flag BEFORE restarting scanner + console.log("Cascade: fetchBookInfo - isScanning is now false (error path). Calling startScanner..."); startScanner(); // Restart the scanner on error as well } } function promptUserWithBook(bookData) { // Prepare the information to display in the confirm dialog - const title = bookData.title; - const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author'; - const description = bookData.description || 'No description available'; + const title = bookData.data.title; + const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author'; + const description = bookData.data.description || 'No description available'; // Combine the information into a single message const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nDo you want to add this book to the database?`; @@ -184,12 +247,13 @@ async function confirmBookInLibrary(isbn) { try { const response = await fetch(`/book/confirm/${isbn}`); const bookData = await response.json(); + console.log("Cascade: confirmBookInLibrary - Response from /book/confirm:", bookData); - if (bookData.title) { + if (bookData.data && bookData.data.title) { // Display the book information and a success message - const title = bookData.title; - const authors = bookData.authors ? bookData.authors.join(', ') : 'Unknown Author'; - const description = bookData.description || 'No description available'; + const title = bookData.data.title; + const authors = bookData.data.authors ? (Array.isArray(bookData.data.authors) ? bookData.data.authors.join(', ') : bookData.data.authors) : 'Unknown Author'; + const description = bookData.data.description || 'No description available'; const message = `Title: ${title}\nAuthor(s): ${authors}\nDescription: ${description}\n\nBook found in the library!`; @@ -201,6 +265,9 @@ async function confirmBookInLibrary(isbn) { console.error('Error confirming book in library:', error); alert('An error occurred while confirming the book in the library.'); } finally { + console.log("Cascade: confirmBookInLibrary - finally block. Resetting isScanning."); + isScanning = false; // Reset flag BEFORE restarting scanner + console.log("Cascade: confirmBookInLibrary - isScanning is now false. Calling startScanner..."); startScanner(); // Restart the scanner after processing } } @@ -295,4 +362,34 @@ function selectBook(index) { window.onload = function() { getCameras(); searchByTitle(''); + + const flashToggle = document.getElementById('flash-toggle'); + if (flashToggle) { + flashToggle.addEventListener('change', function() { + if (quaggaInitialized && Quagga.CameraAccess && Quagga.CameraAccess.getActiveTrack()) { + const track = Quagga.CameraAccess.getActiveTrack(); + const constraints = { advanced: [{ torch: this.checked }] }; + track.applyConstraints(constraints) + .then(() => { + console.log("Flash toggled via applyConstraints:", this.checked); + }) + .catch(err => { + console.error("Error applying flash constraint dynamically:", err, ". Falling back to scanner restart."); + // Fallback: restart scanner + if (quaggaInitialized) { + Quagga.stop(); + quaggaInitialized = false; + startScanner(); // This will pick up the new toggle state from the checkbox + } + }); + } else if (quaggaInitialized) { + // If getActiveTrack is not available or Quagga is initialized but track isn't, restart scanner. + console.log("Flash toggled, getActiveTrack not available or other issue, restarting scanner for change to take effect."); + Quagga.stop(); + quaggaInitialized = false; // Force re-init + startScanner(); // This will pick up the new toggle state + } + // If Quagga is not yet initialized, startScanner() will pick up the state when it's called. + }); + } }; diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..a9203c9 --- /dev/null +++ b/public/style.css @@ -0,0 +1,110 @@ +/* Basic Modal Styling */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1000; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgba(0,0,0,0.4); /* Black w/ opacity (backdrop) */ +} + +.modal-content { + background-color: #fefefe; + margin: 10% auto; /* 10% from the top and centered horizontally */ + padding: 20px; + border: 1px solid #888; + width: 80%; /* Could be more or less, depending on screen size */ + max-width: 700px; /* Max width for larger screens */ + border-radius: 8px; + position: relative; /* For positioning the close button */ + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19); +} + +/* Close Button */ +.close-button { + color: #aaa; + float: right; /* Position to the top-right */ + font-size: 28px; + font-weight: bold; + position: absolute; /* Position relative to modal-content */ + top: 10px; + right: 20px; +} + +.close-button:hover, +.close-button:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +/* Style for the book list entries on edit_books.html for better layout */ +#book-list-container ul { + list-style-type: none; + padding: 0; +} + +#book-list-container li { + border: 1px solid #ddd; + margin-bottom: 10px; + padding: 10px; + border-radius: 5px; +} + +.book-entry { + display: flex; /* Use flexbox for alignment */ + align-items: flex-start; /* Align items to the start of the cross axis */ +} + +.book-entry img { + margin-right: 15px; + flex-shrink: 0; /* Prevent image from shrinking */ +} + +.book-entry div { + flex-grow: 1; /* Allow text content to take up remaining space */ +} + +.book-entry button { + margin-left: 10px; + align-self: center; /* Center button vertically within the flex container */ +} + +/* Ensure modal input fields are a bit more organized */ +.modal-content div { + margin-bottom: 10px; +} +.modal-content label { + display: block; + margin-bottom: 5px; +} +.modal-content input[type="text"], +.modal-content input[type="number"], +.modal-content textarea { + width: calc(100% - 22px); /* Adjust width to account for padding/border */ + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; /* Include padding and border in the element's total width and height */ +} +.modal-content textarea { + resize: vertical; + min-height: 80px; +} +.modal-content button { + padding: 10px 15px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} +.modal-content button:hover { + background-color: #0056b3; +} +#modal-search-button { + margin-left: 5px; +}