mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-04-20 05:51:21 -04:00
197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
"""Backend logic for the book download application."""
|
|
|
|
import threading, time
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
import subprocess
|
|
import os
|
|
|
|
from logger import setup_logger
|
|
from config import CUSTOM_SCRIPT, CROSS_FILE_SYSTEM
|
|
from env import INGEST_DIR, TMP_DIR, MAIN_LOOP_SLEEP_TIME, USE_BOOK_TITLE
|
|
from models import book_queue, BookInfo, QueueStatus, SearchFilters
|
|
import book_manager
|
|
|
|
logger = setup_logger(__name__)
|
|
|
|
def _sanitize_filename(filename: str) -> str:
|
|
"""Sanitize a filename by replacing spaces with underscores and removing invalid characters."""
|
|
keepcharacters = (' ','.','_')
|
|
return "".join(c for c in filename if c.isalnum() or c in keepcharacters).rstrip()
|
|
|
|
def search_books(query: str, filters: SearchFilters) -> List[Dict[str, Any]]:
|
|
"""Search for books matching the query.
|
|
|
|
Args:
|
|
query: Search term
|
|
filters: Search filters object
|
|
|
|
Returns:
|
|
List[Dict]: List of book information dictionaries
|
|
"""
|
|
try:
|
|
books = book_manager.search_books(query, filters)
|
|
return [_book_info_to_dict(book) for book in books]
|
|
except Exception as e:
|
|
logger.error_trace(f"Error searching books: {e}")
|
|
return []
|
|
|
|
def get_book_info(book_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Get detailed information for a specific book.
|
|
|
|
Args:
|
|
book_id: Book identifier
|
|
|
|
Returns:
|
|
Optional[Dict]: Book information dictionary if found
|
|
"""
|
|
try:
|
|
book = book_manager.get_book_info(book_id)
|
|
return _book_info_to_dict(book)
|
|
except Exception as e:
|
|
logger.error_trace(f"Error getting book info: {e}")
|
|
return None
|
|
|
|
def queue_book(book_id: str) -> bool:
|
|
"""Add a book to the download queue.
|
|
|
|
Args:
|
|
book_id: Book identifier
|
|
|
|
Returns:
|
|
bool: True if book was successfully queued
|
|
"""
|
|
try:
|
|
book_info = book_manager.get_book_info(book_id)
|
|
book_queue.add(book_id, book_info)
|
|
logger.info(f"Book queued: {book_info.title}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error_trace(f"Error queueing book: {e}")
|
|
return False
|
|
|
|
def queue_status() -> Dict[str, Dict[str, Any]]:
|
|
"""Get current status of the download queue.
|
|
|
|
Returns:
|
|
Dict: Queue status organized by status type
|
|
"""
|
|
status = book_queue.get_status()
|
|
# Convert Enum keys to strings and properly format the response
|
|
return {
|
|
status_type.value: books
|
|
for status_type, books in status.items()
|
|
}
|
|
|
|
def get_book_data(book_id: str) -> Tuple[Optional[bytes], str] :
|
|
"""Get book data for a specific book, including its title.
|
|
|
|
Args:
|
|
book_id: Book identifier
|
|
|
|
Returns:
|
|
Tuple[Optional[bytes], str]: Book data if available, and the book title
|
|
"""
|
|
try:
|
|
book_info = book_queue._book_data[book_id]
|
|
path = book_info.download_path
|
|
with open(path, "rb") as f:
|
|
return f.read(), book_info.title
|
|
except Exception as e:
|
|
logger.error_trace(f"Error getting book data: {e}")
|
|
book_info.download_path = None
|
|
return None, ""
|
|
|
|
def _book_info_to_dict(book: BookInfo) -> Dict[str, Any]:
|
|
"""Convert BookInfo object to dictionary representation."""
|
|
return {
|
|
key: value for key, value in book.__dict__.items()
|
|
if value is not None
|
|
}
|
|
|
|
def _download_book(book_id: str) -> Optional[str]:
|
|
"""Download and process a book.
|
|
|
|
Args:
|
|
book_id: Book identifier
|
|
|
|
Returns:
|
|
str: Path to the downloaded book if successful, None otherwise
|
|
"""
|
|
try:
|
|
book_info = book_queue._book_data[book_id]
|
|
|
|
if USE_BOOK_TITLE:
|
|
book_name = _sanitize_filename(book_info.title)
|
|
else:
|
|
book_name = book_id
|
|
book_name += f".{book_info.format}"
|
|
book_path = TMP_DIR / book_name
|
|
|
|
success = book_manager.download_book(book_info, book_path)
|
|
if not success:
|
|
raise Exception("Unkown error downloading book")
|
|
|
|
if CUSTOM_SCRIPT:
|
|
logger.info(f"Running custom script: {CUSTOM_SCRIPT}")
|
|
subprocess.run([CUSTOM_SCRIPT, book_path])
|
|
|
|
intermediate_path = INGEST_DIR / book_id # Without extension
|
|
final_path = INGEST_DIR / book_name
|
|
|
|
if os.path.exists(book_path):
|
|
if CROSS_FILE_SYSTEM:
|
|
logger.info(f"Copying book to ingest directory then renaming: {book_path} -> {intermediate_path} -> {final_path}")
|
|
try:
|
|
shutil.move(book_path, intermediate_path)
|
|
except Exception as e:
|
|
logger.debug(f"Error moving book: {e}, will try copying instead")
|
|
shutil.copy(book_path, intermediate_path)
|
|
os.remove(book_path)
|
|
else:
|
|
logger.info(f"Moving book to ingest directory: {book_path} -> {intermediate_path}")
|
|
shutil.move(book_path, intermediate_path)
|
|
logger.info(f"Renaming book: {intermediate_path} -> {final_path}")
|
|
os.rename(intermediate_path, final_path)
|
|
return str(final_path)
|
|
except Exception as e:
|
|
logger.error_trace(f"Error downloading book: {e}")
|
|
return None
|
|
|
|
def download_loop() -> None:
|
|
"""Background thread for processing download queue."""
|
|
logger.info("Starting download loop")
|
|
|
|
while True:
|
|
book_id = book_queue.get_next()
|
|
if not book_id:
|
|
time.sleep(MAIN_LOOP_SLEEP_TIME)
|
|
continue
|
|
|
|
try:
|
|
book_queue.update_status(book_id, QueueStatus.DOWNLOADING)
|
|
download_path = _download_book(book_id)
|
|
if download_path:
|
|
book_queue.update_download_path(book_id, download_path)
|
|
|
|
new_status = (
|
|
QueueStatus.AVAILABLE if download_path else QueueStatus.ERROR
|
|
)
|
|
book_queue.update_status(book_id, new_status)
|
|
|
|
logger.info(
|
|
f"Book {book_id} download {'successful' if download_path else 'failed'}"
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error_trace(f"Error in download loop: {e}")
|
|
book_queue.update_status(book_id, QueueStatus.ERROR)
|
|
|
|
# Start download loop in background thread
|
|
download_thread = threading.Thread(
|
|
target=download_loop,
|
|
daemon=True
|
|
)
|
|
download_thread.start()
|