This commit is contained in:
2026-01-30 12:04:37 -08:00
parent ab94c57244
commit e57dcf2190
6 changed files with 462 additions and 275 deletions

View File

@@ -154,6 +154,7 @@ class ConfigModal(ModalScreen):
yield ListItem(Label("Global Settings"), id="cat-globals")
yield ListItem(Label("Stores"), id="cat-stores")
yield ListItem(Label("Providers"), id="cat-providers")
yield ListItem(Label("Tools"), id="cat-tools")
with Vertical(id="config-content"):
yield ScrollableContainer(id="fields-container")
@@ -161,14 +162,17 @@ class ConfigModal(ModalScreen):
yield Button("Save", variant="success", id="save-btn")
yield Button("Add Store", variant="primary", id="add-store-btn")
yield Button("Add Provider", variant="primary", id="add-provider-btn")
yield Button("Add Tool", variant="primary", id="add-tool-btn")
yield Button("Back", id="back-btn")
yield Button("Restore Backup", id="restore-backup-btn")
yield Button("Copy DB Path", id="copy-db-btn")
yield Button("Close", variant="error", id="cancel-btn")
def on_mount(self) -> None:
self.query_one("#add-store-btn", Button).display = False
self.query_one("#add-provider-btn", Button).display = False
try:
self.query_one("#add-tool-btn", Button).display = False
except Exception:
pass
# Update DB path and last-saved on mount
try:
self.query_one("#config-db-path", Static).update(self._db_path)
@@ -213,6 +217,7 @@ class ConfigModal(ModalScreen):
try:
self.query_one("#add-store-btn", Button).display = (self.current_category == "stores" and self.editing_item_name is None)
self.query_one("#add-provider-btn", Button).display = (self.current_category == "providers" and self.editing_item_name is None)
self.query_one("#add-tool-btn", Button).display = (self.current_category == "tools" and self.editing_item_name is None)
self.query_one("#back-btn", Button).display = (self.editing_item_name is not None)
self.query_one("#save-btn", Button).display = (self.editing_item_name is not None or self.current_category == "globals")
except Exception:
@@ -238,6 +243,8 @@ class ConfigModal(ModalScreen):
self.render_stores(container)
elif self.current_category == "providers":
self.render_providers(container)
elif self.current_category == "tools":
self.render_tools(container)
self.call_after_refresh(do_mount)
@@ -354,6 +361,26 @@ class ConfigModal(ModalScreen):
)
container.mount(row)
def render_tools(self, container: ScrollableContainer) -> None:
container.mount(Label("Configured Tools", classes="config-label"))
tools = self.config_data.get("tool", {})
if not tools:
container.mount(Static("No tools configured."))
else:
for i, (name, _) in enumerate(tools.items()):
edit_id = f"edit-tool-{i}"
del_id = f"del-tool-{i}"
self._button_id_map[edit_id] = ("edit", "tool", name)
self._button_id_map[del_id] = ("del", "tool", name)
row = Horizontal(
Static(name, classes="item-label"),
Button("Edit", id=edit_id),
Button("Delete", variant="error", id=del_id),
classes="item-row"
)
container.mount(row)
def render_item_editor(self, container: ScrollableContainer) -> None:
item_type = str(self.editing_item_type or "")
item_name = str(self.editing_item_name or "")
@@ -392,6 +419,18 @@ class ConfigModal(ModalScreen):
provider_schema_map[k.upper()] = field_def
except Exception:
pass
# Fetch Tool schema
if item_type == "tool":
try:
import importlib
mod = importlib.import_module(f"tool.{item_name}")
if hasattr(mod, "config_schema") and callable(mod.config_schema):
for field_def in mod.config_schema():
k = field_def.get("key")
if k:
provider_schema_map[k.upper()] = field_def
except Exception:
pass
# Use columns for better layout of inputs with paste buttons
container.mount(Label("Edit Settings"))
@@ -569,6 +608,8 @@ class ConfigModal(ModalScreen):
self.current_category = "stores"
elif event.item.id == "cat-providers":
self.current_category = "providers"
elif event.item.id == "cat-tools":
self.current_category = "tools"
self.editing_item_name = None
self.editing_item_type = None
@@ -645,6 +686,10 @@ class ConfigModal(ModalScreen):
removed = True
if str(name).strip().lower() == "alldebrid":
self._remove_alldebrid_store_entry()
elif itype == "tool":
if "tool" in self.config_data and name in self.config_data["tool"]:
del self.config_data["tool"][name]
removed = True
if removed:
try:
saved = self.save_all()
@@ -676,31 +721,33 @@ class ConfigModal(ModalScreen):
except Exception:
pass
self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected)
elif bid == "add-tool-btn":
# Discover tool modules that advertise a config_schema()
options = []
try:
import pkgutil
import importlib
import tool as _tool_pkg
for _mod in pkgutil.iter_modules(_tool_pkg.__path__):
try:
mod = importlib.import_module(f"tool.{_mod.name}")
if hasattr(mod, "config_schema") and callable(mod.config_schema):
options.append(_mod.name)
except Exception:
continue
except Exception:
# Fallback to known entry
options = ["ytdlp"]
if options:
options.sort()
self.app.push_screen(SelectionModal("Select Tool", options), callback=self.on_tool_type_selected)
elif bid == "matrix-test-btn":
self._request_matrix_test()
elif bid == "matrix-rooms-btn":
self._open_matrix_room_picker()
elif bid == "restore-backup-btn":
try:
backup = self._get_last_backup_path()
if not backup:
self.notify("No backups available", severity="warning")
else:
# Ask for confirmation via a simple notification and perform restore
self.notify(f"Restoring {backup.name}...", timeout=2)
self._restore_backup_background(str(backup))
except Exception as exc:
self.notify(f"Restore failed: {exc}", severity="error")
elif bid == "copy-db-btn":
try:
if hasattr(self.app, "copy_to_clipboard"):
self.app.copy_to_clipboard(str(db.db_path))
self.notify("DB path copied to clipboard")
else:
# Fall back to a visible notification
self.notify(str(db.db_path))
except Exception:
self.notify("Failed to copy DB path", severity="warning")
# Restore UI removed: backups/audit remain available programmatically
elif bid.startswith("paste-"):
# Programmatic paste button
target_id = bid.replace("paste-", "")
@@ -735,64 +782,7 @@ class ConfigModal(ModalScreen):
else:
self.notify("Clipboard not supported in this terminal", severity="warning")
def _get_last_backup_path(self):
try:
backup_dir = Path(db.db_path).with_name("config_backups")
if not backup_dir.exists():
return None
files = sorted(backup_dir.glob("medios-backup-*.db"), key=lambda p: p.stat().st_mtime, reverse=True)
return files[0] if files else None
except Exception:
return None
@work(thread=True)
def _restore_backup_background(self, backup_path: str) -> None:
try:
import sqlite3, json
cfg = {}
with sqlite3.connect(backup_path) as conn:
cur = conn.cursor()
cur.execute("SELECT category, subtype, item_name, key, value FROM config")
rows = cur.fetchall()
for cat, sub, name, key, val in rows:
try:
parsed = json.loads(val)
except Exception:
parsed = val
if cat == 'global':
cfg[key] = parsed
elif cat in ('provider', 'tool'):
cd = cfg.setdefault(cat, {})
sd = cd.setdefault(sub, {})
sd[key] = parsed
elif cat == 'store':
cd = cfg.setdefault('store', {})
sd = cd.setdefault(sub, {})
nd = sd.setdefault(name, {})
nd[key] = parsed
else:
cfg.setdefault(cat, {})[key] = parsed
# Persist restored config using save_config
from SYS.config import save_config, reload_config
saved = save_config(cfg)
# Reload and update UI from main thread
self.app.call_from_thread(self._on_restore_complete, True, backup_path, saved)
except Exception as exc:
self.app.call_from_thread(self._on_restore_complete, False, backup_path, str(exc))
def _on_restore_complete(self, success: bool, backup_path: str, saved_or_error):
if success:
# Refresh our in-memory view and UI
try:
from SYS.config import reload_config
self.config_data = reload_config()
self.refresh_view()
except Exception:
pass
self.notify(f"Restore complete: re-saved {saved_or_error} entries from {Path(backup_path).name}")
else:
self.notify(f"Restore failed: {saved_or_error}", severity="error")
# Backup/restore helpers removed: forensics/audit mode disabled and restore UI removed.
def on_store_type_selected(self, stype: str) -> None:
if not stype:
@@ -878,6 +868,31 @@ class ConfigModal(ModalScreen):
self.editing_item_name = ptype
self.refresh_view()
def on_tool_type_selected(self, tname: str) -> None:
if not tname:
return
self._capture_editor_snapshot()
if "tool" not in self.config_data:
self.config_data["tool"] = {}
if tname not in self.config_data["tool"]:
new_config = {}
try:
import importlib
mod = importlib.import_module(f"tool.{tname}")
if hasattr(mod, "config_schema") and callable(mod.config_schema):
for field_def in mod.config_schema():
key = field_def.get("key")
if key:
new_config[key] = field_def.get("default", "")
except Exception:
pass
self.config_data["tool"][tname] = new_config
self.editing_item_type = "tool"
self.editing_item_name = tname
self.refresh_view()
def _update_config_value(self, widget_id: str, value: Any) -> None:
if widget_id not in self._input_id_map:
return
@@ -1282,6 +1297,19 @@ class ConfigModal(ModalScreen):
except Exception:
pass
section = self.config_data.get("provider", {}).get(item_name, {})
elif item_type == "tool":
try:
import importlib
mod = importlib.import_module(f"tool.{item_name}")
if hasattr(mod, "config_schema") and callable(mod.config_schema):
for field_def in mod.config_schema():
if field_def.get("required"):
k = field_def.get("key")
if k and k not in required_keys:
required_keys.append(k)
except Exception:
pass
section = self.config_data.get("tool", {}).get(item_name, {})
# Check required keys
for rk in required_keys: