This commit is contained in:
2025-12-30 23:19:02 -08:00
parent a97657a757
commit 3bbaa28fb4
17 changed files with 1735 additions and 558 deletions

177
CLI.py
View File

@@ -808,14 +808,6 @@ class CmdletIntrospection:
provider_choices: List[str] = []
if canonical_cmd in {"search-provider"
} and list_search_providers is not None:
providers = list_search_providers(config) or {}
available = [
name for name, is_ready in providers.items() if is_ready
]
return sorted(available) if available else sorted(providers.keys())
if canonical_cmd in {"add-file"} and list_file_providers is not None:
providers = list_file_providers(config) or {}
available = [
@@ -1298,48 +1290,6 @@ class CmdletExecutor:
emitted_items: Optional[List[Any]] = None,
cmd_args: Optional[List[str]] = None,
) -> str:
if cmd_name in ("search-provider", "search_provider") and cmd_args:
provider: str = ""
query: str = ""
tokens = [str(a) for a in (cmd_args or [])]
pos: List[str] = []
i = 0
while i < len(tokens):
low = tokens[i].lower()
if low in {"-provider",
"--provider"} and i + 1 < len(tokens):
provider = str(tokens[i + 1]).strip()
i += 2
continue
if low in {"-query",
"--query"} and i + 1 < len(tokens):
query = str(tokens[i + 1]).strip()
i += 2
continue
if low in {"-limit",
"--limit"} and i + 1 < len(tokens):
i += 2
continue
if not str(tokens[i]).startswith("-"):
pos.append(str(tokens[i]))
i += 1
if not provider and pos:
provider = str(pos[0]).strip()
pos = pos[1:]
if not query and pos:
query = " ".join(pos).strip()
if provider and query:
provider_lower = provider.lower()
if provider_lower == "youtube":
provider_label = "Youtube"
elif provider_lower == "openlibrary":
provider_label = "OpenLibrary"
else:
provider_label = provider[:1].upper() + provider[1:]
return f"{provider_label}: {query}".strip().rstrip(":")
title_map = {
"search-file": "Results",
"search_file": "Results",
@@ -1807,10 +1757,6 @@ class CmdletExecutor:
"tags",
"search-file",
"search_file",
"search-provider",
"search_provider",
"search-store",
"search_store",
}
if cmd_name in self_managing_commands:
@@ -3029,8 +2975,6 @@ class PipelineExecutor:
stage_is_last = (stage_index + 1 >= len(stages))
if filter_spec is not None and stage_is_last:
try:
from SYS.result_table import ResultTable
base_table = stage_table
if base_table is None:
base_table = ctx.get_last_result_table()
@@ -3801,6 +3745,15 @@ class MedeiaCLI:
def __init__(self) -> None:
self._config_loader = ConfigLoader(root=self.ROOT)
# Optional dependency auto-install for configured tools (best-effort).
try:
from SYS.optional_deps import maybe_auto_install_configured_tools
maybe_auto_install_configured_tools(self._config_loader.load())
except Exception:
pass
self._cmdlet_executor = CmdletExecutor(config_loader=self._config_loader)
self._pipeline_executor = PipelineExecutor(config_loader=self._config_loader)
@@ -3833,54 +3786,6 @@ class MedeiaCLI:
pass
return value
def _complete_search_provider(ctx, param, incomplete: str): # pragma: no cover
try:
from click.shell_completion import CompletionItem
except Exception:
return []
try:
from ProviderCore.registry import list_search_providers
providers = list_search_providers(self._config_loader.load()) or {}
available = [n for n, ok in providers.items() if ok]
choices = sorted(available) if available else sorted(providers.keys())
except Exception:
choices = []
inc = (incomplete or "").lower()
return [
CompletionItem(name) for name in choices
if name and name.lower().startswith(inc)
]
@app.command("search-provider")
def search_provider(
provider: str = typer.Option(
...,
"--provider",
"-p",
help="Provider name (bandcamp, libgen, soulseek, youtube)",
shell_complete=_complete_search_provider,
),
query: str = typer.Argument(...,
help="Search query (quote for spaces)"),
limit: int = typer.Option(
36,
"--limit",
"-l",
help="Maximum results to return"
),
) -> None:
self._cmdlet_executor.execute(
"search-provider",
["-provider",
provider,
query,
"-limit",
str(limit)]
)
@app.command("pipeline")
def pipeline(
command: str = typer.Option(
@@ -3942,7 +3847,7 @@ class MedeiaCLI:
if ctx.invoked_subcommand is None:
self.run_repl()
_ = (search_provider, pipeline, repl, main_callback)
_ = (pipeline, repl, main_callback)
# Dynamically register all cmdlets as top-level Typer commands so users can
# invoke `mm <cmdlet> [args]` directly from the shell. We use Click/Typer
@@ -3950,9 +3855,7 @@ class MedeiaCLI:
# the cmdlet system without Typer trying to parse them.
try:
names = list_cmdlet_names()
skip = {"search-provider",
"pipeline",
"repl"}
skip = {"pipeline", "repl"}
for nm in names:
if not nm or nm in skip:
continue
@@ -3964,7 +3867,7 @@ class MedeiaCLI:
cmd_name,
context_settings={
"ignore_unknown_options": True,
"allow_extra_args": True
"allow_extra_args": True,
},
)
def _handler(ctx: typer.Context):
@@ -4119,6 +4022,13 @@ Come to love it when others take what you share, as there is no greater joy
block = provider_cfg.get(str(name).strip().lower())
return isinstance(block, dict) and bool(block)
def _has_tool(cfg: dict, name: str) -> bool:
tool_cfg = cfg.get("tool")
if not isinstance(tool_cfg, dict):
return False
block = tool_cfg.get(str(name).strip().lower())
return isinstance(block, dict) and bool(block)
def _ping_url(url: str, timeout: float = 3.0) -> tuple[bool, str]:
try:
from API.HTTP import HTTPClient
@@ -4542,6 +4452,45 @@ Come to love it when others take what you share, as there is no greater joy
except Exception as exc:
_add_startup_check("ERROR", "Cookies", detail=str(exc))
# Tool checks (configured via [tool=...])
if _has_tool(config, "florencevision"):
try:
tool_cfg = config.get("tool")
fv_cfg = tool_cfg.get("florencevision") if isinstance(tool_cfg, dict) else None
enabled = bool(fv_cfg.get("enabled")) if isinstance(fv_cfg, dict) else False
if not enabled:
_add_startup_check(
"DISABLED",
"FlorenceVision",
provider="tool",
detail="Not enabled",
)
else:
from SYS.optional_deps import florencevision_missing_modules
missing = florencevision_missing_modules()
if missing:
_add_startup_check(
"DISABLED",
"FlorenceVision",
provider="tool",
detail="Missing: " + ", ".join(missing),
)
else:
_add_startup_check(
"ENABLED",
"FlorenceVision",
provider="tool",
detail="Ready",
)
except Exception as exc:
_add_startup_check(
"DISABLED",
"FlorenceVision",
provider="tool",
detail=str(exc),
)
if startup_table.rows:
stdout_console().print()
stdout_console().print(startup_table)
@@ -4709,7 +4658,7 @@ Come to love it when others take what you share, as there is no greater joy
if last_table is None:
last_table = ctx.get_last_result_table()
# Auto-refresh search-store tables when navigating back,
# Auto-refresh search-file tables when navigating back,
# so row payloads (titles/tags) reflect latest store state.
try:
src_cmd = (
@@ -4720,7 +4669,7 @@ Come to love it when others take what you share, as there is no greater joy
if (isinstance(src_cmd,
str)
and src_cmd.lower().replace("_",
"-") == "search-store"):
"-") == "search-file"):
src_args = (
getattr(last_table,
"source_args",
@@ -4748,7 +4697,7 @@ Come to love it when others take what you share, as there is no greater joy
else:
ctx.set_current_command_text(
" ".join(
["search-store",
["search-file",
*cleaned_args]
).strip()
)
@@ -4756,7 +4705,7 @@ Come to love it when others take what you share, as there is no greater joy
pass
try:
self._cmdlet_executor.execute(
"search-store",
"search-file",
cleaned_args + ["--refresh"]
)
finally:
@@ -4768,7 +4717,7 @@ Come to love it when others take what you share, as there is no greater joy
continue
except Exception as exc:
print(
f"Error refreshing search-store table: {exc}",
f"Error refreshing search-file table: {exc}",
file=sys.stderr
)