AuthorBot Cursor commited on
Commit
69ed736
·
1 Parent(s): 506f7f7

fix: show URL-imported book covers in chatbot book picker

Browse files
app/api/chat.py CHANGED
@@ -39,6 +39,7 @@ from app.services.token_budget import check_and_warn_budget
39
  from app.repositories.book_repo import BookRepository
40
  from app.repositories.link_repo import LinkRepository
41
  from app.repositories.visitor_repo import VisitorRepository
 
42
  from app.services.visitor_profile_service import (
43
  profile_is_complete,
44
  resolve_profile,
@@ -237,11 +238,13 @@ async def init_session(
237
  books_payload = []
238
  for b in active_books:
239
  link = await link_repo.get_for_book(b.id, author.id)
 
240
  books_payload.append({
241
  "id": b.id,
242
  "title": b.title,
243
  "tagline": b.tagline,
244
- "cover_path": b.cover_path,
 
245
  "ai_summary": b.ai_summary,
246
  "purchase_url": link.purchase_url if link else None,
247
  })
 
39
  from app.repositories.book_repo import BookRepository
40
  from app.repositories.link_repo import LinkRepository
41
  from app.repositories.visitor_repo import VisitorRepository
42
+ from app.services.pipeline.helpers import public_cover_src
43
  from app.services.visitor_profile_service import (
44
  profile_is_complete,
45
  resolve_profile,
 
238
  books_payload = []
239
  for b in active_books:
240
  link = await link_repo.get_for_book(b.id, author.id)
241
+ cover = public_cover_src(b)
242
  books_payload.append({
243
  "id": b.id,
244
  "title": b.title,
245
  "tagline": b.tagline,
246
+ "cover_path": cover or b.cover_path,
247
+ "cover_url": cover or None,
248
  "ai_summary": b.ai_summary,
249
  "purchase_url": link.purchase_url if link else None,
250
  })
app/schemas/chatbot.py CHANGED
@@ -55,6 +55,7 @@ class BookInfo(BaseModel):
55
  title: str
56
  tagline: str | None
57
  cover_path: str | None
 
58
  ai_summary: str | None = None
59
  purchase_url: str | None = None
60
 
 
55
  title: str
56
  tagline: str | None
57
  cover_path: str | None
58
+ cover_url: str | None = None
59
  ai_summary: str | None = None
60
  purchase_url: str | None = None
61
 
app/services/pipeline/helpers.py CHANGED
@@ -268,6 +268,30 @@ def selected_book_title(books: list, selected_book_id: str | None) -> str:
268
  return "the books"
269
 
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  def book_selector_items(books: list) -> list[dict]:
272
  """Build the list of book selector items for the widget UI."""
273
  items = []
@@ -279,7 +303,7 @@ def book_selector_items(books: list) -> list[dict]:
279
  "id": book.id,
280
  "title": book.title,
281
  "tagline": tagline,
282
- "cover_path": book.cover_path,
283
  })
284
  return items
285
 
 
268
  return "the books"
269
 
270
 
271
+ def public_cover_src(book) -> str:
272
+ """Return a browser-resolvable cover URL for widget / session payloads.
273
+
274
+ Prefers local processed images under /data/covers (rewritten to /covers),
275
+ then falls back to the scraped remote cover_url from URL import.
276
+ """
277
+ for attr in ("cover_medium_path", "cover_thumbnail_path", "cover_path"):
278
+ raw = getattr(book, attr, None) or ""
279
+ path = str(raw).strip()
280
+ if not path:
281
+ continue
282
+ if path.startswith("/data/covers/"):
283
+ return "/covers/" + path[len("/data/covers/"):]
284
+ if path.startswith("/covers/"):
285
+ return path
286
+ if path.startswith("http://") or path.startswith("https://") or path.startswith("data:"):
287
+ return path
288
+ remote = getattr(book, "cover_url", None) or ""
289
+ remote = str(remote).strip()
290
+ if remote.startswith("http://") or remote.startswith("https://") or remote.startswith("data:"):
291
+ return remote
292
+ return ""
293
+
294
+
295
  def book_selector_items(books: list) -> list[dict]:
296
  """Build the list of book selector items for the widget UI."""
297
  items = []
 
303
  "id": book.id,
304
  "title": book.title,
305
  "tagline": tagline,
306
+ "cover_path": public_cover_src(book),
307
  })
308
  return items
309
 
tests/unit/test_public_cover_src.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for chatbot public cover URL resolution."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ from app.services.pipeline.helpers import book_selector_items, public_cover_src
6
+
7
+
8
+ def test_public_cover_src_rewrites_local_medium_path():
9
+ book = SimpleNamespace(
10
+ cover_medium_path="/data/covers/auth1/book1/medium.webp",
11
+ cover_thumbnail_path="/data/covers/auth1/book1/thumb.webp",
12
+ cover_path="/data/covers/auth1/book1/original.jpg",
13
+ cover_url="https://cdn.example.com/ignored.jpg",
14
+ )
15
+ assert public_cover_src(book) == "/covers/auth1/book1/medium.webp"
16
+
17
+
18
+ def test_public_cover_src_falls_back_to_remote_cover_url():
19
+ book = SimpleNamespace(
20
+ cover_medium_path=None,
21
+ cover_thumbnail_path=None,
22
+ cover_path=None,
23
+ cover_url="https://images-na.ssl-images-amazon.com/images/I/abc.jpg",
24
+ )
25
+ assert public_cover_src(book) == (
26
+ "https://images-na.ssl-images-amazon.com/images/I/abc.jpg"
27
+ )
28
+
29
+
30
+ def test_public_cover_src_empty_when_missing():
31
+ book = SimpleNamespace(
32
+ cover_medium_path=None,
33
+ cover_thumbnail_path=None,
34
+ cover_path=None,
35
+ cover_url=None,
36
+ )
37
+ assert public_cover_src(book) == ""
38
+
39
+
40
+ def test_book_selector_items_uses_public_cover():
41
+ book = SimpleNamespace(
42
+ id="b1",
43
+ title="The Key",
44
+ tagline="A story",
45
+ status="ready",
46
+ cover_medium_path=None,
47
+ cover_thumbnail_path=None,
48
+ cover_path=None,
49
+ cover_url="https://example.com/cover.jpg",
50
+ )
51
+ items = book_selector_items([book])
52
+ assert items[0]["cover_path"] == "https://example.com/cover.jpg"