refactor(download): remove ProviderCore/download.py, move sanitize_filename to SYS.utils, replace callers to use API.HTTP.HTTPClient

This commit is contained in:
2026-01-06 01:38:59 -08:00
parent 3b363dd536
commit 41c11d39fd
38 changed files with 2640 additions and 526 deletions

View File

@@ -219,17 +219,18 @@ class SharedArgs:
SharedArgs.STORE.choices = SharedArgs.get_store_choices(config)
"""
try:
from Store import Store
# Use the non-instantiating helper so autocomplete doesn't trigger backend init.
from Store.registry import list_configured_backend_names
# If no config provided, try to load it
if config is None:
try:
from SYS.config import load_config
config = load_config()
except Exception:
return []
store = Store(config)
return store.list_backends()
return list_configured_backend_names(config)
except Exception:
# Fallback to empty list if FileStorage isn't available
return []

View File

@@ -321,9 +321,11 @@ class Add_File(Cmdlet):
is_storage_backend_location = False
if location:
try:
store_probe = Store(config)
# Use a config-only check to avoid instantiating backends (which may perform network checks).
from Store.registry import list_configured_backend_names
is_storage_backend_location = location in (
store_probe.list_backends() or []
list_configured_backend_names(config) or []
)
except Exception:
is_storage_backend_location = False

View File

@@ -70,6 +70,7 @@ class Download_File(Cmdlet):
"download-http"],
arg=[
SharedArgs.URL,
SharedArgs.PROVIDER,
SharedArgs.PATH,
SharedArgs.QUERY,
# Prefer -path for output directory to match other cmdlets; keep -output for backwards compatibility.
@@ -121,6 +122,7 @@ class Download_File(Cmdlet):
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Main execution method."""
debug(f"[download-file] run invoked with args: {list(args)}")
return self._run_impl(result, args, config)
@staticmethod
@@ -889,7 +891,7 @@ class Download_File(Cmdlet):
return expanded_items
def _process_provider_items(
def _process_provider_items(self,
*,
piped_items: Sequence[Any],
final_output_dir: Path,

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from typing import Any, Dict, Iterable, Optional, Sequence
from pathlib import Path
import sys
from typing import Any, Dict, Iterable, Sequence
from . import _shared as sh
from SYS.logger import log, debug
@@ -68,47 +68,34 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
return 1
items = inputs
# Build rows
try:
rows = list(provider.adapter(items))
table = provider.build_table(items)
except Exception as exc:
log(f"Provider adapter failed: {exc}", file=sys.stderr)
log(f"Provider '{provider.name}' failed: {exc}", file=sys.stderr)
return 1
cols = provider.get_columns(rows)
# Emit rows for downstream pipeline consumption (pipable behavior).
try:
for r in rows:
for item in provider.serialize_rows(table.rows):
try:
item = {
"title": getattr(r, "title", None) or None,
"path": getattr(r, "path", None) or None,
"ext": getattr(r, "ext", None) or None,
"size_bytes": getattr(r, "size_bytes", None) or None,
"metadata": getattr(r, "metadata", None) or {},
"source": getattr(r, "source", None) or provider.name,
"_selection_args": provider.selection_args(r),
}
ctx.emit(item)
except Exception:
# Best-effort: continue emitting other rows
continue
except Exception:
# Non-fatal: continue to rendering even if emission fails
# Non-fatal: rendering still happens
pass
# Render using RichRenderer
try:
table = RichRenderer().render(rows, cols, provider.metadata)
renderable = RichRenderer().render(table.rows, table.columns, table.meta)
try:
from rich.console import Console
Console().print(table)
Console().print(renderable)
except Exception:
# Fallback to simple printing
for r in rows:
print(" ".join(str((c.extractor(r) or "")) for c in cols))
for r in table.rows:
print(" ".join(str((c.extractor(r) or "")) for c in table.columns))
except Exception as exc:
log(f"Rendering failed: {exc}", file=sys.stderr)
return 1
@@ -123,11 +110,11 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
log("Invalid -select value; must be an integer", file=sys.stderr)
return 1
if select_idx < 0 or select_idx >= len(rows):
if select_idx < 0 or select_idx >= len(table.rows):
log("-select out of range", file=sys.stderr)
return 1
selected = rows[select_idx]
selected = table.rows[select_idx]
sel_args = provider.selection_args(selected)
if not run_cmd:

View File

@@ -40,7 +40,7 @@ from SYS import pipeline as pipeline_context
# Playwright & Screenshot Dependencies
# ============================================================================
from tool.playwright import HAS_PLAYWRIGHT, PlaywrightTimeoutError, PlaywrightTool
from tool.playwright import PlaywrightTimeoutError, PlaywrightTool
try:
from SYS.config import resolve_output_dir
@@ -853,12 +853,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
log(f"Cmdlet: {CMDLET.name}\nSummary: {CMDLET.summary}\nUsage: {CMDLET.usage}")
return 0
if not HAS_PLAYWRIGHT:
log(
"playwright is required for screenshot capture; install with: pip install playwright; then: playwright install",
file=sys.stderr,
)
return 1
progress = PipelineProgress(pipeline_context)

View File

@@ -241,6 +241,16 @@ class search_file(Cmdlet):
else:
provider_label = provider_text[:1].upper() + provider_text[1:] if provider_text else "Provider"
normalized_query = str(query or "").strip()
provider_filters: Dict[str, Any] = {}
try:
normalized_query, provider_filters = provider.extract_query_arguments(query)
except Exception:
provider_filters = {}
normalized_query = (normalized_query or "").strip()
query = normalized_query or "*"
provider_filters = dict(provider_filters or {})
if provider_lower == "alldebrid" and effective_open_id is not None:
table_title = f"{provider_label} Files: {effective_open_id}".strip().rstrip(":")
else:
@@ -267,17 +277,22 @@ class search_file(Cmdlet):
table.set_table_metadata(table_meta)
except Exception:
pass
table.set_source_command("search-file", list(args_list))
debug(f"[search-file] Calling {provider_name}.search()")
if provider_lower == "alldebrid":
filters = {"view": "folders"}
search_open_id = parsed_open_id if parsed_open_id is not None else open_id
if search_open_id is not None:
filters = {"view": "files", "magnet_id": search_open_id}
results = provider.search(query, limit=limit, filters=filters)
if provider_lower == "vimm":
# Keep auto-staged download-file from inheriting raw query tokens;
# only propagate provider hint so @N expands to a clean downloader call.
table.set_source_command("search-file", ["-provider", provider_name])
else:
results = provider.search(query, limit=limit)
table.set_source_command("search-file", list(args_list))
search_filters = dict(provider_filters)
debug(f"[search-file] Calling {provider_name}.search(filters={search_filters})")
if provider_lower == "alldebrid":
search_open_id = parsed_open_id if parsed_open_id is not None else open_id
view_value = "files" if search_open_id is not None else "folders"
search_filters["view"] = view_value
if search_open_id is not None:
search_filters["magnet_id"] = search_open_id
results = provider.search(query, limit=limit, filters=search_filters or None)
debug(f"[search-file] {provider_name} -> {len(results or [])} result(s)")
# HIFI artist UX: if there is exactly one artist match, auto-expand
@@ -342,6 +357,10 @@ class search_file(Cmdlet):
if "table" not in item_dict:
item_dict["table"] = table_type
# Ensure provider source is present so downstream cmdlets (select) can resolve provider
if "source" not in item_dict:
item_dict["source"] = provider_name
row_index = len(table.rows)
table.add_result(search_result)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import sys
from typing import Any, Dict, List, Sequence
from . import _shared as sh
from SYS.logger import log, debug
@@ -89,28 +90,22 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
log("No input provided to select; pipe provider-table output or use a cmdlet that emits items.", file=sys.stderr)
return 1
first_src = inputs[0].get("source") if isinstance(inputs[0], dict) else None
if not first_src:
log("Input items must include 'source' to resolve provider for selection.", file=sys.stderr)
return 1
try:
provider = get_provider(first_src)
except Exception:
log(f"Unknown provider: {first_src}", file=sys.stderr)
return 1
# Model-ize items
rows = [_dict_to_result_model(item if isinstance(item, dict) else item) for item in inputs]
# Attempt to detect provider from first item
provider = None
first_src = inputs[0].get("source") if isinstance(inputs[0], dict) else None
if first_src:
try:
provider = get_provider(first_src)
except Exception:
provider = None
# Columns: ask provider for column spec if available, else build minimal columns
if provider:
cols = provider.get_columns(rows)
else:
# Minimal columns built from available keys
from SYS.result_table_api import title_column, ext_column
cols = [title_column()]
if any(r.ext for r in rows):
cols.append(ext_column())
# Columns: provider must supply them (no legacy defaults)
cols = provider.get_columns(rows)
# Render table to console
try:
@@ -172,26 +167,19 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"source": raw.source,
}
else:
# try to call to_dict or fallback
try:
selected = raw.to_dict()
except Exception:
selected = {"title": getattr(raw, "title", str(raw))}
# Ensure selection args exist
# Ensure selection args exist using provider's selector only
if not selected.get("_selection_args"):
if provider:
try:
sel_args = provider.selection_args(rows[idx])
selected["_selection_args"] = sel_args
except Exception:
selected["_selection_args"] = []
else:
# fallback
if selected.get("path"):
selected["_selection_args"] = ["-path", selected.get("path")]
else:
selected["_selection_args"] = ["-title", selected.get("title") or ""]
try:
sel_args = provider.selection_args(rows[idx])
selected["_selection_args"] = sel_args
except Exception:
log("Selection args missing and provider selector failed.", file=sys.stderr)
return 1
selected_items.append(selected)
except Exception: