calibre_config.py•6.06 kB
"""Calibre Configuration Management
Handles configuration loading, validation, and path management.
"""
import json
import logging
from pathlib import Path
from typing import Dict, Any
logger = logging.getLogger(__name__)
class CalibreConfig:
"""Manages Calibre client configuration and validation."""
def __init__(self, config_path: str = "config.json"):
"""Initialize configuration from file.
Resolution order:
1) Environment variable CALIBRE_CONFIG_PATH (if set)
2) Provided config_path
3) If the above does not exist and ends with config.json, try sibling config.local.json
"""
self.config_path = config_path
self.config = self._load_config()
self.library_path = Path(self.config["calibre_library_path"])
self.calibre_path = Path(self.config["calibre_installation_path"])
self.calibre_debug = self.calibre_path / "calibre-debug.exe"
self._validate_setup()
logger.info(f"Configuration loaded for library: {self.library_path}")
def _resolve_config_path(self) -> Path:
"""Resolve the configuration file path with sensible fallbacks."""
import os
# 1) Environment override
env_path = os.environ.get("CALIBRE_CONFIG_PATH")
if env_path and env_path.strip():
p = Path(os.path.expanduser(os.path.expandvars(env_path.strip())))
if p.exists():
self.config_path = str(p)
return p
# 2) Provided path (with local override preference)
p = Path(self.config_path)
# If the provided path looks like config.json and a sibling config.local.json exists,
# prefer the local override even if config.json exists. This avoids using placeholder
# defaults when a real local config is present.
try:
if p.name == "config.json":
candidate = p.with_name("config.local.json")
if candidate.exists():
self.config_path = str(candidate)
return candidate
except Exception:
pass
if p.exists():
return p
# 3) As a final fallback, if provided path ends with config.json, try sibling config.local.json
try:
if p.name == "config.json":
candidate = p.with_name("config.local.json")
if candidate.exists():
self.config_path = str(candidate)
return candidate
except Exception:
pass
return p
def _load_config(self) -> Dict[str, Any]:
"""Load configuration from JSON file."""
config_file = self._resolve_config_path()
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {config_file}")
with config_file.open('r', encoding='utf-8') as f:
return json.load(f)
def _validate_setup(self) -> None:
"""Validate Calibre installation and library paths."""
if not self.calibre_debug.exists():
raise FileNotFoundError(f"Calibre executable not found: {self.calibre_debug}")
if not self.library_path.exists():
raise FileNotFoundError(f"Library path not found: {self.library_path}")
metadata_db = self.library_path / "metadata.db"
if not metadata_db.exists():
raise FileNotFoundError(f"Calibre library database not found: {metadata_db}")
def get_metadata_db_path(self) -> Path:
"""Get path to the Calibre metadata database."""
return self.library_path / "metadata.db"
def get_fts_db_path(self) -> Path:
"""Get path to the Calibre full-text search database."""
# Check if FTS path is explicitly configured
if "calibre_fts_db_path" in self.config:
return Path(self.config["calibre_fts_db_path"])
# Default to library path + full-text-search.db
return self.library_path / "full-text-search.db"
def has_fts_database(self) -> bool:
"""Check if full-text search database exists."""
return self.get_fts_db_path().exists()
def ping(self) -> Dict[str, Any]:
"""Test configuration and paths."""
try:
calibre_exists = self.calibre_debug.exists()
library_exists = self.library_path.exists()
metadata_db_exists = self.get_metadata_db_path().exists()
fts_db_exists = self.has_fts_database()
if calibre_exists and library_exists and metadata_db_exists:
return {
"status": "connected",
"library_path": str(self.library_path),
"message": "All paths exist and accessible",
"calibre_executable": str(self.calibre_debug),
"fts_database_path": str(self.get_fts_db_path()),
"checks": {
"calibre_exe": calibre_exists,
"library_path": library_exists,
"metadata_db": metadata_db_exists,
"fts_db": fts_db_exists
}
}
else:
return {
"status": "error",
"library_path": str(self.library_path),
"message": "Some required paths are missing",
"fts_database_path": str(self.get_fts_db_path()),
"checks": {
"calibre_exe": calibre_exists,
"library_path": library_exists,
"metadata_db": metadata_db_exists,
"fts_db": fts_db_exists
}
}
except Exception as e:
return {
"status": "error",
"library_path": str(self.library_path),
"message": f"Ping failed: {str(e)}"
}