This commit is contained in:
2026-01-23 16:46:48 -08:00
parent 797b5fee40
commit b3a4ba14e5
5 changed files with 193 additions and 106 deletions

View File

@@ -5,6 +5,8 @@ from __future__ import annotations
import re
import tempfile
import json
import sqlite3
import time
from pathlib import Path
from typing import Any, Dict, Optional, List
from SYS.logger import log
@@ -15,6 +17,8 @@ DEFAULT_CONFIG_FILENAME = "config.conf"
SCRIPT_DIR = Path(__file__).resolve().parent
_CONFIG_CACHE: Dict[str, Dict[str, Any]] = {}
_CONFIG_SAVE_MAX_RETRIES = 5
_CONFIG_SAVE_RETRY_DELAY = 0.15
def global_config() -> List[Dict[str, Any]]:
@@ -52,26 +56,36 @@ def get_hydrus_instance(
) -> Optional[Dict[str, Any]]:
"""Get a specific Hydrus instance config by name.
Supports multiple formats:
- Current: config["store"]["hydrusnetwork"][instance_name]
- Legacy: config["storage"]["hydrus"][instance_name]
- Old: config["HydrusNetwork"][instance_name]
Args:
config: Configuration dict
instance_name: Name of the Hydrus instance (default: "home")
Returns:
Dict with access key and URL, or None if not found
Supports modern config plus a fallback when no exact match exists.
"""
# Canonical: config["store"]["hydrusnetwork"]["home"]
store = config.get("store", {})
if isinstance(store, dict):
hydrusnetwork = store.get("hydrusnetwork", {})
if isinstance(hydrusnetwork, dict):
instance = hydrusnetwork.get(instance_name)
if isinstance(instance, dict):
return instance
if not isinstance(store, dict):
return None
hydrusnetwork = store.get("hydrusnetwork", {})
if not isinstance(hydrusnetwork, dict) or not hydrusnetwork:
return None
instance = hydrusnetwork.get(instance_name)
if isinstance(instance, dict):
return instance
target = str(instance_name or "").lower()
for name, conf in hydrusnetwork.items():
if isinstance(conf, dict) and str(name).lower() == target:
return conf
keys = sorted(hydrusnetwork.keys())
for key in keys:
if not str(key or "").startswith("new_"):
candidate = hydrusnetwork.get(key)
if isinstance(candidate, dict):
return candidate
first_key = keys[0]
candidate = hydrusnetwork.get(first_key)
if isinstance(candidate, dict):
return candidate
return None
@@ -349,7 +363,9 @@ def load_config(
def reload_config(
config_dir: Optional[Path] = None, filename: str = DEFAULT_CONFIG_FILENAME
) -> Dict[str, Any]:
cache_key = _make_cache_key(config_dir, filename, None)
base_dir = config_dir or SCRIPT_DIR
config_path = base_dir / filename
cache_key = _make_cache_key(config_dir, filename, config_path)
_CONFIG_CACHE.pop(cache_key, None)
return load_config(config_dir=config_dir, filename=filename)
@@ -359,42 +375,61 @@ def save_config(
config: Dict[str, Any],
config_dir: Optional[Path] = None,
filename: str = DEFAULT_CONFIG_FILENAME,
) -> None:
) -> int:
base_dir = config_dir or SCRIPT_DIR
config_path = base_dir / filename
# 1. Save to Database
try:
from SYS.database import db, save_config_value
def _write_entries() -> int:
count = 0
with db.transaction():
# Replace the table contents so removed entries disappear from the DB.
db.execute("DELETE FROM config")
for key, value in config.items():
if key in ('store', 'provider', 'tool'):
if isinstance(value, dict):
for subtype, instances in value.items():
if isinstance(instances, dict):
# provider/tool are usually config[cat][subtype][key]
# but store is config['store'][subtype][name][key]
if key == 'store':
for name, settings in instances.items():
if isinstance(settings, dict):
for k, v in settings.items():
save_config_value(key, subtype, name, k, v)
count += 1
else:
for k, v in instances.items():
save_config_value(key, subtype, "default", k, v)
count += 1
else:
# global settings
if not key.startswith("_"):
if not key.startswith("_") and value is not None:
save_config_value("global", "none", "none", key, value)
except Exception as e:
log(f"Failed to save config to database: {e}")
count += 1
return count
saved_entries = 0
attempts = 0
while True:
try:
saved_entries = _write_entries()
log(f"Saved {saved_entries} configuration entries to database.")
break
except sqlite3.OperationalError as exc:
attempts += 1
locked_error = "locked" in str(exc).lower()
if not locked_error or attempts >= _CONFIG_SAVE_MAX_RETRIES:
log(f"CRITICAL: Failed to save config to database: {exc}")
raise
delay = _CONFIG_SAVE_RETRY_DELAY * attempts
log(
f"Database busy locking medios.db (attempt {attempts}/{_CONFIG_SAVE_MAX_RETRIES}); retrying in {delay:.2f}s."
)
time.sleep(delay)
except Exception as exc:
log(f"CRITICAL: Failed to save config to database: {exc}")
raise
cache_key = _make_cache_key(config_dir, filename, config_path)
clear_config_cache()
_CONFIG_CACHE[cache_key] = config
return saved_entries
def load() -> Dict[str, Any]:
@@ -402,6 +437,6 @@ def load() -> Dict[str, Any]:
return load_config()
def save(config: Dict[str, Any]) -> None:
def save(config: Dict[str, Any]) -> int:
"""Persist *config* back to disk."""
save_config(config)
return save_config(config)

View File

@@ -3,14 +3,44 @@ from __future__ import annotations
import sqlite3
import json
import threading
import os
from queue import Queue
from pathlib import Path
from typing import Any, Dict, List, Optional
from contextlib import contextmanager
from SYS.logger import log
# The database is located in the project root
ROOT_DIR = Path(__file__).resolve().parent.parent
DB_PATH = ROOT_DIR / "medios.db"
# The database is located in the project root (prefer explicit repo hints).
def _resolve_root_dir() -> Path:
env_root = (
os.environ.get("MM_REPO")
or os.environ.get("MM_ROOT")
or os.environ.get("REPO")
)
if env_root:
try:
candidate = Path(env_root).expanduser().resolve()
if candidate.exists():
return candidate
except Exception:
pass
cwd = Path.cwd().resolve()
for base in [cwd, *cwd.parents]:
if (base / "medios.db").exists():
return base
if (base / "CLI.py").exists():
return base
if (base / "config.conf").exists():
return base
if (base / "scripts").exists() and (base / "SYS").exists():
return base
return Path(__file__).resolve().parent.parent
ROOT_DIR = _resolve_root_dir()
DB_PATH = (ROOT_DIR / "medios.db").resolve()
class Database:
_instance: Optional[Database] = None
@@ -22,8 +52,15 @@ class Database:
return cls._instance
def _init_db(self):
self.db_path = DB_PATH
db_existed = self.db_path.exists()
if db_existed:
log(f"Opening existing medios.db at {self.db_path}")
else:
log(f"Creating medios.db at {self.db_path}")
self.conn = sqlite3.connect(
str(DB_PATH),
str(self.db_path),
check_same_thread=False,
timeout=30.0 # Increase timeout to 30s to avoid locking issues
)