update and cleanup repo

This commit is contained in:
2026-05-26 15:32:01 -07:00
parent 5041d9fbb9
commit 0db899d0c3
72 changed files with 788 additions and 1884 deletions
+5 -776
View File
@@ -17,7 +17,6 @@ from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
from SYS.logger import log, debug, debug_panel
from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
from dataclasses import dataclass, field
from SYS import models
from SYS import pipeline as pipeline_context
from SYS.item_accessors import get_field as _item_accessor_get_field
@@ -25,533 +24,11 @@ from SYS.payload_builders import build_file_result_payload, build_table_result_p
from SYS.result_publication import publish_result_table
from SYS.result_table import Table
from SYS.rich_display import stderr_console as get_stderr_console
from SYS.cmdlet_spec import Cmdlet, CmdletArg, QueryArg, SharedArgs, parse_cmdlet_args
from rich.prompt import Confirm
from contextlib import AbstractContextManager, nullcontext
@dataclass
class CmdletArg:
"""Represents a single cmdlet argument with optional enum choices."""
name: str
"""Argument name, e.g., '-path' or 'location'"""
type: str = "string"
"""Argument type: 'string', 'int', 'flag', 'enum', etc."""
required: bool = False
"""Whether this argument is required"""
description: str = ""
"""Human-readable description of the argument"""
choices: List[str] = field(default_factory=list)
"""Optional list of valid choices for enum/autocomplete, e.g., ['hydrus', 'local', '0x0.st']"""
alias: str = ""
"""Optional alias for the argument name, e.g., 'loc' for 'location'"""
handler: Optional[Any] = None
"""Optional handler function/callable for processing this argument's value"""
variadic: bool = False
"""Whether this argument accepts multiple values (consumes remaining positional args)"""
usage: str = ""
"""dsf"""
requires_db: bool = False
"""Whether this argument requires the local DB/library root to be configured."""
# Query-mapping support:
# Some cmdlets use a unified `-query` string. When configured, individual args
# can be populated from fields inside `-query` (e.g., -query "hash:<sha256>").
query_key: Optional[str] = None
"""Field name inside -query that maps to this argument (e.g., 'hash')."""
query_aliases: List[str] = field(default_factory=list)
"""Additional field names inside -query that map to this argument."""
query_only: bool = False
"""When True, do not accept a dedicated CLI flag for this arg; only map from -query."""
def resolve(self, value: Any) -> Any:
"""Resolve/process the argument value using the handler if available.
Args:
value: The raw argument value to process
Returns:
Processed value from handler, or original value if no handler
Example:
# For STORAGE arg with a handler
storage_path = SharedArgs.STORAGE.resolve('local') # Returns Path(tempfile.gettempdir())
"""
if self.handler is not None and callable(self.handler):
return self.handler(value)
return value
def to_flags(self) -> tuple[str, ...]:
"""Generate all flag variants (short and long form) for this argument.
Returns a tuple of all valid flag forms for this argument, including:
- Long form with double dash: --name
- Single dash multi-char form: -name (for convenience)
- Short form with single dash: -alias (if alias exists)
For flags, also generates negation forms:
- --no-name, -name (negation of multi-char form)
- --no-name, -nalias (negation with alias)
Returns:
Tuple of flag strings, e.g., ('--archive', '-archive', '-arch')
or for flags: ('--archive', '-archive', '-arch', '--no-archive', '-narch')
Example:
archive_flags = SharedArgs.ARCHIVE.to_flags()
# Returns: ('--archive', '-archive', '-arch', '--no-archive', '-narch')
storage_flags = SharedArgs.STORAGE.to_flags()
# Returns: ('--storage', '-storage', '-s')
"""
normalized_name = str(self.name or "").lstrip("-")
if not normalized_name:
return tuple()
flags = [
f"--{normalized_name}",
f"-{normalized_name}"
] # Both double-dash and single-dash variants
# Add short form if alias exists
if self.alias:
flags.append(f"-{self.alias}")
# Add negation forms for flag type
if self.type == "flag":
flags.append(f"--no-{normalized_name}")
flags.append(f"-no{normalized_name}") # Single-dash negation variant
if self.alias:
flags.append(f"-n{self.alias}")
return tuple(flags)
def QueryArg(
name: str,
*,
key: Optional[str] = None,
aliases: Optional[Sequence[str]] = None,
type: str = "string",
required: bool = False,
description: str = "",
choices: Optional[Sequence[str]] = None,
handler: Optional[Any] = None,
query_only: bool = True,
) -> CmdletArg:
"""Create an argument that can be populated from `-query` fields.
By default, this does NOT create a dedicated flag (query_only=True). This is
useful for deprecating bloat flags like `-hash` while still making `hash:` a
first-class, documented, reusable field.
"""
return CmdletArg(
name=str(name),
type=str(type or "string"),
required=bool(required),
description=str(description or ""),
choices=list(choices or []),
handler=handler,
query_key=str(key or name).strip().lower()
if str(key or name).strip() else None,
query_aliases=[
str(a).strip().lower() for a in (aliases or []) if str(a).strip()
],
query_only=bool(query_only),
)
# ============================================================================
# SHARED ARGUMENTS - Reusable argument definitions across cmdlet
# ============================================================================
class SharedArgs:
"""Registry of shared CmdletArg definitions used across multiple cmdlet.
This class provides a centralized location for common arguments so they're
defined once and used consistently everywhere. Reduces duplication and ensures
all cmdlet handle the same arguments identically.
Example:
CMDLET = Cmdlet(
name="my-cmdlet",
summary="Does something",
usage="my-cmdlet",
args=[
SharedArgs.QUERY, # Use predefined shared arg (e.g., -query "hash:<sha256>")
SharedArgs.LOCATION, # Use another shared arg
CmdletArg(...), # Mix with custom args
]
)
"""
# NOTE: This project no longer exposes a dedicated -hash flag.
# Use SharedArgs.QUERY with `hash:` syntax instead (e.g., -query "hash:<sha256>").
STORE = CmdletArg(
name="store",
type="enum",
choices=[], # Dynamically populated via get_store_choices()
description="Selects a storage backend",
query_key="store",
)
INSTANCE = CmdletArg(
name="instance",
type="string",
description="Selects a plugin instance",
query_key="instance",
)
URL = CmdletArg(
name="url",
type="string",
description="http parser",
)
PLUGIN = CmdletArg(
name="plugin",
type="string",
description="selects plugin",
)
@staticmethod
def get_store_choices(config: Optional[Dict[str, Any]] = None, force: bool = False) -> List[str]:
"""Get list of available store backend names.
This method returns the cached list of available backends from the most
recent startup check. Stores that failed to initialize are filtered out.
Users must restart to refresh the list if stores are enabled/disabled.
Args:
config: Optional config dict. Used if force=True or no cache exists.
force: If True, force a fresh check of the backends.
Returns:
List of backend names (e.g., ['default', 'test', 'home', 'work'])
Only includes backends that successfully initialized at startup.
Example:
SharedArgs.INSTANCE.choices = SharedArgs.get_store_choices(config)
"""
# Use the cached startup check result if available (unless force=True)
if not force and hasattr(SharedArgs, "_cached_available_stores"):
return SharedArgs._cached_available_stores or []
# Autocomplete and shared arg choices must only expose backends that actually
# initialized successfully. Do a full refresh when the cache is missing.
SharedArgs._refresh_store_choices_cache(config, skip_instantiation=False)
return SharedArgs._cached_available_stores or []
@staticmethod
def _refresh_store_choices_cache(config: Optional[Dict[str, Any]] = None, skip_instantiation: bool = False) -> None:
"""Refresh the cached store choices list. Should be called once at startup.
Store choices are user-facing and should only include backends that actually
initialized successfully. When `skip_instantiation` is True, this method keeps
the cache empty rather than surfacing configured-but-disabled store names.
Args:
config: Config dict. If not provided, will try to load from config module.
skip_instantiation: When True, do not instantiate backend classes; use a lightweight list only.
"""
try:
if config is None:
try:
from SYS.config import load_config
config = load_config(emit_summary=False)
except Exception:
SharedArgs._cached_available_stores = []
return
SharedArgs._cached_available_stores = []
# If caller requested a lightweight pass, avoid exposing configured names
# that may be disabled or unavailable.
if skip_instantiation:
return
names: set[str] = set()
# Plugin-based multi-instance backends (config["plugin"] / config["provider"] sections)
try:
from PluginCore.registry import REGISTRY
plugin_instances = REGISTRY.list_storage_plugin_instances(config)
for _plugin_name, instance_names in plugin_instances.items():
names.update(instance_names)
except Exception:
pass
if names:
SharedArgs._cached_available_stores = sorted(names)
except Exception:
SharedArgs._cached_available_stores = []
LOCATION = CmdletArg(
"location",
type="enum",
choices=["hydrus",
"0x0"],
required=True,
description="Destination location",
)
DELETE = CmdletArg(
"delete",
type="flag",
description="Delete the file after successful operation.",
)
# Metadata arguments
ARTIST = CmdletArg(
"artist",
type="string",
description="Filter by artist name (case-insensitive, partial match).",
)
ALBUM = CmdletArg(
"album",
type="string",
description="Filter by album name (case-insensitive, partial match).",
)
TRACK = CmdletArg(
"track",
type="string",
description="Filter by track title (case-insensitive, partial match).",
)
# Library/Search arguments
LIBRARY = CmdletArg(
"library",
type="string",
choices=["hydrus",
"local",
"soulseek",
"libgen",
"ftp"],
description="Search library or source location.",
)
TIMEOUT = CmdletArg(
"timeout",
type="integer",
description="Search or operation timeout in seconds."
)
LIMIT = CmdletArg(
"limit",
type="integer",
description="Maximum number of results to return."
)
# Path/File arguments
PATH = CmdletArg("path", type="string", description="File or directory path.")
# Generic arguments
QUERY = CmdletArg(
"query",
type="string",
description="Unified query string (e.g., hash:<sha256>, hash:{<h1>,<h2>}).",
)
REASON = CmdletArg(
"reason",
type="string",
description="Reason or explanation for the operation."
)
ARCHIVE = CmdletArg(
"archive",
type="flag",
description=
"Archive the URL to Wayback Machine, Archive.today, and Archive.ph (requires URL argument in cmdlet).",
alias="arch",
)
@staticmethod
def resolve_storage(
storage_value: Optional[str],
default: Optional[Path] = None
) -> Path:
"""Resolve a storage location name to a filesystem Path.
Maps storage identifiers to their actual filesystem paths.
This project has been refactored to use system temporary directories
for all staging/downloads by default.
Args:
storage_value: One of 'hydrus', 'local', 'ftp', or None (currently unified to temp)
default: Path to return if storage_value is None (defaults to temp directory)
Returns:
Resolved Path object for the storage location (typically system temp)
Example:
# In a cmdlet:
storage_path = SharedArgs.resolve_storage(parsed.get('storage'))
# Returns Path(tempfile.gettempdir())
"""
# We no longer maintain a hardcoded map for 'hydrus' (~/.hydrus) or 'local' (~/Videos).
# Everything defaults to the system temp directory unless a specific default is provided.
# This ensures environment independence.
if default is not None:
return default
return Path(tempfile.gettempdir())
@classmethod
def get(cls, name: str) -> Optional[CmdletArg]:
"""Get a shared argument by name.
Args:
name: Uppercase name like 'HASH', 'LOCATION', etc.
Returns:
CmdletArg if found, None otherwise
Example:
arg = SharedArgs.get('QUERY') # Returns SharedArgs.QUERY
"""
try:
return getattr(cls, name.upper())
except AttributeError:
return None
@dataclass
class Cmdlet:
"""Represents a cmdlet with metadata and arguments.
Example:
cmd = Cmdlet(
name="add-file",
summary="Upload a media file",
usage="add-file <location>",
aliases=["add-file-alias"],
args=[
CmdletArg("location", required=True, description="Destination location"),
CmdletArg("-delete", type="flag", description="Delete after upload"),
],
details=[
"- This is a detail line",
"- Another detail",
]
)
# Access properties
log(cmd.name) # "add-file"
log(cmd.summary) # "Upload a media file"
log(cmd.args[0].name) # "location"
"""
name: str
""""""
summary: str
"""One-line summary of the cmdlet"""
usage: str
"""Usage string, e.g., 'add-file <location> [-delete]'"""
alias: List[str] = field(default_factory=list)
"""List of aliases for this cmdlet, e.g., ['add', 'add-f']"""
arg: List[CmdletArg] = field(default_factory=list)
"""List of arguments accepted by this cmdlet"""
detail: List[str] = field(default_factory=list)
"""Detailed explanation lines (for help text)"""
examples: List[str] = field(default_factory=list)
"""Example invocations shown in `.help`."""
# Execution function: func(result, args, config) -> int
exec: Optional[Callable[[Any,
Sequence[str],
Dict[str,
Any]],
int]] = field(default=None)
def _collect_names(self) -> List[str]:
"""Collect primary name plus aliases, de-duplicated and normalized."""
names: List[str] = []
if self.name:
names.append(self.name)
for alias in self.alias or []:
if alias:
names.append(alias)
for alias in getattr(self, "aliases", None) or []:
if alias:
names.append(alias)
seen: Set[str] = set()
deduped: List[str] = []
for name in names:
key = name.replace("_", "-").lower()
if key in seen:
continue
seen.add(key)
deduped.append(name)
return deduped
def register(self) -> "Cmdlet":
"""Register this cmdlet's exec under its name and aliases."""
if not callable(self.exec):
return self
try:
from . import (
register_callable as _register_callable,
) # Local import to avoid circular import cost
except Exception:
return self
names = self._collect_names()
if not names:
return self
_register_callable(names, self.exec)
return self
def get_flags(self, arg_name: str) -> set[str]:
"""Generate -name and --name flag variants for an argument.
Args:
arg_name: The argument name (e.g., 'library', 'tag', 'size')
Returns:
Set containing both single-dash and double-dash variants
(e.g., {'-library', '--library'})
Example:
if low in cmdlet.get_flags('library'):
# handle library flag
"""
return {f"-{arg_name}",
f"--{arg_name}"}
def build_flag_registry(self) -> Dict[str, set[str]]:
"""Build a registry of all flag variants for this cmdlet's arguments.
Automatically generates all -name and --name variants for each argument.
Useful for parsing command-line arguments without hardcoding flags.
Returns:
Dict mapping argument names to their flag sets
(e.g., {'library': {'-library', '--library'}, 'tag': {'-tag', '--tag'}})
Example:
flags = cmdlet.build_flag_registry()
if low in flags.get('library', set()):
# handle library
elif low in flags.get('tag', set()):
# handle tag
"""
registry: Dict[str, set[str]] = {}
for arg in self.arg:
try:
registry[arg.name] = {str(flag).lower() for flag in arg.to_flags()}
except Exception:
registry[arg.name] = {flag.lower() for flag in self.get_flags(arg.name)}
return registry
# Tag groups cache (loaded from JSON config file)
_TAG_GROUPS_CACHE: Optional[Dict[str, List[str]]] = None
_TAG_GROUPS_MTIME: Optional[float] = None
@@ -566,240 +43,6 @@ def set_tag_groups_path(path: Path) -> None:
TAG_GROUPS_PATH = path
def parse_cmdlet_args(args: Sequence[str],
cmdlet_spec: Dict[str,
Any] | Cmdlet) -> Dict[str,
Any]:
"""Parse command-line arguments based on cmdlet specification.
Extracts argument values from command-line tokens using the argument names
and types defined in the cmdlet metadata. Automatically supports single-dash
and double-dash variants of flag names. Arguments without dashes in definition
are treated as positional arguments.
Args:
args: Command-line arguments (e.g., ["-path", "/home/file.txt", "-foo", "bar"])
cmdlet_spec: Cmdlet metadata dict with "args" key containing list of arg specs,
or a Cmdlet object. Each arg spec should have at least "name" key.
Argument names can be defined with or without prefixes.
Returns:
Dict mapping canonical arg names to their parsed values. If an arg is not
provided, it will not be in the dict. Lookup will normalize prefixes.
Example:
cmdlet = {
"args": [
{"name": "path", "type": "string"}, # Positional - matches bare value or -path/--path
{"name": "count", "type": "int"} # Positional - matches bare value or -count/--count
]
}
result = parse_cmdlet_args(["value1", "-count", "5"], cmdlet)
# result = {"path": "value1", "count": "5"}
"""
try:
from SYS.cmdlet_spec import parse_cmdlet_args as _parse_cmdlet_args_fast
return _parse_cmdlet_args_fast(args, cmdlet_spec)
except Exception:
# Fall back to local implementation below to preserve behavior if the
# lightweight parser is unavailable.
pass
result: Dict[str,
Any] = {}
# Only accept Cmdlet objects
if not isinstance(cmdlet_spec, Cmdlet):
raise TypeError(f"Expected Cmdlet, got {type(cmdlet_spec).__name__}")
# Build arg specs from cmdlet
arg_specs: List[CmdletArg] = cmdlet_spec.arg
positional_args: List[CmdletArg] = [] # args without prefix in definition
flagged_args: List[CmdletArg] = [] # args with prefix in definition
query_mapped_args: List[CmdletArg] = []
arg_spec_map: Dict[str,
str] = {} # prefix variant -> canonical name (without prefix)
for spec in arg_specs:
name = spec.name
if not name:
continue
# Track args that can be populated from -query.
try:
if getattr(spec, "query_key", None):
query_mapped_args.append(spec)
except Exception:
pass
name_str = str(name)
canonical_name = name_str.lstrip("-")
# Query-only args do not register dedicated flags/positionals.
try:
if bool(getattr(spec, "query_only", False)):
continue
except Exception:
pass
# Determine if this is positional (no dashes in original definition)
if "-" not in name_str:
positional_args.append(spec)
else:
flagged_args.append(spec)
# Register all supported flag variants, including legacy aliases.
arg_spec_map[canonical_name.lower()] = canonical_name # bare canonical name
try:
for flag in spec.to_flags():
arg_spec_map[str(flag).lower()] = canonical_name
except Exception:
arg_spec_map[f"-{canonical_name}".lower()] = canonical_name
arg_spec_map[f"--{canonical_name}".lower()] = canonical_name
# Parse arguments
i = 0
positional_index = 0 # Track which positional arg we're on
while i < len(args):
token = str(args[i])
token_lower = token.lower()
# Legacy guidance: -hash/--hash was removed in favor of -query "hash:...".
# However, some cmdlets may explicitly re-introduce a -hash flag.
if token_lower in {"-hash",
"--hash"} and token_lower not in arg_spec_map:
try:
log(
'Legacy flag -hash is no longer supported. Use: -query "hash:<sha256>"',
file=sys.stderr,
)
except Exception:
pass
i += 1
continue
# Check if this token is a known flagged argument
if token_lower in arg_spec_map:
canonical_name = arg_spec_map[token_lower]
spec = next(
(
s for s in arg_specs
if str(s.name).lstrip("-").lower() == canonical_name.lower()
),
None,
)
# Check if it's a flag type (which doesn't consume next value, just marks presence)
is_flag = spec and spec.type == "flag"
if is_flag:
# For flags, just mark presence without consuming next token
result[canonical_name] = True
i += 1
else:
# For non-flags, consume next token as the value
if i + 1 < len(args) and not str(args[i + 1]).startswith("-"):
value = args[i + 1]
# Check if variadic
is_variadic = spec and spec.variadic
if is_variadic:
if canonical_name not in result:
result[canonical_name] = []
elif not isinstance(result[canonical_name], list):
result[canonical_name] = [result[canonical_name]]
result[canonical_name].append(value)
else:
result[canonical_name] = value
i += 2
else:
i += 1
# Otherwise treat as positional if we have positional args remaining
elif positional_index < len(positional_args):
positional_spec = positional_args[positional_index]
canonical_name = str(positional_spec.name).lstrip("-")
is_variadic = positional_spec.variadic
if is_variadic:
# For variadic args, append to a list
if canonical_name not in result:
result[canonical_name] = []
elif not isinstance(result[canonical_name], list):
# Should not happen if logic is correct, but safety check
result[canonical_name] = [result[canonical_name]]
result[canonical_name].append(token)
# Do not increment positional_index so subsequent tokens also match this arg
# Note: Variadic args should typically be the last positional argument
i += 1
else:
result[canonical_name] = token
positional_index += 1
i += 1
else:
# Unknown token, skip it
i += 1
# Populate query-mapped args from the unified -query string.
try:
raw_query = result.get("query")
except Exception:
raw_query = None
if query_mapped_args and raw_query is not None:
try:
from SYS.cli_syntax import parse_query as _parse_query
parsed_query = _parse_query(str(raw_query))
fields = parsed_query.get("fields",
{}) if isinstance(parsed_query,
dict) else {}
norm_fields = (
{
str(k).strip().lower(): v
for k, v in fields.items()
} if isinstance(fields,
dict) else {}
)
except Exception:
norm_fields = {}
for spec in query_mapped_args:
canonical_name = str(getattr(spec, "name", "") or "").lstrip("-")
if not canonical_name:
continue
# Do not override explicit flags.
if canonical_name in result and result.get(canonical_name) not in (None,
""):
continue
try:
key = str(getattr(spec, "query_key", "") or "").strip().lower()
aliases = getattr(spec, "query_aliases", None)
alias_list = [
str(a).strip().lower() for a in (aliases or []) if str(a).strip()
]
except Exception:
key = ""
alias_list = []
candidates = [k for k in [key, canonical_name] + alias_list if k]
val = None
for k in candidates:
if k in norm_fields:
val = norm_fields.get(k)
break
if val is None:
continue
try:
result[canonical_name] = spec.resolve(val)
except Exception:
result[canonical_name] = val
return result
def resolve_target_dir(
parsed: Dict[str, Any],
config: Dict[str, Any],
@@ -3011,20 +2254,6 @@ def collapse_namespace_tags(
kept_ns = True
result.append(text)
return result
def collapse_namespace_tag(
tags: Optional[Iterable[Any]],
namespace: str,
prefer: str = "last"
) -> list[str]:
"""Singular alias for collapse_namespace_tags.
Some cmdlet prefer the singular name; keep behavior centralized.
"""
return collapse_namespace_tags(tags, namespace, prefer=prefer)
def extract_tag_from_result(result: Any) -> list[str]:
"""Extract all tags from a result dict or PipeObject.
@@ -3395,11 +2624,11 @@ def coerce_to_pipe_object(
pipe_obj = models.PipeObject(
hash=hash_val,
store=store_val,
provider=str(
value.get("provider")
plugin=str(
value.get("plugin")
or value.get("prov")
or value.get("source")
or extra.get("provider")
or extra.get("plugin")
or extra.get("source")
or ""
).strip() or None,
@@ -3456,7 +2685,7 @@ def coerce_to_pipe_object(
pipe_obj = models.PipeObject(
hash=hash_val,
store=store_val,
provider=None,
plugin=None,
path=str(path_val) if path_val and path_val != "unknown" else None,
title=title_val,
url=url_val,
+22 -23
View File
@@ -17,7 +17,7 @@ from SYS.result_publication import overlay_existing_result_table, publish_result
from SYS.rich_display import show_available_plugins_panel, show_plugin_config_panel
from SYS.utils_constant import ALL_SUPPORTED_EXTENSIONS
from PluginCore.backend_registry import BackendRegistry
from API.HTTP import _download_direct_file
from API.HTTP import download_direct_file
from .. import _shared as sh
Cmdlet = sh.Cmdlet
@@ -31,7 +31,7 @@ merge_sequences = sh.merge_sequences
extract_relationships = sh.extract_relationships
extract_duration = sh.extract_duration
coerce_to_pipe_object = sh.coerce_to_pipe_object
collapse_namespace_tag = sh.collapse_namespace_tag
collapse_namespace_tags = sh.collapse_namespace_tags
resolve_target_dir = sh.resolve_target_dir
resolve_media_kind_by_extension = sh.resolve_media_kind_by_extension
coerce_to_path = sh.coerce_to_path
@@ -102,10 +102,10 @@ def _maybe_apply_florencevision_tags(
config: Dict[str, Any],
pipe_obj: Optional[models.PipeObject] = None,
) -> List[str]:
"""Optionally auto-tag images using the FlorenceVision tool.
"""Optionally auto-tag images using the FlorenceVision plugin helper.
Controlled via config:
[tool=florencevision]
[plugin=florencevision]
enabled=true
strict=false
@@ -114,8 +114,8 @@ def _maybe_apply_florencevision_tags(
"""
strict = False
try:
tool_block = (config or {}).get("tool")
fv_block = tool_block.get("florencevision") if isinstance(tool_block, dict) else None
plugin_block = (config or {}).get("plugin")
fv_block = plugin_block.get("florencevision") if isinstance(plugin_block, dict) else None
enabled = False
if isinstance(fv_block, dict):
enabled = bool(fv_block.get("enabled"))
@@ -123,7 +123,7 @@ def _maybe_apply_florencevision_tags(
if not enabled:
return tags
from tool.florencevision import FlorenceVisionTool
from plugins.florencevision import FlorenceVisionTool
# Special-case: if this file was produced by the `screen-shot` cmdlet,
# OCR is more useful than caption/detection for tagging screenshots.
@@ -134,12 +134,12 @@ def _maybe_apply_florencevision_tags(
if action.lower().startswith("cmdlet:"):
cmdlet_name = action.split(":", 1)[1].strip().lower()
if cmdlet_name in {"screen-shot", "screen_shot", "screenshot"}:
tool_block2 = dict((config or {}).get("tool") or {})
fv_block2 = dict(tool_block2.get("florencevision") or {})
plugin_block2 = dict((config or {}).get("plugin") or {})
fv_block2 = dict(plugin_block2.get("florencevision") or {})
fv_block2["task"] = "ocr"
tool_block2["florencevision"] = fv_block2
plugin_block2["florencevision"] = fv_block2
cfg_for_tool = dict(config or {})
cfg_for_tool["tool"] = tool_block2
cfg_for_tool["plugin"] = plugin_block2
except Exception:
cfg_for_tool = config
@@ -1237,7 +1237,7 @@ class Add_File(Cmdlet):
except Exception:
pass
downloaded = _download_direct_file(
downloaded = download_direct_file(
url_text,
download_root,
quiet=False,
@@ -1693,9 +1693,8 @@ class Add_File(Cmdlet):
) -> Tuple[Optional[Path], Optional[str], Optional[Path]]:
plugin_key = None
for source in (
pipe_obj.provider,
pipe_obj.plugin,
get_field(result, "plugin"),
get_field(result, "provider"),
get_field(result, "table"),
):
candidate = Add_File._normalize_provider_key(source)
@@ -1760,7 +1759,7 @@ class Add_File(Cmdlet):
str(r_hash),
source_url,
)
downloaded = _download_direct_file(
downloaded = download_direct_file(
source_url,
download_dir,
quiet=True,
@@ -2028,7 +2027,7 @@ class Add_File(Cmdlet):
*,
hash_value: str,
store: str,
provider: Optional[str] = None,
plugin: Optional[str] = None,
path: Optional[str],
tag: List[str],
title: Optional[str],
@@ -2037,7 +2036,7 @@ class Add_File(Cmdlet):
) -> None:
pipe_obj.hash = hash_value
pipe_obj.store = store
pipe_obj.provider = provider
pipe_obj.plugin = plugin
pipe_obj.is_temp = False
pipe_obj.path = path
pipe_obj.tag = tag
@@ -2260,7 +2259,7 @@ class Add_File(Cmdlet):
t for t in tags_from_result
if not str(t).strip().lower().startswith("title:")
]
sidecar_tags = collapse_namespace_tag(
sidecar_tags = collapse_namespace_tags(
[normalize_title_tag(t) for t in sidecar_tags],
"title",
prefer="last"
@@ -2449,15 +2448,15 @@ class Add_File(Cmdlet):
or "unknown"
).strip() or "unknown"
store_value = str(payload.get("store") or "").strip()
provider_value = payload.get("provider")
if provider_value is None and plugin_name:
provider_value = plugin_name
plugin_value = payload.get("plugin")
if plugin_value is None and plugin_name:
plugin_value = plugin_name
Add_File._update_pipe_object_destination(
pipe_obj,
hash_value=hash_value,
store=store_value,
provider=str(provider_value) if provider_value else None,
plugin=str(plugin_value) if plugin_value else None,
path=path_value,
tag=tag_values,
title=title_value,
@@ -2584,7 +2583,7 @@ class Add_File(Cmdlet):
pipe_obj,
hash_value=f_hash or "unknown",
store="",
provider=plugin_name or None,
plugin=plugin_name or None,
path=file_path,
tag=pipe_obj.tag,
title=pipe_obj.title or (media_path.name if media_path else None),
-5
View File
@@ -1,5 +0,0 @@
from __future__ import annotations
"""Compatibility wrapper for moved metadata note add cmdlet."""
from cmdlet.metadata.note_add import * # noqa: F401,F403
-9
View File
@@ -1,9 +0,0 @@
from __future__ import annotations
"""Compatibility wrapper for moved metadata relationship add cmdlet."""
from cmdlet.metadata import relationship_add as _relationship_add
from cmdlet.metadata.relationship_add import * # noqa: F401,F403
# Preserve direct private helper imports used by tests and legacy callers.
_extract_hash_and_store = _relationship_add._extract_hash_and_store
-5
View File
@@ -1,5 +0,0 @@
from __future__ import annotations
"""Compatibility wrapper for moved metadata URL add cmdlet."""
from cmdlet.metadata.url_add import * # noqa: F401,F403
+3 -3
View File
@@ -49,9 +49,9 @@ def _extract_hash_from_hydrus_file_url(url: str) -> str:
def _hydrus_instance_names(config: Dict[str, Any]) -> Set[str]:
instances: Set[str] = set()
try:
store_cfg = config.get("store") if isinstance(config, dict) else None
if isinstance(store_cfg, dict):
hydrus_cfg = store_cfg.get("hydrusnetwork")
plugin_cfg = config.get("plugin") if isinstance(config, dict) else None
if isinstance(plugin_cfg, dict):
hydrus_cfg = plugin_cfg.get("hydrusnetwork")
if isinstance(hydrus_cfg, dict):
instances = {
str(k).strip().lower()
+3 -3
View File
@@ -133,13 +133,13 @@ class Delete_File(sh.Cmdlet):
provider_name = None
full_metadata: Dict[str, Any] = {}
if isinstance(item, dict):
provider_name = item.get("provider") or item.get("table")
provider_name = item.get("plugin") or item.get("table")
raw_meta = item.get("full_metadata") or item.get("metadata")
if isinstance(raw_meta, dict):
full_metadata = raw_meta
else:
try:
provider_name = sh.get_field(item, "provider") or sh.get_field(item, "table")
provider_name = sh.get_field(item, "plugin") or sh.get_field(item, "table")
except Exception:
pass
try:
@@ -542,4 +542,4 @@ class Delete_File(sh.Cmdlet):
# Instantiate and register the cmdlet
Delete_File()
CMDLET = Delete_File()
+9 -10
View File
@@ -19,7 +19,7 @@ import shutil
import webbrowser
from API.HTTP import _download_direct_file
from API.HTTP import download_direct_file
from SYS.models import DownloadError, DownloadOptions, DownloadMediaResult
from SYS.logger import log, debug_panel, is_debug_enabled
from SYS.payload_builders import build_file_result_payload, build_table_result_payload
@@ -235,7 +235,7 @@ class Download_File(Cmdlet):
action = str(
result.get("action")
or result.get("provider_action")
or result.get("plugin_action")
or ""
).strip().lower()
@@ -338,12 +338,12 @@ class Download_File(Cmdlet):
path_value: Optional[Any] = path
if isinstance(path, dict):
provider_action = str(
plugin_action = str(
path.get("action")
or path.get("provider_action")
or path.get("plugin_action")
or ""
).strip().lower()
if provider_action == "download_items" or bool(path.get("download_items")):
if plugin_action == "download_items" or bool(path.get("download_items")):
request_metadata = path.get("metadata") or path.get("full_metadata") or {}
if not isinstance(request_metadata, dict):
request_metadata = {}
@@ -522,7 +522,7 @@ class Download_File(Cmdlet):
# Direct Download Fallback
attempted_download = True
result_obj = _download_direct_file(
result_obj = download_direct_file(
str(url),
final_output_dir,
quiet=quiet_mode,
@@ -569,7 +569,7 @@ class Download_File(Cmdlet):
key = self._normalize_provider_key(table_hint)
if key:
return key
provider_hint = get_field(item, "provider")
provider_hint = get_field(item, "plugin")
key = self._normalize_provider_key(provider_hint)
if key:
return key
@@ -743,7 +743,7 @@ class Download_File(Cmdlet):
and isinstance(target, str) and target.startswith("http")):
suggested_name = str(title).strip() if title is not None else None
result_obj = _download_direct_file(
result_obj = download_direct_file(
target,
final_output_dir,
quiet=quiet_mode,
@@ -926,7 +926,6 @@ class Download_File(Cmdlet):
}
if provider_hint:
payload["plugin"] = str(provider_hint)
payload["provider"] = str(provider_hint)
if full_metadata:
payload["metadata"] = full_metadata
if notes:
@@ -1125,7 +1124,7 @@ class Download_File(Cmdlet):
filename += ext_text
if download_url:
result_obj = _download_direct_file(
result_obj = download_direct_file(
download_url,
final_output_dir,
quiet=True,
+16 -16
View File
@@ -43,7 +43,7 @@ from SYS import pipeline as pipeline_context
# Playwright & Screenshot Dependencies
# ============================================================================
from tool.playwright import PlaywrightTimeoutError, PlaywrightTool
from plugins.playwright import PlaywrightTimeoutError, PlaywrightTool
try:
from SYS.config import resolve_output_dir
@@ -1525,22 +1525,22 @@ def _capture(
{}) or {})
except Exception:
base_cfg = {}
tool_block = dict(base_cfg.get("tool") or {}
plugin_block = dict(base_cfg.get("plugin") or {}
) if isinstance(base_cfg,
dict) else {}
pw_block = (
dict(tool_block.get("playwright") or {})
if isinstance(tool_block,
dict(plugin_block.get("playwright") or {})
if isinstance(plugin_block,
dict) else {}
)
pw_block["browser"] = "chromium"
tool_block["playwright"] = pw_block
plugin_block["playwright"] = pw_block
if isinstance(base_cfg, dict):
base_cfg["tool"] = tool_block
base_cfg["plugin"] = plugin_block
tool = PlaywrightTool(base_cfg)
except Exception:
tool = PlaywrightTool({
"tool": {
"plugin": {
"playwright": {
"browser": "chromium"
}
@@ -1888,8 +1888,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
quality_value: Optional[int] = None
if not format_value:
try:
tool_cfg = config.get("tool", {}) if isinstance(config, dict) else {}
pw_cfg = tool_cfg.get("playwright") if isinstance(tool_cfg, dict) else None
plugin_cfg = config.get("plugin", {}) if isinstance(config, dict) else {}
pw_cfg = plugin_cfg.get("playwright") if isinstance(plugin_cfg, dict) else None
if isinstance(pw_cfg, dict):
format_value = pw_cfg.get("format")
except Exception:
@@ -1901,8 +1901,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
quality_value = _normalize_quality(raw_quality_value)
else:
try:
tool_cfg = config.get("tool", {}) if isinstance(config, dict) else {}
pw_cfg = tool_cfg.get("playwright") if isinstance(tool_cfg, dict) else None
plugin_cfg = config.get("plugin", {}) if isinstance(config, dict) else {}
pw_cfg = plugin_cfg.get("playwright") if isinstance(plugin_cfg, dict) else None
if isinstance(pw_cfg, dict) and pw_cfg.get("screenshot_quality") not in (None, ""):
quality_value = _normalize_quality(pw_cfg.get("screenshot_quality"))
except Exception:
@@ -1994,18 +1994,18 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
shared_playwright_tool: Optional[PlaywrightTool] = None
try:
if isinstance(config, dict):
tool_block = dict(config.get("tool") or {})
pw_block = dict(tool_block.get("playwright") or {})
plugin_block = dict(config.get("plugin") or {})
pw_block = dict(plugin_block.get("playwright") or {})
pw_block["browser"] = "chromium"
pw_block["user_agent"] = "native"
pw_block["viewport_width"] = int(DEFAULT_VIEWPORT.get("width", 1920))
pw_block["viewport_height"] = int(DEFAULT_VIEWPORT.get("height", 1080))
tool_block["playwright"] = pw_block
plugin_block["playwright"] = pw_block
pw_local_cfg = dict(config)
pw_local_cfg["tool"] = tool_block
pw_local_cfg["plugin"] = plugin_block
else:
pw_local_cfg = {
"tool": {
"plugin": {
"playwright": {
"browser": "chromium",
"user_agent": "native",
+6 -6
View File
@@ -164,7 +164,7 @@ def _summarize_worker_results(results: Sequence[Dict[str, Any]], preview_limit:
class search_file(Cmdlet):
"""Class-based search-file cmdlet for searching backends and providers."""
"""Class-based search-file cmdlet for searching backends and plugins."""
def __init__(self) -> None:
super().__init__(
@@ -187,9 +187,9 @@ class search_file(Cmdlet):
),
],
detail=[
"Search across configured store backends or plugin providers.",
"Search across configured storage backends or plugins.",
"Use -instance to target a specific configured backend/instance by name.",
"Use -plugin with -instance to target a named provider config.",
"Use -plugin with -instance to target a named plugin config.",
"URL search: url:* (any URL) or url:<value> (URL substring)",
"Extension search: ext:<value> (e.g., ext:png)",
"Hydrus-style extension: system:filetype = png",
@@ -1216,7 +1216,7 @@ class search_file(Cmdlet):
try:
table.set_table_metadata(
{
"provider": "web",
"plugin": "web",
"site": site_host,
"query": search_query,
"filetype": requested_type,
@@ -1490,7 +1490,7 @@ class search_file(Cmdlet):
return 1
# Align with provider default when user did not set -limit.
# Align with plugin default when user did not set -limit.
if not limit_set:
limit = 50
@@ -1632,7 +1632,7 @@ 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
# Ensure plugin source is present so downstream cmdlets can resolve the owner.
if "source" not in item_dict:
item_dict["source"] = plugin_name
-4
View File
@@ -125,10 +125,6 @@ class File(Cmdlet):
if callable(exec_fn):
return int(exec_fn(result, args, config))
fallback_run = getattr(module, "_run", None)
if callable(fallback_run):
return int(fallback_run(result, args, config))
log(f"file: cannot dispatch action '{action}' via module '{module_name}'", file=sys.stderr)
return 1
+7 -7
View File
@@ -640,8 +640,8 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
except Exception:
hydrus_client = None
# Sidecar/tag import fallback DB root (legacy): if a folder store is selected, use it;
# otherwise fall back to configured local storage path.
# Use the selected store root when available; otherwise use the configured
# local plugin root for sidecar/tag import lookup.
from SYS.config import get_local_storage_path
local_storage_root: Optional[Path] = None
@@ -852,8 +852,8 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
"path",
None)
# Legacy LOCAL STORAGE MODE: Handle relationships for local files
# (kept as stub - folder store removed)
# Handle relationships for local-file results using the configured
# local plugin root when available.
from SYS.config import get_local_storage_path
local_storage_path = get_local_storage_path(config) if config else None
@@ -869,7 +869,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
try:
file_path_obj = Path(str(file_path_from_result))
except Exception as exc:
log(f"Local storage error: {exc}", file=sys.stderr)
log(f"Local library error: {exc}", file=sys.stderr)
return 1
if not file_path_obj.exists():
@@ -879,12 +879,12 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
if file_path_obj is not None:
try:
if local_storage_root is None:
log("Local storage path unavailable", file=sys.stderr)
log("Local plugin path unavailable", file=sys.stderr)
return 1
with LocalLibrarySearchOptimizer(local_storage_root) as opt:
if opt.db is None:
log("Local storage DB unavailable", file=sys.stderr)
log("Local library DB unavailable", file=sys.stderr)
return 1
if king_hash:
+7 -7
View File
@@ -25,7 +25,7 @@ expand_tag_groups = sh.expand_tag_groups
merge_sequences = sh.merge_sequences
render_tag_value_templates = sh.render_tag_value_templates
parse_cmdlet_args = sh.parse_cmdlet_args
collapse_namespace_tag = sh.collapse_namespace_tag
collapse_namespace_tags = sh.collapse_namespace_tags
should_show_help = sh.should_show_help
get_field = sh.get_field
@@ -800,7 +800,7 @@ class Add_Tag(Cmdlet):
file=sys.stderr,
)
item_tag_to_add = collapse_namespace_tag(
item_tag_to_add = collapse_namespace_tags(
item_tag_to_add,
"title",
prefer="last"
@@ -843,7 +843,7 @@ class Add_Tag(Cmdlet):
)
unresolved_template_count += len(unresolved_templates)
item_tag_to_add = collapse_namespace_tag(
item_tag_to_add = collapse_namespace_tags(
item_tag_to_add,
"title",
prefer="last"
@@ -877,7 +877,7 @@ class Add_Tag(Cmdlet):
]
updated_tag_list.extend(actual_tag_to_add)
updated_tag_list = collapse_namespace_tag(
updated_tag_list = collapse_namespace_tags(
updated_tag_list,
"title",
prefer="last"
@@ -977,7 +977,7 @@ class Add_Tag(Cmdlet):
file=sys.stderr,
)
item_tag_to_add = collapse_namespace_tag(
item_tag_to_add = collapse_namespace_tags(
item_tag_to_add,
"title",
prefer="last"
@@ -1016,7 +1016,7 @@ class Add_Tag(Cmdlet):
)
unresolved_template_count += len(unresolved_templates)
item_tag_to_add = collapse_namespace_tag(
item_tag_to_add = collapse_namespace_tags(
item_tag_to_add,
"title",
prefer="last"
@@ -1032,7 +1032,7 @@ class Add_Tag(Cmdlet):
]
if len(existing_title_tags) > 1:
item_tag_to_add.append(existing_title_tags[-1])
item_tag_to_add = collapse_namespace_tag(
item_tag_to_add = collapse_namespace_tags(
item_tag_to_add,
"title",
prefer="last"
+4 -4
View File
@@ -649,7 +649,7 @@ def _run_impl(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
item_title=str(items[0].get("title") or provider.name),
path=None,
subject={
"provider": provider.name,
"plugin": provider.name,
"url": str(query_hint)
},
quiet=emit_mode,
@@ -692,7 +692,7 @@ def _run_impl(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
)
payload = {
"tag": tags,
"provider": provider.name,
"plugin": provider.name,
"title": item.get("title"),
"artist": item.get("artist"),
"album": item.get("album"),
@@ -702,7 +702,7 @@ def _run_impl(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"path": path_for_payload,
"extra": {
"tag": tags,
"provider": provider.name,
"plugin": provider.name,
},
}
selection_payload.append(payload)
@@ -721,7 +721,7 @@ def _run_impl(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
# If the current result already carries a tag list (e.g. a selected metadata
# row from get-tag -scrape itunes), APPLY those tags to the file in the store.
result_provider = get_field(result, "provider", None)
result_provider = get_field(result, "plugin", None)
result_tags = get_field(result, "tag", None)
if result_provider and isinstance(result_tags, list) and result_tags: