diff --git a/bindery/cwa.py b/bindery/cwa.py index 5070863..3c796bc 100644 --- a/bindery/cwa.py +++ b/bindery/cwa.py @@ -56,11 +56,12 @@ class Book: formats: dict[str, str] = field(default_factory=dict) # "epub" -> download href def to_dict(self) -> dict[str, Any]: + """The book in the form of get_book, with its publisher.""" return { "id": self.id, "uuid": self.uuid, "title": self.title, - "authors": " & ".join(self.authors), + "authors": self.authors, "publisher": self.publisher, "formats": sorted(self.formats), } diff --git a/bindery/orchestrator.py b/bindery/orchestrator.py index 1e5953e..a0a7782 100644 --- a/bindery/orchestrator.py +++ b/bindery/orchestrator.py @@ -620,11 +620,7 @@ def search_library(query: str) -> list[dict[str, Any]]: def library_candidates() -> list[dict[str, Any]]: """Library books from a story site that are not tracked yet.""" tracked_ids = {s["calibre_id"] for s in store.list_stories() if s["calibre_id"]} - return [ - {"calibre_id": b.id, "uuid": b.uuid, "title": b.title, "authors": " & ".join(b.authors), "site": b.publisher} - for b in cwa().site_books(fff.is_supported_site) - if b.id not in tracked_ids - ] + return [b.to_dict() for b in cwa().site_books(fff.is_supported_site) if b.id not in tracked_ids] def build_for_download(story_id: int) -> tuple[BinaryIO, str]: @@ -779,19 +775,24 @@ def _link(story: dict[str, Any], book_id: int, book_uuid: str) -> None: store.update_story(story["id"], calibre_id=book_id, calibre_uuid=book_uuid, ingested_at=story["ingested_at"]) +def _saved_book(story: dict[str, Any]) -> dict[str, Any] | None: + """The library book that the story names, without a check that CWA still has it.""" + if story["calibre_id"] is None: + return None + return {"id": story["calibre_id"], "uuid": story["calibre_uuid"], "title": story["title"], "authors": []} + + def _linked_book(story: dict[str, Any]) -> dict[str, Any] | None: """The library book for a story. Links the story if CWA has a book with its URL.""" if not _link_books(): - if story["calibre_id"] is not None: - return {"id": story["calibre_id"], "uuid": story["calibre_uuid"], "title": story["title"]} - return None + return _saved_book(story) if story["calibre_uuid"]: try: book = cwa().get_book(story["calibre_uuid"]) except CwaError as e: # CWA checks the book id itself, so an update can still go out. logger.warning(f"Cannot check library book {story['calibre_id']}: {e}") - return {"id": story["calibre_id"], "uuid": story["calibre_uuid"], "title": story["title"]} + return _saved_book(story) if book: if book["id"] != story["calibre_id"]: _link(story, book["id"], book["uuid"]) diff --git a/bindery/static/js/stories.js b/bindery/static/js/stories.js index b3c562b..af822de 100644 --- a/bindery/static/js/stories.js +++ b/bindery/static/js/stories.js @@ -50,7 +50,7 @@ function libraryImport(config) { 'th', {}, checkbox(selected.size === candidates.length, (on) => { - selected = new Set(on ? candidates.map((c) => c.calibre_id) : []); + selected = new Set(on ? candidates.map((c) => c.id) : []); render(); }, { 'aria-label': 'Select all' }), ), @@ -66,10 +66,10 @@ function libraryImport(config) { h( 'tr', {}, - h('td', {}, checkbox(selected.has(c.calibre_id), (on) => toggle(c.calibre_id, on), { 'aria-label': c.title })), + h('td', {}, checkbox(selected.has(c.id), (on) => toggle(c.id, on), { 'aria-label': c.title })), h('td', {}, c.title), - h('td', {}, c.authors), - h('td', {}, c.site), + h('td', {}, c.authors.join(' & ')), + h('td', {}, c.publisher), ), ), ), @@ -79,8 +79,8 @@ function libraryImport(config) { 'Import selected', async () => { const books = candidates - .filter((c) => selected.has(c.calibre_id)) - .map((c) => ({ id: c.calibre_id, uuid: c.uuid, title: c.title })); + .filter((c) => selected.has(c.id)) + .map(({ id, uuid, title }) => ({ id, uuid, title })); if (!books.length) return; const jobs = await api('library/import', 'POST', { books, check }); toast(`Queued ${jobs.length} imports`); @@ -106,7 +106,7 @@ function libraryImport(config) { { class: 'controls' }, button('Find stories in the library', async () => { candidates = await api('library/candidates'); - selected = new Set(candidates.map((c) => c.calibre_id)); + selected = new Set(candidates.map((c) => c.id)); render(); }), h('label', {}, checkbox(true, (on) => (check = on)), ' Check the site for new chapters after import'), diff --git a/bindery/static/js/story.js b/bindery/static/js/story.js index 30ceed7..cce2b1b 100644 --- a/bindery/static/js/story.js +++ b/bindery/static/js/story.js @@ -55,7 +55,7 @@ const editText = (key, value) => { return formatField(key, value); }; -const authorsText = (authors) => (Array.isArray(authors) ? authors.join(' & ') : authors); +const authorsText = (authors) => authors.join(' & '); // On a narrow screen, the rows stack instead of scrolling sideways. `extra` names the wrapper class // that gives a table's stacked rows their own layout, such as picking out the row's first cell. diff --git a/tests/js/flows.test.mjs b/tests/js/flows.test.mjs index 6b1bda0..adef628 100644 --- a/tests/js/flows.test.mjs +++ b/tests/js/flows.test.mjs @@ -188,7 +188,7 @@ test('library import sends only selected books and handles an empty candidate li api.on('GET', 'library/candidates', []); await click(main, 'Find stories in the library'); assert.match(main.textContent, /tracked already/); - const books = [{ calibre_id: 8, uuid: 'a', title: 'A' }, { calibre_id: 9, uuid: 'b', title: 'B' }]; + const books = [{ id: 8, uuid: 'a', title: 'A', authors: ['W'] }, { id: 9, uuid: 'b', title: 'B', authors: [] }]; api.on('GET', 'library/candidates', books); await click(main, 'Find stories in the library'); change(field(main, 'Select all'), false); @@ -479,7 +479,7 @@ test('library search trims the query and links by UUID', async () => { await settle(); assert.match(main.textContent, /No library books match/); api.on('GET', 'library/search?q=A%20%26%20B', [{ id: 7, uuid: 'book-7', title: 'A & B', authors: ['A', 'B'] }, - { id: 8, uuid: 'book-8', title: 'Tracked', authors: 'Writer', tracked_as: 'Other' }]); + { id: 8, uuid: 'book-8', title: 'Tracked', authors: ['Writer'], tracked_as: 'Other' }]); emit(form, 'submit'); await settle(); const links = nodes(main).filter((el) => el.tagName === 'button' && el.textContent === 'Link'); diff --git a/tests/test_api_flows.py b/tests/test_api_flows.py index 15800a3..64ec00e 100644 --- a/tests/test_api_flows.py +++ b/tests/test_api_flows.py @@ -91,7 +91,7 @@ def test_library_search_link_import_and_unlink(client, story, monkeypatch): cwa.get_book.return_value = book.to_dict() monkeypatch.setattr(o, "cwa", lambda: cwa) monkeypatch.setattr(config, "CWA_URL", "https://cwa.example") - assert client.get("/api/library/candidates").json[0]["calibre_id"] == 7 + assert client.get("/api/library/candidates").json[0]["id"] == 7 assert client.get("/api/library/search?q=Title").json[0]["tracked_as"] is None cwa.search.assert_called_once_with("Title") linked = client.post(f"/api/stories/{sid}/link", json={"uuid": "book-7"}, headers=H) diff --git a/tests/test_deliver.py b/tests/test_deliver.py index 003d565..b4016d8 100644 --- a/tests/test_deliver.py +++ b/tests/test_deliver.py @@ -142,7 +142,14 @@ def test_import_from_the_library(fake): def test_library_candidates_and_search(fake): candidates = orchestrator.library_candidates() assert candidates == [ - {"calibre_id": 1, "uuid": "uuid-1", "title": "Test Story", "authors": "Writer", "site": "www.royalroad.com"} + { + "id": 1, + "uuid": "uuid-1", + "title": "Test Story", + "authors": ["Writer"], + "publisher": "www.royalroad.com", + "formats": ["epub"], + } ] story_id = add_story() orchestrator.link_story(story_id, "uuid-1")