update and cleanup repo
This commit is contained in:
+5
-776
@@ -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
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user