bookcli 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bookcli-0.1.0 → bookcli-0.2.0}/PKG-INFO +1 -1
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/cli.py +420 -2
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli.egg-info/PKG-INFO +1 -1
- {bookcli-0.1.0 → bookcli-0.2.0}/pyproject.toml +1 -1
- {bookcli-0.1.0 → bookcli-0.2.0}/README.md +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/__init__.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/cache.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/config.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/database/__init__.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/database/migrations.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/downloader.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/exceptions.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/models.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/opener.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/providers/__init__.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/providers/base.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/providers/google_books.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/providers/gutenberg.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/providers/internet_archive.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/providers/openlibrary.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/services/__init__.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/services/history.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/services/ranking.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/services/search.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/settings.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli/utils.py +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli.egg-info/SOURCES.txt +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli.egg-info/dependency_links.txt +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli.egg-info/entry_points.txt +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli.egg-info/requires.txt +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/bookcli.egg-info/top_level.txt +0 -0
- {bookcli-0.1.0 → bookcli-0.2.0}/setup.cfg +0 -0
|
@@ -14,6 +14,8 @@ from rich.console import Console
|
|
|
14
14
|
from rich.table import Table
|
|
15
15
|
from rich.panel import Panel
|
|
16
16
|
from rich.theme import Theme
|
|
17
|
+
from rich.align import Align
|
|
18
|
+
from rich.text import Text
|
|
17
19
|
|
|
18
20
|
from bookcli.settings import DB_PATH
|
|
19
21
|
from bookcli.config import load_config, save_config, AppConfig
|
|
@@ -45,7 +47,7 @@ console = Console()
|
|
|
45
47
|
app = typer.Typer(
|
|
46
48
|
name="book",
|
|
47
49
|
help="BookCLI: Search, inspect, and legally download books from public sources.",
|
|
48
|
-
no_args_is_help=
|
|
50
|
+
no_args_is_help=False
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
logger = logging.getLogger("bookcli")
|
|
@@ -292,8 +294,414 @@ async def add_favorite_internal(db_manager: DatabaseManager, book_id_input: str,
|
|
|
292
294
|
|
|
293
295
|
# --- CLI Commands ---
|
|
294
296
|
|
|
295
|
-
|
|
297
|
+
def make_banner() -> Panel:
|
|
298
|
+
banner_text = Text()
|
|
299
|
+
banner_text.append("██████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗██╗ ██╗\n", style="bold cyan")
|
|
300
|
+
banner_text.append("██╔══██╗██╔═══██╗██╔═══██╗██║ ██╔╝██╔════╝██║ ██║\n", style="bold cyan")
|
|
301
|
+
banner_text.append("██████╔╝██║ ██║██║ ██║█████╔╝ ██║ ██║ ██║\n", style="bold green")
|
|
302
|
+
banner_text.append("██╔══██╗██║ ██║██║ ██║██╔═██╗ ██║ ██║ ██║\n", style="bold green")
|
|
303
|
+
banner_text.append("██████╔╝╚██████╔╝╚██████╔╝██║ ██╗╚██████╗███████╗██║\n", style="bold yellow")
|
|
304
|
+
banner_text.append("╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝\n", style="bold yellow")
|
|
305
|
+
banner_text.append(" Search, inspect, and legally download books\n", style="italic dim white")
|
|
306
|
+
banner_text.append(" Powered by Open Library & Gutenberg", style="italic dim white")
|
|
307
|
+
|
|
308
|
+
from rich.box import ROUNDED
|
|
309
|
+
return Panel(
|
|
310
|
+
Align.center(banner_text),
|
|
311
|
+
box=ROUNDED,
|
|
312
|
+
border_style="bold blue",
|
|
313
|
+
padding=(1, 2)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def make_menu_panel() -> Panel:
|
|
318
|
+
menu_table = Table.grid(padding=(0, 2))
|
|
319
|
+
menu_table.add_column(style="bold yellow", justify="right")
|
|
320
|
+
menu_table.add_column(style="bold white")
|
|
321
|
+
|
|
322
|
+
menu_table.add_row("[1]", "🔍 Search Books")
|
|
323
|
+
menu_table.add_row("[2]", "📜 Search History")
|
|
324
|
+
menu_table.add_row("[3]", "⭐ Favorite Books")
|
|
325
|
+
menu_table.add_row("[4]", "⚡ Metadata Cache Stats")
|
|
326
|
+
menu_table.add_row("[5]", "🧹 Clear Metadata Cache")
|
|
327
|
+
menu_table.add_row("[6]", "⚙️ Settings / Configuration")
|
|
328
|
+
menu_table.add_row("[7]", "❌ Exit Dashboard")
|
|
329
|
+
|
|
330
|
+
from rich.box import ROUNDED
|
|
331
|
+
return Panel(
|
|
332
|
+
menu_table,
|
|
333
|
+
title="[bold green]Main Menu[/bold green]",
|
|
334
|
+
box=ROUNDED,
|
|
335
|
+
border_style="green",
|
|
336
|
+
padding=(1, 4),
|
|
337
|
+
expand=False
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def make_commands_panel() -> Panel:
|
|
342
|
+
cmd_table = Table(box=None, padding=(0, 1), show_header=True, header_style="bold magenta")
|
|
343
|
+
cmd_table.add_column("Command Syntax", style="cyan")
|
|
344
|
+
cmd_table.add_column("Description", style="dim white")
|
|
345
|
+
|
|
346
|
+
cmd_table.add_row("book search \"<query>\"", "Search books across all providers")
|
|
347
|
+
cmd_table.add_row("book info <id>", "Show detailed metadata for a book (use ID or index)")
|
|
348
|
+
cmd_table.add_row("book download <id>", "Download a book locally")
|
|
349
|
+
cmd_table.add_row("book open <id>", "Open a downloaded book in OS default reader")
|
|
350
|
+
cmd_table.add_row("book history", "Show search query history")
|
|
351
|
+
cmd_table.add_row("book favorite list", "List all favorite books")
|
|
352
|
+
cmd_table.add_row("book favorite add <id>", "Add a book to favorites")
|
|
353
|
+
cmd_table.add_row("book favorite remove <id>", "Remove a book from favorites")
|
|
354
|
+
cmd_table.add_row("book cache stats / clear", "Show cache statistics or clear cached items")
|
|
355
|
+
cmd_table.add_row("book config [set <key> <val>]", "View/modify configuration settings")
|
|
356
|
+
|
|
357
|
+
from rich.box import ROUNDED
|
|
358
|
+
return Panel(
|
|
359
|
+
cmd_table,
|
|
360
|
+
title="[bold magenta]Direct CLI Commands Guide[/bold magenta]",
|
|
361
|
+
box=ROUNDED,
|
|
362
|
+
border_style="magenta",
|
|
363
|
+
padding=(1, 2)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
async def run_dashboard() -> None:
|
|
368
|
+
"""Persistent dashboard interactive menu loop."""
|
|
369
|
+
db_manager = DatabaseManager(str(DB_PATH))
|
|
370
|
+
await db_manager.initialize()
|
|
371
|
+
config = load_config()
|
|
372
|
+
|
|
373
|
+
while True:
|
|
374
|
+
console.clear()
|
|
375
|
+
console.print(make_banner())
|
|
376
|
+
console.print(make_menu_panel())
|
|
377
|
+
console.print(make_commands_panel())
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
choice = console.input("\n[bold yellow]Select an option (1-7) > [/bold yellow]").strip()
|
|
381
|
+
except (KeyboardInterrupt, EOFError):
|
|
382
|
+
console.print("\n[yellow]Goodbye![/yellow]")
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
if not choice:
|
|
386
|
+
continue
|
|
387
|
+
|
|
388
|
+
if choice == "1":
|
|
389
|
+
# 🔍 Search Books
|
|
390
|
+
console.clear()
|
|
391
|
+
console.print(Panel("[bold green]🔍 Search Books[/bold green]", border_style="green"))
|
|
392
|
+
query = console.input("[bold white]Enter search query (or press Enter to go back): [/bold white]").strip()
|
|
393
|
+
if not query:
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
# Optional filters
|
|
397
|
+
author = console.input("[dim]Filter by Author (optional, press Enter to skip): [/dim]").strip() or None
|
|
398
|
+
isbn = console.input("[dim]Filter by ISBN (optional, press Enter to skip): [/dim]").strip() or None
|
|
399
|
+
publisher = console.input("[dim]Filter by Publisher (optional, press Enter to skip): [/dim]").strip() or None
|
|
400
|
+
subject = console.input("[dim]Filter by Subject (optional, press Enter to skip): [/dim]").strip() or None
|
|
401
|
+
|
|
402
|
+
with console.status("[bold green]Searching enabled providers..."):
|
|
403
|
+
try:
|
|
404
|
+
results = await search_books(
|
|
405
|
+
query=query,
|
|
406
|
+
config=config,
|
|
407
|
+
db_manager=db_manager,
|
|
408
|
+
author=author,
|
|
409
|
+
isbn=isbn,
|
|
410
|
+
publisher=publisher,
|
|
411
|
+
subject=subject
|
|
412
|
+
)
|
|
413
|
+
except Exception as e:
|
|
414
|
+
console.print(Panel(f"[bold red]Search Error:[/bold red] {e}", title="Error", border_style="red"))
|
|
415
|
+
console.input("\nPress Enter to return to main menu...")
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
if not results:
|
|
419
|
+
console.print("[yellow]No books found matching the search criteria.[/yellow]")
|
|
420
|
+
console.input("\nPress Enter to return to main menu...")
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
# Store search results session in DB
|
|
424
|
+
async with db_manager.connection() as conn:
|
|
425
|
+
await conn.execute("DELETE FROM search_results_session")
|
|
426
|
+
for idx, book in enumerate(results, start=1):
|
|
427
|
+
await conn.execute(
|
|
428
|
+
"INSERT INTO search_results_session (result_index, book_id) VALUES (?, ?)",
|
|
429
|
+
(idx, book.id)
|
|
430
|
+
)
|
|
431
|
+
await conn.commit()
|
|
432
|
+
|
|
433
|
+
# Format output as Rich table
|
|
434
|
+
table = Table(title=f"Search Results for '{query}'")
|
|
435
|
+
table.add_column("ID", justify="right", style="cyan", no_wrap=True)
|
|
436
|
+
table.add_column("Title", style="magenta")
|
|
437
|
+
table.add_column("Author(s)", style="green")
|
|
438
|
+
table.add_column("Year", justify="center", style="yellow")
|
|
439
|
+
table.add_column("Source", style="blue")
|
|
440
|
+
table.add_column("Download", justify="center")
|
|
441
|
+
|
|
442
|
+
for idx, book in enumerate(results, start=1):
|
|
443
|
+
if book.download_availability:
|
|
444
|
+
avail_str = f"[green]{book.file_format.upper()}[/green]" if book.file_format else "[green]Yes[/green]"
|
|
445
|
+
else:
|
|
446
|
+
avail_str = "[red]No[/red]"
|
|
447
|
+
author_str = ", ".join(book.authors) if book.authors else "Unknown"
|
|
448
|
+
year_str = str(book.published_year) if book.published_year else "N/A"
|
|
449
|
+
table.add_row(
|
|
450
|
+
str(idx),
|
|
451
|
+
book.title,
|
|
452
|
+
author_str,
|
|
453
|
+
year_str,
|
|
454
|
+
book.source,
|
|
455
|
+
avail_str
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
console.print(table)
|
|
459
|
+
console.print(f"[dim]Total: {len(results)} books.[/dim]")
|
|
460
|
+
|
|
461
|
+
# Start Interactive Explorer mode for the search results
|
|
462
|
+
console.print("\n[bold cyan]Interactive Explorer Mode[/bold cyan]")
|
|
463
|
+
console.print("[dim]Enter <ID> to download, 'i <ID>' for info, 'f <ID>' to favorite, 'o <ID>' to open, or 'q' to quit explorer.[/dim]")
|
|
464
|
+
while True:
|
|
465
|
+
try:
|
|
466
|
+
action = console.input("\n[bold yellow]Action > [/bold yellow]").strip()
|
|
467
|
+
if not action or action.lower() in ("q", "quit", "exit"):
|
|
468
|
+
break
|
|
469
|
+
|
|
470
|
+
tokens = action.split()
|
|
471
|
+
if not tokens:
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
if tokens[0].isdigit():
|
|
475
|
+
val = tokens[0]
|
|
476
|
+
custom_path = None
|
|
477
|
+
if len(tokens) >= 3 and tokens[1] in ("-o", "--output"):
|
|
478
|
+
custom_path = " ".join(tokens[2:])
|
|
479
|
+
elif len(tokens) > 1:
|
|
480
|
+
console.print("[red]Invalid output path syntax. Use: <ID> -o <path>[/red]")
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
await download_book_internal(db_manager, val, config, custom_path=Path(custom_path) if custom_path else None)
|
|
485
|
+
except BookCLIError as e:
|
|
486
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
487
|
+
elif len(tokens) >= 2:
|
|
488
|
+
cmd, val = tokens[0].lower(), " ".join(tokens[1:])
|
|
489
|
+
try:
|
|
490
|
+
if cmd == "i":
|
|
491
|
+
await show_info_internal(db_manager, val, config)
|
|
492
|
+
elif cmd == "f":
|
|
493
|
+
await add_favorite_internal(db_manager, val, config)
|
|
494
|
+
elif cmd == "o":
|
|
495
|
+
await open_book_internal(db_manager, val)
|
|
496
|
+
else:
|
|
497
|
+
console.print("[red]Unknown command prefix. Use 'i', 'f', 'o', or just the ID.[/red]")
|
|
498
|
+
except BookCLIError as e:
|
|
499
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
500
|
+
else:
|
|
501
|
+
console.print("[red]Invalid input. Enter an ID index number or 'q' to exit.[/red]")
|
|
502
|
+
except (KeyboardInterrupt, EOFError):
|
|
503
|
+
break
|
|
504
|
+
|
|
505
|
+
elif choice == "2":
|
|
506
|
+
# 📜 Search History
|
|
507
|
+
console.clear()
|
|
508
|
+
console.print(Panel("[bold green]📜 Search History[/bold green]", border_style="green"))
|
|
509
|
+
hist = await get_search_history(db_manager, limit=20)
|
|
510
|
+
if not hist:
|
|
511
|
+
console.print("[yellow]Search history is empty.[/yellow]")
|
|
512
|
+
else:
|
|
513
|
+
table = Table(title="Search History")
|
|
514
|
+
table.add_column("Timestamp", style="cyan")
|
|
515
|
+
table.add_column("Query/Filters", style="magenta")
|
|
516
|
+
table.add_column("Results Count", style="green", justify="right")
|
|
517
|
+
|
|
518
|
+
for entry in hist:
|
|
519
|
+
table.add_row(
|
|
520
|
+
entry["timestamp"],
|
|
521
|
+
entry["query"],
|
|
522
|
+
str(entry["results_count"])
|
|
523
|
+
)
|
|
524
|
+
console.print(table)
|
|
525
|
+
|
|
526
|
+
console.print("\nOptions: [c] Clear history, [Any other key] Go back")
|
|
527
|
+
hist_choice = console.input("[bold yellow]Choose > [/bold yellow]").strip().lower()
|
|
528
|
+
if hist_choice == 'c':
|
|
529
|
+
await clear_search_history(db_manager)
|
|
530
|
+
console.print("[green]Search history cleared successfully.[/green]")
|
|
531
|
+
await asyncio.sleep(1)
|
|
532
|
+
continue
|
|
533
|
+
console.input("\nPress Enter to return to main menu...")
|
|
534
|
+
|
|
535
|
+
elif choice == "3":
|
|
536
|
+
# ⭐ Favorite Books
|
|
537
|
+
console.clear()
|
|
538
|
+
console.print(Panel("[bold green]⭐ Favorite Books[/bold green]", border_style="green"))
|
|
539
|
+
async with db_manager.connection() as conn:
|
|
540
|
+
async with conn.execute("SELECT book_id, title, added_at FROM favorites ORDER BY added_at DESC") as cursor:
|
|
541
|
+
rows = await cursor.fetchall()
|
|
542
|
+
|
|
543
|
+
if not rows:
|
|
544
|
+
console.print("[yellow]Favorites list is empty.[/yellow]")
|
|
545
|
+
console.input("\nPress Enter to return to main menu...")
|
|
546
|
+
continue
|
|
547
|
+
|
|
548
|
+
table = Table(title="Favorite Books")
|
|
549
|
+
table.add_column("Index", style="cyan", justify="right")
|
|
550
|
+
table.add_column("Book ID", style="cyan")
|
|
551
|
+
table.add_column("Title", style="magenta")
|
|
552
|
+
table.add_column("Added At", style="green")
|
|
553
|
+
|
|
554
|
+
favorites_map = {}
|
|
555
|
+
for idx, row in enumerate(rows, start=1):
|
|
556
|
+
table.add_row(
|
|
557
|
+
str(idx),
|
|
558
|
+
row["book_id"],
|
|
559
|
+
row["title"],
|
|
560
|
+
row["added_at"]
|
|
561
|
+
)
|
|
562
|
+
favorites_map[idx] = row["book_id"]
|
|
563
|
+
|
|
564
|
+
console.print(table)
|
|
565
|
+
|
|
566
|
+
console.print("\nOptions: [d <index>] Download, [o <index>] Open, [r <index>] Remove, [Any other key] Go back")
|
|
567
|
+
fav_action = console.input("[bold yellow]Choose > [/bold yellow]").strip()
|
|
568
|
+
if fav_action:
|
|
569
|
+
tokens = fav_action.split()
|
|
570
|
+
if len(tokens) >= 2 and tokens[0].lower() in ('d', 'o', 'r') and tokens[1].isdigit():
|
|
571
|
+
cmd = tokens[0].lower()
|
|
572
|
+
idx = int(tokens[1])
|
|
573
|
+
if idx in favorites_map:
|
|
574
|
+
book_id = favorites_map[idx]
|
|
575
|
+
if cmd == 'd':
|
|
576
|
+
try:
|
|
577
|
+
await download_book_internal(db_manager, book_id, config)
|
|
578
|
+
console.input("\nPress Enter...")
|
|
579
|
+
except BookCLIError as e:
|
|
580
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
581
|
+
console.input("\nPress Enter...")
|
|
582
|
+
elif cmd == 'o':
|
|
583
|
+
try:
|
|
584
|
+
await open_book_internal(db_manager, book_id)
|
|
585
|
+
console.input("\nPress Enter...")
|
|
586
|
+
except BookCLIError as e:
|
|
587
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
588
|
+
console.input("\nPress Enter...")
|
|
589
|
+
elif cmd == 'r':
|
|
590
|
+
async with db_manager.connection() as conn:
|
|
591
|
+
await conn.execute("DELETE FROM favorites WHERE book_id = ?", (book_id,))
|
|
592
|
+
await conn.commit()
|
|
593
|
+
console.print(f"[green]Removed book from favorites.[/green]")
|
|
594
|
+
await asyncio.sleep(1)
|
|
595
|
+
else:
|
|
596
|
+
console.print("[red]Invalid favorite index.[/red]")
|
|
597
|
+
await asyncio.sleep(1)
|
|
598
|
+
else:
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
elif choice == "4":
|
|
602
|
+
# ⚡ Cache Statistics
|
|
603
|
+
console.clear()
|
|
604
|
+
console.print(Panel("[bold green]⚡ Cache Statistics[/bold green]", border_style="green"))
|
|
605
|
+
stats = await get_cache_db_stats(db_manager)
|
|
606
|
+
console.print(Panel(
|
|
607
|
+
f"Total Cached Books: [green]{stats['total_items']}[/green]\n"
|
|
608
|
+
f"Oldest Record: [yellow]{stats['oldest_item']}[/yellow]\n"
|
|
609
|
+
f"Newest Record: [yellow]{stats['newest_item']}[/yellow]",
|
|
610
|
+
title="Metadata Cache Statistics",
|
|
611
|
+
border_style="cyan"
|
|
612
|
+
))
|
|
613
|
+
console.input("\nPress Enter to return to main menu...")
|
|
614
|
+
|
|
615
|
+
elif choice == "5":
|
|
616
|
+
# 🧹 Clear Cache
|
|
617
|
+
console.clear()
|
|
618
|
+
console.print(Panel("[bold green]🧹 Clear Cache[/bold green]", border_style="green"))
|
|
619
|
+
deleted_count = await clear_cache_db(db_manager)
|
|
620
|
+
console.print(f"[green]Metadata cache cleared. Removed {deleted_count} items.[/green]")
|
|
621
|
+
await asyncio.sleep(1.5)
|
|
622
|
+
|
|
623
|
+
elif choice == "6":
|
|
624
|
+
# ⚙️ Settings / Configuration
|
|
625
|
+
while True:
|
|
626
|
+
console.clear()
|
|
627
|
+
console.print(Panel("[bold green]⚙️ Settings / Configuration[/bold green]", border_style="green"))
|
|
628
|
+
config = load_config()
|
|
629
|
+
table = Table(title="Current Configuration Settings")
|
|
630
|
+
table.add_column("Index", style="cyan", justify="right")
|
|
631
|
+
table.add_column("Setting Key", style="cyan")
|
|
632
|
+
table.add_column("Current Value", style="yellow")
|
|
633
|
+
|
|
634
|
+
settings_list = [
|
|
635
|
+
("download-dir", config.download_dir),
|
|
636
|
+
("cache-ttl", str(config.cache_ttl_seconds)),
|
|
637
|
+
("timeout", str(config.timeout_seconds)),
|
|
638
|
+
("theme", config.theme),
|
|
639
|
+
("provider:google-books", str(config.providers.google_books)),
|
|
640
|
+
("provider:openlibrary", str(config.providers.openlibrary)),
|
|
641
|
+
("provider:gutenberg", str(config.providers.gutenberg)),
|
|
642
|
+
("provider:internet-archive", str(config.providers.internet_archive)),
|
|
643
|
+
]
|
|
644
|
+
|
|
645
|
+
for idx, (k, v) in enumerate(settings_list, start=1):
|
|
646
|
+
table.add_row(str(idx), k, v)
|
|
647
|
+
|
|
648
|
+
console.print(table)
|
|
649
|
+
console.print("\nOptions: Enter index (1-8) to edit, or [q] to return to Main Menu")
|
|
650
|
+
cfg_choice = console.input("[bold yellow]Select option > [/bold yellow]").strip()
|
|
651
|
+
if cfg_choice.lower() in ('q', 'quit', 'back', 'exit'):
|
|
652
|
+
break
|
|
653
|
+
if cfg_choice.isdigit():
|
|
654
|
+
idx = int(cfg_choice)
|
|
655
|
+
if 1 <= idx <= len(settings_list):
|
|
656
|
+
key, current_val = settings_list[idx - 1]
|
|
657
|
+
new_val = console.input(f"Enter new value for [cyan]{key}[/cyan] (current: {current_val}): ").strip()
|
|
658
|
+
if not new_val:
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
# Save config
|
|
662
|
+
if key == "download-dir":
|
|
663
|
+
config.download_dir = new_val
|
|
664
|
+
os.makedirs(new_val, exist_ok=True)
|
|
665
|
+
elif key == "cache-ttl":
|
|
666
|
+
if not new_val.isdigit():
|
|
667
|
+
console.print("[red]Cache TTL must be an integer.[/red]")
|
|
668
|
+
console.input("\nPress Enter...")
|
|
669
|
+
continue
|
|
670
|
+
config.cache_ttl_seconds = int(new_val)
|
|
671
|
+
elif key == "timeout":
|
|
672
|
+
if not new_val.isdigit():
|
|
673
|
+
console.print("[red]Timeout must be an integer.[/red]")
|
|
674
|
+
console.input("\nPress Enter...")
|
|
675
|
+
continue
|
|
676
|
+
config.timeout_seconds = int(new_val)
|
|
677
|
+
elif key == "theme":
|
|
678
|
+
config.theme = new_val
|
|
679
|
+
elif key.startswith("provider:"):
|
|
680
|
+
provider_name = key.split(":")[1]
|
|
681
|
+
prov_key = provider_name.replace("-", "_")
|
|
682
|
+
bool_val = new_val.lower() in ("true", "1", "yes", "on")
|
|
683
|
+
if hasattr(config.providers, prov_key):
|
|
684
|
+
setattr(config.providers, prov_key, bool_val)
|
|
685
|
+
|
|
686
|
+
try:
|
|
687
|
+
save_config(config)
|
|
688
|
+
console.print(f"[green]Successfully updated configuration setting '{key}' to '{new_val}'.[/green]")
|
|
689
|
+
await asyncio.sleep(1)
|
|
690
|
+
except Exception as e:
|
|
691
|
+
console.print(f"[red]Error saving configuration: {e}[/red]")
|
|
692
|
+
console.input("\nPress Enter...")
|
|
693
|
+
else:
|
|
694
|
+
console.print("[red]Invalid index.[/red]")
|
|
695
|
+
await asyncio.sleep(1)
|
|
696
|
+
|
|
697
|
+
elif choice == "7" or choice.lower() in ("q", "exit", "quit"):
|
|
698
|
+
console.print("[yellow]Goodbye![/yellow]")
|
|
699
|
+
break
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@app.callback(invoke_without_command=True)
|
|
296
703
|
def global_callback(
|
|
704
|
+
ctx: typer.Context,
|
|
297
705
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging (INFO)"),
|
|
298
706
|
debug: bool = typer.Option(False, "--debug", "-d", help="Enable debug logging (DEBUG)")
|
|
299
707
|
):
|
|
@@ -310,6 +718,16 @@ def global_callback(
|
|
|
310
718
|
)
|
|
311
719
|
logger.setLevel(level)
|
|
312
720
|
|
|
721
|
+
# Check if we should run the interactive dashboard
|
|
722
|
+
if ctx.invoked_subcommand is None:
|
|
723
|
+
is_interactive = sys.stdin.isatty() or os.environ.get("BOOKCLI_INTERACTIVE") == "true"
|
|
724
|
+
if is_interactive:
|
|
725
|
+
asyncio.run(run_dashboard())
|
|
726
|
+
else:
|
|
727
|
+
console.print(make_banner())
|
|
728
|
+
console.print(make_commands_panel())
|
|
729
|
+
console.print("\n[bold yellow]Note:[/bold yellow] Run in an interactive terminal to access the full visual dashboard menu.")
|
|
730
|
+
|
|
313
731
|
|
|
314
732
|
@app.command()
|
|
315
733
|
def search(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|