diff --git a/abogen/integrations/calibre_opds.py b/abogen/integrations/calibre_opds.py index d202282..dff4e52 100644 --- a/abogen/integrations/calibre_opds.py +++ b/abogen/integrations/calibre_opds.py @@ -320,7 +320,8 @@ class CalibreOPDSClient: mime = (link.type or "").lower() if mime in _EPUB_MIME_TYPES: return link - return iterable[0] if iterable else None + # No valid acquisition-style link exposed + return None def feed_to_dict(feed: OPDSFeed) -> Dict[str, Any]: diff --git a/abogen/web/static/find_books.js b/abogen/web/static/find_books.js index 537916a..2147652 100644 --- a/abogen/web/static/find_books.js +++ b/abogen/web/static/find_books.js @@ -12,6 +12,12 @@ if (browser) { query: '', }; + const ENTRY_TYPES = { + BOOK: 'book', + NAVIGATION: 'navigation', + OTHER: 'other', + }; + const setStatus = (message, level) => { if (!statusEl) { return; @@ -54,6 +60,36 @@ if (browser) { return authors.filter((author) => !!author).join(', '); }; + const findNavigationLink = (entry) => { + if (!entry || !Array.isArray(entry.links)) { + return null; + } + const candidates = entry.links.filter((link) => link && link.href); + return candidates.find((link) => { + const rel = (link.rel || '').toLowerCase(); + const type = (link.type || '').toLowerCase(); + if (!link.href) { + return false; + } + if (rel.includes('acquisition')) { + return false; + } + if (rel === 'self') { + return false; + } + if (type.includes('opds-catalog')) { + return true; + } + if (rel.includes('subsection') || rel.includes('collection')) { + return true; + } + if (rel.startsWith('http://opds-spec.org/sort') || rel.startsWith('http://opds-spec.org/group')) { + return true; + } + return false; + }) || null; + }; + const renderNav = (links) => { if (!navEl) { return; @@ -117,17 +153,36 @@ if (browser) { const downloadLink = entry.download && entry.download.href ? entry.download.href : null; const alternateLink = entry.alternate && entry.alternate.href ? entry.alternate.href : null; + const navigationLink = findNavigationLink(entry); + let entryType = ENTRY_TYPES.OTHER; if (downloadLink) { + entryType = ENTRY_TYPES.BOOK; + } else if (navigationLink && navigationLink.href) { + entryType = ENTRY_TYPES.NAVIGATION; + item.classList.add('opds-browser__entry--navigation'); + } + + if (entryType === ENTRY_TYPES.BOOK) { const queueButton = document.createElement('button'); queueButton.type = 'button'; queueButton.className = 'button'; queueButton.textContent = 'Queue for conversion'; queueButton.addEventListener('click', () => importEntry(entry, queueButton)); actions.appendChild(queueButton); + } else if (entryType === ENTRY_TYPES.NAVIGATION && navigationLink) { + const browseButton = document.createElement('button'); + browseButton.type = 'button'; + browseButton.className = 'button button--ghost'; + browseButton.textContent = 'Browse view'; + browseButton.addEventListener('click', () => { + clearStatus(); + loadFeed({ href: navigationLink.href, query: '' }); + }); + actions.appendChild(browseButton); } - if (alternateLink) { + if (alternateLink && entryType !== ENTRY_TYPES.NAVIGATION) { const previewLink = document.createElement('a'); previewLink.className = 'button button--ghost'; previewLink.href = alternateLink; @@ -145,12 +200,12 @@ if (browser) { } item.appendChild(actions); - return item; + return { element: item, type: entryType }; }; const renderEntries = (entries) => { if (!resultsEl) { - return; + return { [ENTRY_TYPES.BOOK]: 0, [ENTRY_TYPES.NAVIGATION]: 0, [ENTRY_TYPES.OTHER]: 0 }; } resultsEl.innerHTML = ''; if (!Array.isArray(entries) || !entries.length) { @@ -158,13 +213,21 @@ if (browser) { empty.className = 'opds-browser__empty'; empty.textContent = 'No books found in this view.'; resultsEl.appendChild(empty); - return; + return { [ENTRY_TYPES.BOOK]: 0, [ENTRY_TYPES.NAVIGATION]: 0, [ENTRY_TYPES.OTHER]: 0 }; } const fragment = document.createDocumentFragment(); + const stats = { + [ENTRY_TYPES.BOOK]: 0, + [ENTRY_TYPES.NAVIGATION]: 0, + [ENTRY_TYPES.OTHER]: 0, + }; entries.forEach((entry) => { - fragment.appendChild(createEntry(entry)); + const { element, type } = createEntry(entry); + stats[type] += 1; + fragment.appendChild(element); }); resultsEl.appendChild(fragment); + return stats; }; const importEntry = async (entry, trigger) => { @@ -220,10 +283,19 @@ if (browser) { } const feed = payload.feed || {}; state.query = query; + if (searchInput && typeof query === 'string') { + searchInput.value = query; + } renderNav(feed.links); - renderEntries(feed.entries || []); - if (Array.isArray(feed.entries) && feed.entries.length) { - setStatus(`Showing ${feed.entries.length} item${feed.entries.length === 1 ? '' : 's'}.`, 'success'); + const stats = renderEntries(feed.entries || []); + const books = stats?.[ENTRY_TYPES.BOOK] || 0; + const views = stats?.[ENTRY_TYPES.NAVIGATION] || 0; + if (books && views) { + setStatus(`Showing ${books} book${books === 1 ? '' : 's'} and ${views} catalog view${views === 1 ? '' : 's'}.`, 'success'); + } else if (books) { + setStatus(`Found ${books} book${books === 1 ? '' : 's'} in this view.`, 'success'); + } else if (views) { + setStatus(`Browse ${views} catalog view${views === 1 ? '' : 's'} to drill deeper.`, 'info'); } else { setStatus('No books found in this view.', 'info'); } diff --git a/abogen/web/templates/find_books.html b/abogen/web/templates/find_books.html index 22cafd9..73f286b 100644 --- a/abogen/web/templates/find_books.html +++ b/abogen/web/templates/find_books.html @@ -5,22 +5,52 @@ {% block content %}
Find Books
-

Browse your Calibre OPDS catalog and queue downloads directly into the conversion wizard.

- {% if not opds_available %} -
Enable the Calibre OPDS integration in settings to browse your library here.
- {% else %} -
- -
-
- +

Browse trusted public-domain libraries or drill into your Calibre catalog without leaving abogen.

+
+
+
+

Standard Ebooks

+

Discover meticulously produced public-domain ebooks with consistent typography, metadata, and clean EPUB sources.

+

+ + Visit standardebooks.org + +

+

Opens in a new tab so you can grab EPUB downloads and drag them into abogen.

+
+ +
+

Project Gutenberg

+

The world’s largest public-domain ebook library, offering thousands of EPUB and plain-text editions sourced from volunteer transcriptions.

+

+ + Visit gutenberg.org + +

+

Download the EPUB version for best results, then import it into abogen.

+
- {% endif %} + + {% if not opds_available %} +
Enable the Calibre OPDS integration in settings to browse your library here.
+ {% else %} +
+

Calibre catalog

+

Navigate Calibre shelves and queue downloadable titles directly into the conversion wizard.

+
+ +
+
+
    +
    +
    + {% endif %} +
    {% endblock %}