Add SQLite library with API, library desktop apps and MobyGames scraper
This commit is contained in:
1 parent
6674428346
commit
a16444451a
69 files changed
+2988
-1026
No files matched your search
@@ -7,13 +7,13 @@ from textual.binding import Binding
|
||||
from textual.containers import Horizontal, VerticalScroll
|
||||
from textual.widgets import DataTable, Static
|
||||
|
||||
import book_cache
|
||||
import books
|
||||
import goodreads
|
||||
import goodreads_series
|
||||
import library_api
|
||||
from tui.cache_indicator import cache_state, cache_text
|
||||
from tui.content_pane import ContentPane
|
||||
from tui.dialogs import ConfirmDialog, FormDialog, FormField
|
||||
from tui.library_pane import LibraryPane
|
||||
from tui.series_picker import SeriesPicker
|
||||
|
||||
REQUEST_DELAY_SECONDS = 3
|
||||
@@ -29,6 +29,8 @@ def book_details(book: dict) -> str:
|
||||
("Published", book.get("published")),
|
||||
("Genres", ", ".join(book.get("genres") or [])),
|
||||
("Added", book.get("added")),
|
||||
("Started", book.get("started")),
|
||||
("Finished", book.get("finished")),
|
||||
("Cache", book_cache_state(book)),
|
||||
]
|
||||
lines = [f"[b]{escape(book.get('title', ''))}[/b]", escape(", ".join(book.get("authors") or [])), ""]
|
||||
@@ -41,15 +43,16 @@ def book_details(book: dict) -> str:
|
||||
|
||||
def book_cache_state(book: dict) -> str:
|
||||
has_details = bool(book.get("authors")) or bool(book.get("description"))
|
||||
return cache_state(has_details, book_cache.is_local_cover(book.get("cover")))
|
||||
return cache_state(has_details, library_api.is_stored_image(book.get("cover")))
|
||||
|
||||
|
||||
class BooksPane(ContentPane):
|
||||
class BooksPane(LibraryPane):
|
||||
BINDINGS = [
|
||||
Binding("a", "add", "Add"),
|
||||
Binding("S", "add_series", "Add series"),
|
||||
Binding("e", "rate", "Rate"),
|
||||
Binding("s", "shelf", "Shelf"),
|
||||
Binding("h", "dates", "Dates"),
|
||||
Binding("d", "delete", "Delete"),
|
||||
Binding("u", "refresh", "Refresh"),
|
||||
Binding("U", "refresh_all", "Refresh all"),
|
||||
@@ -57,7 +60,9 @@ class BooksPane(ContentPane):
|
||||
Binding("r", "reload", "Reload"),
|
||||
]
|
||||
|
||||
content_file = "books.json"
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.books: dict[str, dict] = {}
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="split-layout"):
|
||||
@@ -74,14 +79,19 @@ class BooksPane(ContentPane):
|
||||
table.add_column("Cache", key="cache")
|
||||
self.reload()
|
||||
|
||||
def current_shelves(self) -> dict[str, str]:
|
||||
return {identifier: book.get("shelf", "read") for identifier, book in self.books.items()}
|
||||
|
||||
def book_by_id(self, identifier: str | None) -> dict | None:
|
||||
return next((book for book in books.load_books() if book.get("goodreadsId") == identifier), None)
|
||||
return self.books.get(identifier) if identifier else None
|
||||
|
||||
def reload(self, focus_key: str | None = None):
|
||||
table = self.table()
|
||||
table.clear()
|
||||
shelf_order = {shelf: index for index, shelf in enumerate(books.SHELVES)}
|
||||
sorted_books = sorted(books.load_books(), key=lambda book: (shelf_order.get(book.get("shelf"), 99), -(book.get("rating") or -1)))
|
||||
loaded = self.load_entries(books.load_books)
|
||||
self.books = {book["goodreadsId"]: book for book in loaded}
|
||||
sorted_books = sorted(loaded, key=lambda book: (shelf_order.get(book.get("shelf"), 99), -(book.get("rating") or -1)))
|
||||
for book in sorted_books:
|
||||
rating = f"{book['rating']}/10" if book.get("rating") is not None else "-"
|
||||
authors = ", ".join(book.get("authors") or [])
|
||||
@@ -141,6 +151,13 @@ class BooksPane(ContentPane):
|
||||
|
||||
self.app.push_screen(FormDialog("Move to shelf", fields, "Move"), moved)
|
||||
|
||||
def action_dates(self):
|
||||
book = self.book_by_id(self.selected_key())
|
||||
if not book:
|
||||
return
|
||||
identifier = book["goodreadsId"]
|
||||
self.edit_dates(book["title"], book, lambda started, finished: books.set_dates(identifier, started, finished), identifier)
|
||||
|
||||
def action_delete(self):
|
||||
book = self.book_by_id(self.selected_key())
|
||||
if not book:
|
||||
@@ -173,7 +190,7 @@ class BooksPane(ContentPane):
|
||||
@work(thread=True, exclusive=True, group="refresh")
|
||||
def refresh_every_book(self):
|
||||
refreshed = 0
|
||||
all_books = books.load_books()
|
||||
all_books = list(self.books.values())
|
||||
for index, book in enumerate(all_books):
|
||||
if index:
|
||||
time.sleep(REQUEST_DELAY_SECONDS)
|
||||
@@ -190,9 +207,9 @@ class BooksPane(ContentPane):
|
||||
self.app.call_from_thread(self.notify, f"Refreshed {refreshed} of {len(all_books)} books")
|
||||
|
||||
def action_cache_covers(self):
|
||||
missing = books.books_missing_local_covers()
|
||||
missing = [book for book in self.books.values() if book.get("coverSource") and not library_api.is_stored_image(book.get("cover"))]
|
||||
if not missing:
|
||||
self.notify("Every cover is already saved in static/covers/books")
|
||||
self.notify("Every cover is already saved in the library")
|
||||
return
|
||||
self.notify(f"Saving {len(missing)} covers...")
|
||||
self.cache_missing_covers(missing)
|
||||
@@ -202,7 +219,7 @@ class BooksPane(ContentPane):
|
||||
saved = 0
|
||||
for book in missing:
|
||||
try:
|
||||
books.cache_book_cover(book["goodreadsId"])
|
||||
books.store_book_cover(book)
|
||||
except (OSError, ValueError) as error:
|
||||
self.app.call_from_thread(self.notify, f"{book['title']}: {error}", title="Skipped", severity="warning")
|
||||
continue
|
||||
@@ -225,12 +242,12 @@ class BooksPane(ContentPane):
|
||||
self.notify(f"Adding {len(choices)} books from {series['name']}...")
|
||||
self.add_series_books(series, choices)
|
||||
|
||||
self.app.push_screen(SeriesPicker(series, books.library_shelves()), picked)
|
||||
self.app.push_screen(SeriesPicker(series, self.current_shelves()), picked)
|
||||
|
||||
@work(thread=True, exclusive=True, group="series")
|
||||
def add_series_books(self, series: dict, choices: dict[str, str]):
|
||||
added = 0
|
||||
already_listed = set(books.library_shelves())
|
||||
already_listed = set(self.current_shelves())
|
||||
fetched_any = False
|
||||
for entry in series["books"]:
|
||||
shelf = choices.get(entry["goodreadsId"])
|
||||
|
||||
Reference in new issue
Block a user