update local and mpv plugins, add file cmdlet, update docs
This commit is contained in:
@@ -731,8 +731,6 @@ class CmdletCompleter(Completer):
|
|||||||
return canonical_cmd
|
return canonical_cmd
|
||||||
|
|
||||||
lowered = {str(tok or "").strip().lower() for tok in (stage_tokens or [])}
|
lowered = {str(tok or "").strip().lower() for tok in (stage_tokens or [])}
|
||||||
if "-search" in lowered or "--search" in lowered:
|
|
||||||
return "search-file"
|
|
||||||
if "-download" in lowered or "--download" in lowered or "-dl" in lowered or "--dl" in lowered:
|
if "-download" in lowered or "--download" in lowered or "-dl" in lowered or "--dl" in lowered:
|
||||||
return "download-file"
|
return "download-file"
|
||||||
if "-add" in lowered or "--add" in lowered:
|
if "-add" in lowered or "--add" in lowered:
|
||||||
@@ -743,6 +741,10 @@ class CmdletCompleter(Completer):
|
|||||||
return "delete-file"
|
return "delete-file"
|
||||||
if "-merge" in lowered or "--merge" in lowered:
|
if "-merge" in lowered or "--merge" in lowered:
|
||||||
return "merge-file"
|
return "merge-file"
|
||||||
|
if "-plugin" in lowered or "--plugin" in lowered or any(tok.startswith("-plugin=") or tok.startswith("--plugin=") for tok in lowered):
|
||||||
|
return "search-file"
|
||||||
|
if "-query" in lowered or "--query" in lowered or any(tok.startswith("-query=") or tok.startswith("--query=") for tok in lowered):
|
||||||
|
return "search-file"
|
||||||
return canonical_cmd
|
return canonical_cmd
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -750,7 +752,12 @@ class CmdletCompleter(Completer):
|
|||||||
canonical_cmd = CmdletCompleter._effective_cmd_name(cmd_name, stage_tokens)
|
canonical_cmd = CmdletCompleter._effective_cmd_name(cmd_name, stage_tokens)
|
||||||
if canonical_cmd not in {"search-file", "add-file", "download-file"}:
|
if canonical_cmd not in {"search-file", "add-file", "download-file"}:
|
||||||
return None
|
return None
|
||||||
return CmdletCompleter._flag_value(stage_tokens, "-plugin", "--plugin")
|
raw_plugin = CmdletCompleter._flag_value(stage_tokens, "-plugin", "--plugin")
|
||||||
|
if raw_plugin:
|
||||||
|
# Strip quotes if present, then normalize to lowercase
|
||||||
|
stripped = CmdletCompleter._strip_quotes(str(raw_plugin or ""))
|
||||||
|
return stripped.strip().lower() if stripped else None
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _plugin_instance_choices(plugin_name: Optional[str], config: Dict[str, Any]) -> List[str]:
|
def _plugin_instance_choices(plugin_name: Optional[str], config: Dict[str, Any]) -> List[str]:
|
||||||
@@ -788,6 +795,84 @@ class CmdletCompleter(Completer):
|
|||||||
out.append(text)
|
out.append(text)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _plugin_instance_accepts_direct_path(plugin_name: Optional[str]) -> bool:
|
||||||
|
return str(plugin_name or "").strip().lower() == "local"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_path_fragment(value: str) -> bool:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
if text[:1] in {"'", '"'}:
|
||||||
|
text = text[1:]
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
if text.startswith((".", "~", "\\", "/")):
|
||||||
|
return True
|
||||||
|
if "\\" in text or "/" in text:
|
||||||
|
return True
|
||||||
|
if len(text) >= 2 and text[1] == ":":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _path_instance_choices(current_token: str) -> List[str]:
|
||||||
|
raw = str(current_token or "")
|
||||||
|
if not CmdletCompleter._looks_like_path_fragment(raw):
|
||||||
|
return []
|
||||||
|
|
||||||
|
quote_prefix = raw[:1] if raw[:1] in {"'", '"'} else ""
|
||||||
|
fragment = raw[1:] if quote_prefix else raw
|
||||||
|
if not fragment:
|
||||||
|
return []
|
||||||
|
|
||||||
|
expanded = os.path.expanduser(fragment)
|
||||||
|
candidate = Path(expanded)
|
||||||
|
|
||||||
|
if fragment.endswith(("\\", "/")):
|
||||||
|
parent = candidate
|
||||||
|
prefix = ""
|
||||||
|
else:
|
||||||
|
parent = candidate.parent if str(candidate.parent) not in {"", "."} else Path.cwd()
|
||||||
|
prefix = candidate.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not parent.exists() or not parent.is_dir():
|
||||||
|
return []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
out: List[str] = []
|
||||||
|
seen: Set[str] = set()
|
||||||
|
prefix_lower = prefix.lower()
|
||||||
|
try:
|
||||||
|
entries = sorted(parent.iterdir(), key=lambda item: item.name.lower())
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
try:
|
||||||
|
if not entry.is_dir():
|
||||||
|
continue
|
||||||
|
if prefix_lower and not entry.name.lower().startswith(prefix_lower):
|
||||||
|
continue
|
||||||
|
suggestion = str(entry)
|
||||||
|
if quote_prefix:
|
||||||
|
suggestion = quote_prefix + suggestion
|
||||||
|
elif " " in suggestion:
|
||||||
|
suggestion = f'"{suggestion}"'
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lowered = suggestion.lower()
|
||||||
|
if lowered in seen:
|
||||||
|
continue
|
||||||
|
seen.add(lowered)
|
||||||
|
out.append(suggestion)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
def _filter_stage_arg_names(
|
def _filter_stage_arg_names(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -803,12 +888,15 @@ class CmdletCompleter(Completer):
|
|||||||
plugin_name = self._selected_plugin_name(canonical_cmd, stage_tokens)
|
plugin_name = self._selected_plugin_name(canonical_cmd, stage_tokens)
|
||||||
instance_choices = self._plugin_instance_choices(plugin_name, config)
|
instance_choices = self._plugin_instance_choices(plugin_name, config)
|
||||||
has_named_instances = bool(instance_choices)
|
has_named_instances = bool(instance_choices)
|
||||||
|
accepts_direct_path = self._plugin_instance_accepts_direct_path(plugin_name)
|
||||||
|
|
||||||
filtered: List[str] = []
|
filtered: List[str] = []
|
||||||
for arg in arg_names:
|
for arg in arg_names:
|
||||||
logical = str(arg or "").lstrip("-").strip().lower()
|
logical = str(arg or "").lstrip("-").strip().lower()
|
||||||
if logical == "instance":
|
if logical == "instance":
|
||||||
if not plugin_name or not has_named_instances:
|
if not plugin_name:
|
||||||
|
continue
|
||||||
|
if not has_named_instances and not accepts_direct_path:
|
||||||
continue
|
continue
|
||||||
if canonical_cmd == "search-file" and logical == "open":
|
if canonical_cmd == "search-file" and logical == "open":
|
||||||
if str(plugin_name or "").strip().lower() != "alldebrid":
|
if str(plugin_name or "").strip().lower() != "alldebrid":
|
||||||
@@ -816,6 +904,59 @@ class CmdletCompleter(Completer):
|
|||||||
filtered.append(arg)
|
filtered.append(arg)
|
||||||
return filtered
|
return filtered
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _tokenize_quoted(text: str) -> List[str]:
|
||||||
|
"""Tokenize text preserving quoted strings as single tokens.
|
||||||
|
|
||||||
|
Handles pipes as pipeline separators and preserves quoted strings
|
||||||
|
(single or double quotes) as atomic tokens.
|
||||||
|
"""
|
||||||
|
tokens = []
|
||||||
|
current = ""
|
||||||
|
in_quote = None # None, "'", or '"'
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(text):
|
||||||
|
char = text[i]
|
||||||
|
|
||||||
|
if in_quote:
|
||||||
|
current += char
|
||||||
|
if char == in_quote and (i == 0 or text[i - 1] != "\\"):
|
||||||
|
in_quote = None
|
||||||
|
elif char in ("'", '"'):
|
||||||
|
in_quote = char
|
||||||
|
current += char
|
||||||
|
elif char == "|":
|
||||||
|
if current.strip():
|
||||||
|
tokens.append(current.strip())
|
||||||
|
tokens.append("|")
|
||||||
|
current = ""
|
||||||
|
elif char.isspace():
|
||||||
|
if current.strip():
|
||||||
|
tokens.append(current.strip())
|
||||||
|
current = ""
|
||||||
|
else:
|
||||||
|
current += char
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if current.strip():
|
||||||
|
tokens.append(current.strip())
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _strip_quotes(token: str) -> str:
|
||||||
|
"""Remove surrounding quotes from a token if present.
|
||||||
|
|
||||||
|
Preserves internal content exactly. Only removes matching outer quotes.
|
||||||
|
"""
|
||||||
|
token = str(token or "").strip()
|
||||||
|
if len(token) >= 2:
|
||||||
|
if (token[0] == '"' and token[-1] == '"') or (token[0] == "'" and token[-1] == "'"):
|
||||||
|
return token[1:-1]
|
||||||
|
return token
|
||||||
|
|
||||||
def get_completions(
|
def get_completions(
|
||||||
self,
|
self,
|
||||||
document: Document,
|
document: Document,
|
||||||
@@ -824,7 +965,7 @@ class CmdletCompleter(Completer):
|
|||||||
self._refresh_cmdlet_names()
|
self._refresh_cmdlet_names()
|
||||||
|
|
||||||
text = document.text_before_cursor
|
text = document.text_before_cursor
|
||||||
tokens = text.split()
|
tokens = self._tokenize_quoted(text)
|
||||||
ends_with_space = bool(text) and text[-1].isspace()
|
ends_with_space = bool(text) and text[-1].isspace()
|
||||||
|
|
||||||
last_pipe = -1
|
last_pipe = -1
|
||||||
@@ -885,17 +1026,22 @@ class CmdletCompleter(Completer):
|
|||||||
cmd_name = stage_tokens[0].replace("_", "-").lower()
|
cmd_name = stage_tokens[0].replace("_", "-").lower()
|
||||||
effective_cmd = self._effective_cmd_name(cmd_name, stage_tokens)
|
effective_cmd = self._effective_cmd_name(cmd_name, stage_tokens)
|
||||||
if ends_with_space:
|
if ends_with_space:
|
||||||
|
raw_current_token = ""
|
||||||
current_token = ""
|
current_token = ""
|
||||||
prev_token = stage_tokens[-1].lower()
|
prev_token = stage_tokens[-1].lower()
|
||||||
else:
|
else:
|
||||||
current_token = stage_tokens[-1].lower()
|
raw_current_token = stage_tokens[-1]
|
||||||
|
current_token = raw_current_token.lower()
|
||||||
prev_token = stage_tokens[-2].lower() if len(stage_tokens) > 1 else ""
|
prev_token = stage_tokens[-2].lower() if len(stage_tokens) > 1 else ""
|
||||||
|
|
||||||
config = self._config_loader.load_shared()
|
config = self._config_loader.load_shared()
|
||||||
|
|
||||||
provider_name = None
|
provider_name = None
|
||||||
if effective_cmd == "search-file":
|
if effective_cmd == "search-file":
|
||||||
provider_name = self._flag_value(stage_tokens, "-plugin", "--plugin")
|
raw_provider = self._flag_value(stage_tokens, "-plugin", "--plugin")
|
||||||
|
if raw_provider:
|
||||||
|
stripped = self._strip_quotes(str(raw_provider or ""))
|
||||||
|
provider_name = stripped.strip().lower() if stripped else None
|
||||||
|
|
||||||
selected_plugin = self._selected_plugin_name(effective_cmd, stage_tokens)
|
selected_plugin = self._selected_plugin_name(effective_cmd, stage_tokens)
|
||||||
|
|
||||||
@@ -912,7 +1058,9 @@ class CmdletCompleter(Completer):
|
|||||||
query_fragment: Optional[str] = None
|
query_fragment: Optional[str] = None
|
||||||
if prev_token in {"-query", "--query"} and current_token[:1] in {"'", '"'}:
|
if prev_token in {"-query", "--query"} and current_token[:1] in {"'", '"'}:
|
||||||
query_fragment = current_token
|
query_fragment = current_token
|
||||||
elif query_started_quoted and not ends_with_space:
|
elif query_started_quoted and not ends_with_space and not current_token.startswith("-"):
|
||||||
|
# Only continue in query mode if the previous token is not a flag (new argument)
|
||||||
|
if not prev_token.startswith("-"):
|
||||||
query_fragment = current_token
|
query_fragment = current_token
|
||||||
elif query_started_quoted and ends_with_space and ":" in prev_token:
|
elif query_started_quoted and ends_with_space and ":" in prev_token:
|
||||||
query_fragment = ""
|
query_fragment = ""
|
||||||
@@ -1010,6 +1158,16 @@ class CmdletCompleter(Completer):
|
|||||||
choices: List[str] = []
|
choices: List[str] = []
|
||||||
if normalized_prev == "instance" and selected_plugin:
|
if normalized_prev == "instance" and selected_plugin:
|
||||||
choices = self._plugin_instance_choices(selected_plugin, config)
|
choices = self._plugin_instance_choices(selected_plugin, config)
|
||||||
|
if self._plugin_instance_accepts_direct_path(selected_plugin):
|
||||||
|
path_choices = self._path_instance_choices(raw_current_token)
|
||||||
|
if path_choices:
|
||||||
|
seen_choice_values = {str(choice).lower() for choice in choices}
|
||||||
|
for choice in path_choices:
|
||||||
|
lowered = str(choice).lower()
|
||||||
|
if lowered in seen_choice_values:
|
||||||
|
continue
|
||||||
|
choices.append(choice)
|
||||||
|
seen_choice_values.add(lowered)
|
||||||
if not choices:
|
if not choices:
|
||||||
choices = self._arg_choices(
|
choices = self._arg_choices(
|
||||||
cmd_name=effective_cmd,
|
cmd_name=effective_cmd,
|
||||||
@@ -1032,7 +1190,7 @@ class CmdletCompleter(Completer):
|
|||||||
choice_list = filtered
|
choice_list = filtered
|
||||||
|
|
||||||
for choice in choice_list:
|
for choice in choice_list:
|
||||||
yield Completion(choice, start_position=-len(current_token))
|
yield Completion(choice, start_position=-len(raw_current_token))
|
||||||
# Example: if the user has typed `download-file -url ...`, then `url`
|
# Example: if the user has typed `download-file -url ...`, then `url`
|
||||||
# is considered used and should not be suggested again (even as `--url`).
|
# is considered used and should not be suggested again (even as `--url`).
|
||||||
return
|
return
|
||||||
@@ -1253,8 +1411,6 @@ class CmdletExecutor:
|
|||||||
def _file_action(args: Optional[List[str]]) -> str | None:
|
def _file_action(args: Optional[List[str]]) -> str | None:
|
||||||
tokens = [str(t or "").strip().lower() for t in (args or [])]
|
tokens = [str(t or "").strip().lower() for t in (args or [])]
|
||||||
token_set = set(tokens)
|
token_set = set(tokens)
|
||||||
if "-search" in token_set or "--search" in token_set:
|
|
||||||
return "search-file"
|
|
||||||
if "-download" in token_set or "--download" in token_set or "-dl" in token_set or "--dl" in token_set:
|
if "-download" in token_set or "--download" in token_set or "-dl" in token_set or "--dl" in token_set:
|
||||||
return "download-file"
|
return "download-file"
|
||||||
if "-get" in token_set or "--get" in token_set:
|
if "-get" in token_set or "--get" in token_set:
|
||||||
@@ -1265,6 +1421,8 @@ class CmdletExecutor:
|
|||||||
return "delete-file"
|
return "delete-file"
|
||||||
if "-merge" in token_set or "--merge" in token_set:
|
if "-merge" in token_set or "--merge" in token_set:
|
||||||
return "merge-file"
|
return "merge-file"
|
||||||
|
if "-query" in token_set or "--query" in token_set or any(tok.startswith("-query=") or tok.startswith("--query=") for tok in token_set):
|
||||||
|
return "search-file"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
normalized_cmd = str(cmd_name or "").replace("_", "-").lower().strip()
|
normalized_cmd = str(cmd_name or "").replace("_", "-").lower().strip()
|
||||||
@@ -1761,8 +1919,6 @@ class CmdletExecutor:
|
|||||||
if norm != "file":
|
if norm != "file":
|
||||||
return norm
|
return norm
|
||||||
lowered = {str(a or "").strip().lower() for a in (args or [])}
|
lowered = {str(a or "").strip().lower() for a in (args or [])}
|
||||||
if "-search" in lowered or "--search" in lowered:
|
|
||||||
return "search-file"
|
|
||||||
if "-download" in lowered or "--download" in lowered or "-dl" in lowered or "--dl" in lowered:
|
if "-download" in lowered or "--download" in lowered or "-dl" in lowered or "--dl" in lowered:
|
||||||
return "download-file"
|
return "download-file"
|
||||||
if "-get" in lowered or "--get" in lowered:
|
if "-get" in lowered or "--get" in lowered:
|
||||||
@@ -1771,6 +1927,8 @@ class CmdletExecutor:
|
|||||||
return "add-file"
|
return "add-file"
|
||||||
if "-delete" in lowered or "--delete" in lowered or "-del" in lowered or "--del" in lowered:
|
if "-delete" in lowered or "--delete" in lowered or "-del" in lowered or "--del" in lowered:
|
||||||
return "delete-file"
|
return "delete-file"
|
||||||
|
if "-query" in lowered or "--query" in lowered or any(a.startswith("-query=") or a.startswith("--query=") for a in lowered):
|
||||||
|
return "search-file"
|
||||||
return norm
|
return norm
|
||||||
|
|
||||||
effective_cmd = _effective_cmd_name(cmd_name, filtered_args)
|
effective_cmd = _effective_cmd_name(cmd_name, filtered_args)
|
||||||
|
|||||||
+62
-2
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from importlib import import_module, reload as reload_module
|
from importlib import import_module, reload as reload_module
|
||||||
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
import logging
|
import logging
|
||||||
@@ -71,17 +72,76 @@ def _normalize_mod_name(mod_name: str) -> str:
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _nested_cmdlet_modules(normalized: str) -> List[str]:
|
||||||
|
"""Return nested cmdlet module candidates for names like search_file."""
|
||||||
|
if not normalized:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmdlet_dir = Path(__file__).resolve().parent.parent / "cmdlet"
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not cmdlet_dir.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
candidates: List[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
parts = normalized.split("_", 1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
children = sorted(cmdlet_dir.iterdir(), key=lambda path: path.name.lower())
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
if not child.is_dir() or not (child / "__init__.py").is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
direct_file = child / f"{normalized}.py"
|
||||||
|
if direct_file.is_file():
|
||||||
|
module_name = f"cmdlet.{child.name}.{normalized}"
|
||||||
|
if module_name not in seen:
|
||||||
|
seen.add(module_name)
|
||||||
|
candidates.append(module_name)
|
||||||
|
|
||||||
|
if len(parts) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
left, right = parts
|
||||||
|
if child.name == right and (child / f"{left}.py").is_file():
|
||||||
|
module_name = f"cmdlet.{right}.{left}"
|
||||||
|
if module_name not in seen:
|
||||||
|
seen.add(module_name)
|
||||||
|
candidates.append(module_name)
|
||||||
|
|
||||||
|
if child.name == left and (child / f"{right}.py").is_file():
|
||||||
|
module_name = f"cmdlet.{left}.{right}"
|
||||||
|
if module_name not in seen:
|
||||||
|
seen.add(module_name)
|
||||||
|
candidates.append(module_name)
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
def import_cmd_module(mod_name: str, *, reload_loaded: bool = False):
|
def import_cmd_module(mod_name: str, *, reload_loaded: bool = False):
|
||||||
"""Import a cmdlet/command module from legacy or plugin-owned packages."""
|
"""Import a cmdlet/command module from legacy or plugin-owned packages."""
|
||||||
normalized = _normalize_mod_name(mod_name)
|
normalized = _normalize_mod_name(mod_name)
|
||||||
if not normalized:
|
if not normalized:
|
||||||
return None
|
return None
|
||||||
for qualified in (
|
qualified_names = [
|
||||||
f"plugins.{normalized}.commands",
|
f"plugins.{normalized}.commands",
|
||||||
f"cmdnat.{normalized}",
|
f"cmdnat.{normalized}",
|
||||||
f"cmdlet.{normalized}",
|
f"cmdlet.{normalized}",
|
||||||
|
*_nested_cmdlet_modules(normalized),
|
||||||
normalized,
|
normalized,
|
||||||
):
|
]
|
||||||
|
|
||||||
|
seen: set[str] = set()
|
||||||
|
for qualified in qualified_names:
|
||||||
|
if qualified in seen:
|
||||||
|
continue
|
||||||
|
seen.add(qualified)
|
||||||
try:
|
try:
|
||||||
# When attempting a bare import (package is None), prefer the repo-local
|
# When attempting a bare import (package is None), prefer the repo-local
|
||||||
# `MPV` package for the `mpv` module name so we don't accidentally
|
# `MPV` package for the `mpv` module name so we don't accidentally
|
||||||
|
|||||||
@@ -2397,7 +2397,7 @@ class PipelineHubApp(App):
|
|||||||
|
|
||||||
query = f"hash:{hash_value}"
|
query = f"hash:{hash_value}"
|
||||||
base_copy = (
|
base_copy = (
|
||||||
f"file -search -instance {json.dumps(store_name)} {json.dumps(query)}"
|
f"file -instance {json.dumps(store_name)} -query {json.dumps(query)}"
|
||||||
f" | file -add -instance {json.dumps(selected_store)}"
|
f" | file -add -instance {json.dumps(selected_store)}"
|
||||||
)
|
)
|
||||||
if action == "move_to_selected_store":
|
if action == "move_to_selected_store":
|
||||||
|
|||||||
+2
-2
@@ -43,8 +43,8 @@ PIPELINE_PRESETS: List[PipelinePreset] = [
|
|||||||
PipelinePreset(
|
PipelinePreset(
|
||||||
label="Search Local Library",
|
label="Search Local Library",
|
||||||
description=
|
description=
|
||||||
"Run file -search against the local library and emit a result table for further piping.",
|
"Run file -query against the local library and emit a result table for further piping.",
|
||||||
pipeline='file -search -library local -query "<keywords>"',
|
pipeline='file -library local -query "<keywords>"',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+187
-268
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, Dict, Optional, Sequence, Tuple, List
|
from typing import Any, Dict, Optional, Sequence, Tuple, List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from copy import deepcopy
|
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -11,7 +10,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
from SYS import models
|
from SYS import models
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from SYS.logger import log, debug, debug_panel, is_debug_enabled
|
from SYS.logger import log, debug, debug_panel
|
||||||
from SYS.payload_builders import build_table_result_payload
|
from SYS.payload_builders import build_table_result_payload
|
||||||
from SYS.pipeline_progress import PipelineProgress
|
from SYS.pipeline_progress import PipelineProgress
|
||||||
from SYS.result_publication import overlay_existing_result_table, publish_result_table
|
from SYS.result_publication import overlay_existing_result_table, publish_result_table
|
||||||
@@ -92,41 +91,11 @@ class _CommandDependencies:
|
|||||||
self._plugins[cache_key] = plugin
|
self._plugins[cache_key] = plugin
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
DEBUG_PIPE_NOTE_PREVIEW_LENGTH = 256
|
|
||||||
|
|
||||||
# Protocol schemes that identify a remote resource / not a local file path.
|
|
||||||
# Used by multiple methods in this file to guard against URL strings being
|
|
||||||
# treated as local file paths.
|
|
||||||
_REMOTE_URL_PREFIXES: tuple[str, ...] = (
|
_REMOTE_URL_PREFIXES: tuple[str, ...] = (
|
||||||
"http://", "https://", "ftp://", "ftps://", "magnet:", "torrent:", "tidal:", "hydrus:",
|
"http://", "https://", "ftp://", "ftps://", "magnet:", "torrent:", "tidal:", "hydrus:",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _truncate_debug_note_text(value: Any) -> str:
|
|
||||||
raw = str(value or "")
|
|
||||||
if len(raw) <= DEBUG_PIPE_NOTE_PREVIEW_LENGTH:
|
|
||||||
return raw
|
|
||||||
return raw[:DEBUG_PIPE_NOTE_PREVIEW_LENGTH].rstrip() + "..."
|
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_pipe_object_for_debug(pipe_obj: models.PipeObject) -> models.PipeObject:
|
|
||||||
safe_po = deepcopy(pipe_obj)
|
|
||||||
try:
|
|
||||||
extra = safe_po.extra
|
|
||||||
if isinstance(extra, dict):
|
|
||||||
sanitized = dict(extra)
|
|
||||||
notes = sanitized.get("notes")
|
|
||||||
if isinstance(notes, dict):
|
|
||||||
truncated_notes: Dict[str, str] = {}
|
|
||||||
for note_name, note_value in notes.items():
|
|
||||||
truncated_notes[str(note_name)] = _truncate_debug_note_text(note_value)
|
|
||||||
sanitized["notes"] = truncated_notes
|
|
||||||
safe_po.extra = sanitized
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return safe_po
|
|
||||||
|
|
||||||
|
|
||||||
def _maybe_apply_florencevision_tags(
|
def _maybe_apply_florencevision_tags(
|
||||||
media_path: Path,
|
media_path: Path,
|
||||||
tags: List[str],
|
tags: List[str],
|
||||||
@@ -224,9 +193,9 @@ class Add_File(Cmdlet):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
name="add-file",
|
name="add-file",
|
||||||
summary=
|
summary=
|
||||||
"Ingest a local media file to a configured instance, upload plugin, or local directory.",
|
"Ingest a local media file to a configured store or plugin destination.",
|
||||||
usage=
|
usage=
|
||||||
"add-file (-path <filepath> | <piped>) (-instance <name|path> | -plugin <upload-plugin>) [-delete]",
|
"add-file (-path <filepath> | <piped>) (-instance <store-name> | -plugin <plugin> [-instance <name|path>]) [-delete]",
|
||||||
arg=[
|
arg=[
|
||||||
SharedArgs.PATH,
|
SharedArgs.PATH,
|
||||||
SharedArgs.INSTANCE,
|
SharedArgs.INSTANCE,
|
||||||
@@ -242,11 +211,10 @@ class Add_File(Cmdlet):
|
|||||||
],
|
],
|
||||||
detail=[
|
detail=[
|
||||||
"Note: add-file ingests local files. To fetch remote sources, use download-file and pipe into add-file.",
|
"Note: add-file ingests local files. To fetch remote sources, use download-file and pipe into add-file.",
|
||||||
"- Instance/location options (use -instance):",
|
"- Store options (use -instance without -plugin):",
|
||||||
" hydrus: Upload to Hydrus database with metadata tagging",
|
" hydrus: Upload to Hydrus database with metadata tagging",
|
||||||
" local: Copy file to local directory",
|
"- Plugin options (use -plugin):",
|
||||||
" <path>: Copy file to specified directory",
|
" local: Copy file to a configured local destination or direct path via -instance",
|
||||||
"- Upload plugin options (use -plugin):",
|
|
||||||
" 0x0: Upload to 0x0.st for temporary hosting",
|
" 0x0: Upload to 0x0.st for temporary hosting",
|
||||||
" file.io: Upload to file.io for temporary hosting",
|
" file.io: Upload to file.io for temporary hosting",
|
||||||
" internetarchive: Upload to archive.org (optional tag: ia:<identifier> to upload into an existing item)",
|
" internetarchive: Upload to archive.org (optional tag: ia:<identifier> to upload into an existing item)",
|
||||||
@@ -254,6 +222,7 @@ class Add_File(Cmdlet):
|
|||||||
],
|
],
|
||||||
examples=[
|
examples=[
|
||||||
'download-file "https://themathesontrust.org/papers/christianity/alcock-alphabet1.pdf" | add-file -instance tutorial',
|
'download-file "https://themathesontrust.org/papers/christianity/alcock-alphabet1.pdf" | add-file -instance tutorial',
|
||||||
|
'@1 | add-file -plugin local -instance C:\\Users\\Me\\Downloads',
|
||||||
'add-file -plugin ftp -instance archive -path C:\\Media\\report.pdf',
|
'add-file -plugin ftp -instance archive -path C:\\Media\\report.pdf',
|
||||||
],
|
],
|
||||||
exec=self.run,
|
exec=self.run,
|
||||||
@@ -275,25 +244,19 @@ class Add_File(Cmdlet):
|
|||||||
source_url_arg = parsed.get("url")
|
source_url_arg = parsed.get("url")
|
||||||
plugin_name = parsed.get("plugin")
|
plugin_name = parsed.get("plugin")
|
||||||
delete_after = parsed.get("delete", False)
|
delete_after = parsed.get("delete", False)
|
||||||
|
local_export_destination: Optional[str] = None
|
||||||
if plugin_name and not plugin_instance and location:
|
if plugin_name and not plugin_instance and location:
|
||||||
plugin_instance = location
|
plugin_instance = location
|
||||||
|
|
||||||
# Convenience: when piping a file into add-file, allow `-path <existing dir>`
|
# Backward-compatible shorthand: when piping a file into add-file, allow
|
||||||
# to act as the destination export directory.
|
# `-path <existing dir>` to normalize into the local export plugin path.
|
||||||
# Example: screen-shot "https://..." | add-file -path "C:\Users\Admin\Desktop"
|
|
||||||
if path_arg and not location and not plugin_name:
|
if path_arg and not location and not plugin_name:
|
||||||
try:
|
try:
|
||||||
candidate_dir = Path(str(path_arg))
|
candidate_dir = Path(str(path_arg))
|
||||||
if candidate_dir.exists() and candidate_dir.is_dir():
|
if candidate_dir.exists() and candidate_dir.is_dir():
|
||||||
debug_panel(
|
plugin_name = "local"
|
||||||
"add-file destination",
|
plugin_instance = str(candidate_dir)
|
||||||
[
|
local_export_destination = str(candidate_dir)
|
||||||
("mode", "local export"),
|
|
||||||
("path", candidate_dir),
|
|
||||||
],
|
|
||||||
border_style="cyan",
|
|
||||||
)
|
|
||||||
location = str(candidate_dir)
|
|
||||||
path_arg = None
|
path_arg = None
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -397,13 +360,44 @@ class Add_File(Cmdlet):
|
|||||||
is_storage_backend_location = False
|
is_storage_backend_location = False
|
||||||
|
|
||||||
if location and not plugin_name and not is_storage_backend_location:
|
if location and not plugin_name and not is_storage_backend_location:
|
||||||
if not Add_File._looks_like_local_export_target(str(location)):
|
resolved_local_instance, resolved_local_path = Add_File._resolve_local_export_plugin_target(
|
||||||
|
location,
|
||||||
|
config,
|
||||||
|
deps=deps,
|
||||||
|
require_explicit=True,
|
||||||
|
)
|
||||||
|
if resolved_local_path:
|
||||||
|
plugin_name = "local"
|
||||||
|
plugin_instance = resolved_local_instance or str(location)
|
||||||
|
location = None
|
||||||
|
local_export_destination = resolved_local_path
|
||||||
|
else:
|
||||||
log(
|
log(
|
||||||
f"Storage backend '{location}' not found. Use -path for local export or configure that store backend.",
|
f"Storage backend '{location}' not found. Use -plugin local -instance <name|path> for local export or configure that store backend.",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
normalized_plugin_name = Add_File._normalize_provider_key(plugin_name)
|
||||||
|
if normalized_plugin_name == "local":
|
||||||
|
resolved_local_instance, resolved_local_path = Add_File._resolve_local_export_plugin_target(
|
||||||
|
plugin_instance or location,
|
||||||
|
config,
|
||||||
|
deps=deps,
|
||||||
|
require_explicit=bool(plugin_instance or location),
|
||||||
|
)
|
||||||
|
if not resolved_local_path:
|
||||||
|
requested_local = str(plugin_instance or location or "").strip() or "<default>"
|
||||||
|
log(
|
||||||
|
f"Local destination '{requested_local}' is not configured. Use -plugin local -instance <name|path>.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
plugin_name = "local"
|
||||||
|
plugin_instance = resolved_local_instance or str(plugin_instance or location or "").strip() or None
|
||||||
|
location = None
|
||||||
|
local_export_destination = resolved_local_path
|
||||||
|
|
||||||
plugin_storage_backend = None
|
plugin_storage_backend = None
|
||||||
if plugin_name:
|
if plugin_name:
|
||||||
plugin_storage_backend = Add_File._resolve_plugin_storage_backend(
|
plugin_storage_backend = Add_File._resolve_plugin_storage_backend(
|
||||||
@@ -469,46 +463,8 @@ class Add_File(Cmdlet):
|
|||||||
except Exception:
|
except Exception:
|
||||||
use_steps = False
|
use_steps = False
|
||||||
|
|
||||||
try:
|
|
||||||
debug_panel(
|
|
||||||
"add-file",
|
|
||||||
[
|
|
||||||
("result_type", type(result).__name__),
|
|
||||||
("items", total_items),
|
|
||||||
("location", location),
|
|
||||||
("plugin", plugin_name),
|
|
||||||
("instance", plugin_instance),
|
|
||||||
("delete", delete_after),
|
|
||||||
],
|
|
||||||
border_style="cyan",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# add-file is ingestion-only: it does not download URLs here.
|
# add-file is ingestion-only: it does not download URLs here.
|
||||||
|
|
||||||
# Show a concise PipeObject preview when debug logging is enabled to aid pipeline troubleshooting.
|
|
||||||
if is_debug_enabled():
|
|
||||||
preview_items = (
|
|
||||||
items_to_process if isinstance(items_to_process, list)
|
|
||||||
else [items_to_process]
|
|
||||||
)
|
|
||||||
max_preview = 5
|
|
||||||
for idx, item in enumerate(preview_items[:max_preview]):
|
|
||||||
po = item if isinstance(item, models.PipeObject) else None
|
|
||||||
if po is None:
|
|
||||||
try:
|
|
||||||
po = coerce_to_pipe_object(item, path_arg)
|
|
||||||
except Exception:
|
|
||||||
po = None
|
|
||||||
if po is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
safe_po = _sanitize_pipe_object_for_debug(po)
|
|
||||||
safe_po.debug_table()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
should_present_directory_selector = bool(dir_scan_mode and not has_downstream_stage)
|
should_present_directory_selector = bool(dir_scan_mode and not has_downstream_stage)
|
||||||
if dir_scan_mode and has_downstream_stage:
|
if dir_scan_mode and has_downstream_stage:
|
||||||
debug(
|
debug(
|
||||||
@@ -666,12 +622,19 @@ class Add_File(Cmdlet):
|
|||||||
if use_steps and steps_started:
|
if use_steps and steps_started:
|
||||||
progress.step("resolving source")
|
progress.step("resolving source")
|
||||||
|
|
||||||
|
export_destination = (
|
||||||
|
Path(local_export_destination)
|
||||||
|
if local_export_destination
|
||||||
|
else Path(location)
|
||||||
|
if location and not is_storage_backend_location
|
||||||
|
else None
|
||||||
|
)
|
||||||
media_path, file_hash, temp_dir_to_cleanup = self._resolve_source(
|
media_path, file_hash, temp_dir_to_cleanup = self._resolve_source(
|
||||||
item,
|
item,
|
||||||
path_arg,
|
path_arg,
|
||||||
pipe_obj,
|
pipe_obj,
|
||||||
config,
|
config,
|
||||||
export_destination=(Path(location) if location and not is_storage_backend_location else None),
|
export_destination=export_destination,
|
||||||
store_instance=storage_registry,
|
store_instance=storage_registry,
|
||||||
deps=deps,
|
deps=deps,
|
||||||
)
|
)
|
||||||
@@ -679,19 +642,6 @@ class Add_File(Cmdlet):
|
|||||||
media_path, file_hash, temp_dir_to_cleanup = Add_File._download_piped_source(
|
media_path, file_hash, temp_dir_to_cleanup = Add_File._download_piped_source(
|
||||||
pipe_obj, config, storage_registry, deps=deps
|
pipe_obj, config, storage_registry, deps=deps
|
||||||
)
|
)
|
||||||
if media_path:
|
|
||||||
try:
|
|
||||||
debug_panel(
|
|
||||||
f"add-file source {idx}/{max(1, total_items)}",
|
|
||||||
[
|
|
||||||
("path", media_path),
|
|
||||||
("hash", file_hash or "N/A"),
|
|
||||||
("plugin", plugin_name or "local"),
|
|
||||||
],
|
|
||||||
border_style="green",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if not media_path:
|
if not media_path:
|
||||||
failures += 1
|
failures += 1
|
||||||
continue
|
continue
|
||||||
@@ -768,13 +718,8 @@ class Add_File(Cmdlet):
|
|||||||
store_instance=storage_registry,
|
store_instance=storage_registry,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
code = self._handle_local_export(
|
log(f"Invalid storage backend: {location}", file=sys.stderr)
|
||||||
media_path,
|
code = 1
|
||||||
location,
|
|
||||||
pipe_obj,
|
|
||||||
config,
|
|
||||||
delete_after_item
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
debug(f"[add-file] ERROR: Failed to resolve location: {exc}")
|
debug(f"[add-file] ERROR: Failed to resolve location: {exc}")
|
||||||
log(f"Invalid location: {location}", file=sys.stderr)
|
log(f"Invalid location: {location}", file=sys.stderr)
|
||||||
@@ -1371,27 +1316,6 @@ class Add_File(Cmdlet):
|
|||||||
pass
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _looks_like_local_export_target(location: str) -> bool:
|
|
||||||
target = str(location or "").strip()
|
|
||||||
if not target:
|
|
||||||
return False
|
|
||||||
|
|
||||||
target_path = Path(target).expanduser()
|
|
||||||
try:
|
|
||||||
if target_path.exists():
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if target.startswith((".", "~")):
|
|
||||||
return True
|
|
||||||
if "\\" in target or "/" in target:
|
|
||||||
return True
|
|
||||||
if len(target) >= 2 and target[1] == ":":
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_source(
|
def _resolve_source(
|
||||||
result: Any,
|
result: Any,
|
||||||
@@ -1608,6 +1532,45 @@ class Add_File(Cmdlet):
|
|||||||
|
|
||||||
return resolved_text
|
return resolved_text
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolve_local_export_plugin_target(
|
||||||
|
requested: Optional[Any],
|
||||||
|
config: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
deps: Optional[_CommandDependencies] = None,
|
||||||
|
require_explicit: bool = False,
|
||||||
|
) -> tuple[Optional[str], Optional[str]]:
|
||||||
|
if deps is None:
|
||||||
|
deps = _CommandDependencies(config)
|
||||||
|
|
||||||
|
file_provider = deps.get_plugin_with_capability("local", "upload")
|
||||||
|
if file_provider is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
resolver = getattr(file_provider, "resolve_destination", None)
|
||||||
|
if not callable(resolver):
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
requested_text = str(requested or "").strip() or None
|
||||||
|
try:
|
||||||
|
resolved_name, settings = resolver(
|
||||||
|
requested_text,
|
||||||
|
require_explicit=require_explicit,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
try:
|
||||||
|
resolved_name, settings = resolver(requested_text)
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
path_value = str((settings or {}).get("path") or "").strip()
|
||||||
|
if not path_value:
|
||||||
|
return None, None
|
||||||
|
resolved_text = str(resolved_name or requested_text or "").strip() or None
|
||||||
|
return resolved_text, path_value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _maybe_download_plugin_result(
|
def _maybe_download_plugin_result(
|
||||||
result: Any,
|
result: Any,
|
||||||
@@ -2294,136 +2257,72 @@ class Add_File(Cmdlet):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _handle_local_export(
|
def _emit_plugin_upload_payload(
|
||||||
media_path: Path,
|
upload_payload: Dict[str, Any],
|
||||||
location: str,
|
plugin_name: str,
|
||||||
|
instance_name: Optional[str],
|
||||||
pipe_obj: models.PipeObject,
|
pipe_obj: models.PipeObject,
|
||||||
config: Dict[str,
|
media_path: Path,
|
||||||
Any],
|
|
||||||
delete_after: bool,
|
delete_after: bool,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""Handle exporting to a specific local path (Copy)."""
|
payload = dict(upload_payload or {})
|
||||||
try:
|
extra_updates: Dict[str, Any] = {}
|
||||||
destination_root = Path(location)
|
raw_extra = payload.get("extra")
|
||||||
except Exception as exc:
|
if isinstance(raw_extra, dict):
|
||||||
log(f"❌ Invalid destination path '{location}': {exc}", file=sys.stderr)
|
extra_updates.update(raw_extra)
|
||||||
return 1
|
|
||||||
|
|
||||||
direct_export_download = False
|
if plugin_name:
|
||||||
try:
|
extra_updates.setdefault("plugin", plugin_name)
|
||||||
if isinstance(pipe_obj.extra, dict):
|
if instance_name:
|
||||||
direct_export_download = bool(pipe_obj.extra.pop("_direct_export_download", False))
|
extra_updates.setdefault("instance", instance_name)
|
||||||
except Exception:
|
|
||||||
direct_export_download = False
|
|
||||||
|
|
||||||
try:
|
raw_urls = payload.get("url")
|
||||||
debug_panel(
|
if isinstance(raw_urls, str):
|
||||||
"add-file export",
|
url_values = [raw_urls.strip()] if raw_urls.strip() else []
|
||||||
[
|
extra_updates["url"] = url_values
|
||||||
("destination", destination_root),
|
elif isinstance(raw_urls, (list, tuple, set)):
|
||||||
("source", media_path),
|
url_values = [str(item).strip() for item in raw_urls if str(item).strip()]
|
||||||
],
|
extra_updates["url"] = url_values
|
||||||
border_style="green",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
result = None
|
relationships = payload.get("relationships")
|
||||||
tags, url, title, f_hash = Add_File._prepare_metadata(result, media_path, pipe_obj, config)
|
|
||||||
|
|
||||||
# Determine Filename (Title-based)
|
|
||||||
title_value = title
|
|
||||||
if not title_value:
|
|
||||||
# Try to find title in tags
|
|
||||||
title_tag = next(
|
|
||||||
(t for t in tags if str(t).strip().lower().startswith("title:")),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
if title_tag:
|
|
||||||
title_value = title_tag.split(":", 1)[1].strip()
|
|
||||||
|
|
||||||
if not title_value:
|
|
||||||
title_value = media_path.stem.replace("_", " ").strip()
|
|
||||||
|
|
||||||
safe_title = "".join(
|
|
||||||
c for c in title_value if c.isalnum() or c in " ._-()[]{}'`"
|
|
||||||
).strip()
|
|
||||||
base_name = safe_title or media_path.stem
|
|
||||||
|
|
||||||
# Fix to prevent double extensions (e.g., file.exe.exe)
|
|
||||||
# If the base name already ends with the extension of the media file,
|
|
||||||
# don't append it again.
|
|
||||||
file_ext = media_path.suffix
|
|
||||||
if file_ext and base_name.lower().endswith(file_ext.lower()):
|
|
||||||
new_name = base_name
|
|
||||||
else:
|
|
||||||
new_name = base_name + file_ext
|
|
||||||
|
|
||||||
destination_root.mkdir(parents=True, exist_ok=True)
|
|
||||||
target_path = destination_root / new_name
|
|
||||||
|
|
||||||
if direct_export_download:
|
|
||||||
target_path = media_path
|
|
||||||
else:
|
|
||||||
if target_path.exists():
|
|
||||||
target_path = unique_path(target_path)
|
|
||||||
|
|
||||||
# COPY Operation (Safe Export)
|
|
||||||
try:
|
|
||||||
shutil.copy2(str(media_path), target_path)
|
|
||||||
except Exception as exc:
|
|
||||||
log(f"❌ Failed to export file: {exc}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Copy Sidecars
|
|
||||||
Add_File._copy_sidecars(media_path, target_path)
|
|
||||||
|
|
||||||
# Ensure hash for exported copy
|
|
||||||
if not f_hash:
|
|
||||||
try:
|
|
||||||
f_hash = sha256_file(target_path)
|
|
||||||
except Exception:
|
|
||||||
f_hash = None
|
|
||||||
|
|
||||||
# Write Metadata Sidecars (since it's an export)
|
|
||||||
relationships = Add_File._get_relationships(result, pipe_obj)
|
|
||||||
try:
|
|
||||||
write_sidecar(target_path, tags, url, f_hash)
|
|
||||||
from SYS.metadata import write_metadata # lazy: avoids 1000+ module chain at startup
|
|
||||||
write_metadata(
|
|
||||||
target_path,
|
|
||||||
hash_value=f_hash,
|
|
||||||
url=url,
|
|
||||||
relationships=relationships or []
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Update PipeObject and emit
|
|
||||||
extra_updates = {
|
|
||||||
"url": url,
|
|
||||||
"export_path": str(destination_root),
|
|
||||||
}
|
|
||||||
if relationships:
|
if relationships:
|
||||||
extra_updates["relationships"] = relationships
|
try:
|
||||||
|
pipe_obj.relationships = relationships
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
chosen_title = title or title_value or pipe_obj.title or target_path.name
|
tags = payload.get("tag")
|
||||||
|
if isinstance(tags, list):
|
||||||
|
tag_values = [str(tag) for tag in tags]
|
||||||
|
else:
|
||||||
|
tag_values = list(pipe_obj.tag or [])
|
||||||
|
|
||||||
|
title_value = str(payload.get("title") or pipe_obj.title or media_path.name).strip() or media_path.name
|
||||||
|
path_value = str(payload.get("path") or pipe_obj.path or media_path).strip()
|
||||||
|
hash_value = str(
|
||||||
|
payload.get("hash")
|
||||||
|
or payload.get("file_hash")
|
||||||
|
or getattr(pipe_obj, "hash", None)
|
||||||
|
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
|
||||||
|
|
||||||
Add_File._update_pipe_object_destination(
|
Add_File._update_pipe_object_destination(
|
||||||
pipe_obj,
|
pipe_obj,
|
||||||
hash_value=f_hash or "unknown",
|
hash_value=hash_value,
|
||||||
store="local",
|
store=store_value,
|
||||||
path=str(target_path),
|
provider=str(provider_value) if provider_value else None,
|
||||||
tag=tags,
|
path=path_value,
|
||||||
title=chosen_title,
|
tag=tag_values,
|
||||||
|
title=title_value,
|
||||||
extra_updates=extra_updates,
|
extra_updates=extra_updates,
|
||||||
)
|
)
|
||||||
Add_File._emit_pipe_object(pipe_obj)
|
Add_File._emit_pipe_object(pipe_obj)
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
# Only delete if explicitly requested!
|
|
||||||
Add_File._cleanup_after_success(media_path, delete_source=delete_after)
|
Add_File._cleanup_after_success(media_path, delete_source=delete_after)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -2459,10 +2358,37 @@ class Add_File(Cmdlet):
|
|||||||
show_available_plugins_panel(sorted(available_uploads))
|
show_available_plugins_panel(sorted(available_uploads))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
hoster_url = file_provider.upload(
|
upload_kwargs: Dict[str, Any] = {
|
||||||
|
"pipe_obj": pipe_obj,
|
||||||
|
"instance": instance_name,
|
||||||
|
}
|
||||||
|
normalized_plugin_name = Add_File._normalize_provider_key(plugin_name)
|
||||||
|
f_hash = Add_File._resolve_file_hash(None, media_path, pipe_obj, None)
|
||||||
|
if normalized_plugin_name == "local":
|
||||||
|
result = None
|
||||||
|
tags, urls, title, f_hash = Add_File._prepare_metadata(result, media_path, pipe_obj, config)
|
||||||
|
relationships = Add_File._get_relationships(result, pipe_obj)
|
||||||
|
direct_export_download = False
|
||||||
|
try:
|
||||||
|
if isinstance(pipe_obj.extra, dict):
|
||||||
|
direct_export_download = bool(pipe_obj.extra.pop("_direct_export_download", False))
|
||||||
|
except Exception:
|
||||||
|
direct_export_download = False
|
||||||
|
|
||||||
|
upload_kwargs.update(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"tags": tags,
|
||||||
|
"urls": urls,
|
||||||
|
"hash_value": f_hash,
|
||||||
|
"relationships": relationships,
|
||||||
|
"direct_export_download": direct_export_download,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
upload_result = file_provider.upload(
|
||||||
str(media_path),
|
str(media_path),
|
||||||
pipe_obj=pipe_obj,
|
**upload_kwargs,
|
||||||
instance=instance_name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
duplicate_upload = False
|
duplicate_upload = False
|
||||||
@@ -2478,29 +2404,22 @@ class Add_File(Cmdlet):
|
|||||||
duplicate_rule = ""
|
duplicate_rule = ""
|
||||||
duplicate_target = ""
|
duplicate_target = ""
|
||||||
|
|
||||||
try:
|
|
||||||
debug_panel(
|
|
||||||
"add-file plugin upload",
|
|
||||||
[
|
|
||||||
("plugin", plugin_name),
|
|
||||||
("instance", instance_name or "<default>"),
|
|
||||||
("source", media_path),
|
|
||||||
("duplicate", duplicate_upload),
|
|
||||||
("rule", duplicate_rule or "none"),
|
|
||||||
("target", duplicate_target or ""),
|
|
||||||
("url", hoster_url),
|
|
||||||
],
|
|
||||||
border_style="yellow" if duplicate_upload else "green",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
f_hash = Add_File._resolve_file_hash(None, media_path, pipe_obj, None)
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Upload failed: {exc}", file=sys.stderr)
|
log(f"Upload failed: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if isinstance(upload_result, dict):
|
||||||
|
return Add_File._emit_plugin_upload_payload(
|
||||||
|
upload_result,
|
||||||
|
plugin_name,
|
||||||
|
instance_name,
|
||||||
|
pipe_obj,
|
||||||
|
media_path,
|
||||||
|
delete_after,
|
||||||
|
)
|
||||||
|
|
||||||
|
hoster_url = str(upload_result or "").strip()
|
||||||
|
|
||||||
# Update PipeObject and emit
|
# Update PipeObject and emit
|
||||||
extra_updates: Dict[str,
|
extra_updates: Dict[str,
|
||||||
Any] = {
|
Any] = {
|
||||||
|
|||||||
+20
-6
@@ -21,7 +21,6 @@ class File(Cmdlet):
|
|||||||
"get": {"-get", "--get"},
|
"get": {"-get", "--get"},
|
||||||
"merge": {"-merge", "--merge"},
|
"merge": {"-merge", "--merge"},
|
||||||
"download": {"-download", "--download", "-dl", "--dl"},
|
"download": {"-download", "--download", "-dl", "--dl"},
|
||||||
"search": {"-search", "--search"},
|
|
||||||
"convert": {"-convert", "--convert"},
|
"convert": {"-convert", "--convert"},
|
||||||
"trim": {"-trim", "--trim"},
|
"trim": {"-trim", "--trim"},
|
||||||
"archive": {"-archive", "--archive"},
|
"archive": {"-archive", "--archive"},
|
||||||
@@ -45,9 +44,10 @@ class File(Cmdlet):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
name="file",
|
name="file",
|
||||||
summary="Manage file operations with one command",
|
summary="Manage file operations with one command",
|
||||||
usage='file (-add|-delete|-get|-merge|-download|-search|-convert|-trim|-archive|-screenshot) [args]',
|
usage='file -query <query> [args] | file (-add|-delete|-get|-merge|-download|-convert|-trim|-archive|-screenshot) [args]',
|
||||||
arg=[
|
arg=[
|
||||||
SharedArgs.QUERY,
|
SharedArgs.QUERY,
|
||||||
|
SharedArgs.PLUGIN,
|
||||||
SharedArgs.INSTANCE,
|
SharedArgs.INSTANCE,
|
||||||
SharedArgs.PATH,
|
SharedArgs.PATH,
|
||||||
CmdletArg("-add", type="flag", required=False, description="Run add-file"),
|
CmdletArg("-add", type="flag", required=False, description="Run add-file"),
|
||||||
@@ -55,21 +55,32 @@ class File(Cmdlet):
|
|||||||
CmdletArg("-get", type="flag", required=False, description="Run get-file"),
|
CmdletArg("-get", type="flag", required=False, description="Run get-file"),
|
||||||
CmdletArg("-merge", type="flag", required=False, description="Run merge-file"),
|
CmdletArg("-merge", type="flag", required=False, description="Run merge-file"),
|
||||||
CmdletArg("-download", type="flag", required=False, description="Run download-file", alias="dl"),
|
CmdletArg("-download", type="flag", required=False, description="Run download-file", alias="dl"),
|
||||||
CmdletArg("-search", type="flag", required=False, description="Run search-file"),
|
|
||||||
CmdletArg("-convert", type="flag", required=False, description="Run convert-file"),
|
CmdletArg("-convert", type="flag", required=False, description="Run convert-file"),
|
||||||
CmdletArg("-trim", type="flag", required=False, description="Run trim-file"),
|
CmdletArg("-trim", type="flag", required=False, description="Run trim-file"),
|
||||||
CmdletArg("-archive", type="flag", required=False, description="Run archive-file"),
|
CmdletArg("-archive", type="flag", required=False, description="Run archive-file"),
|
||||||
CmdletArg("-screenshot", type="flag", required=False, description="Run screen-shot", alias="shot"),
|
CmdletArg("-screenshot", type="flag", required=False, description="Run screen-shot", alias="shot"),
|
||||||
],
|
],
|
||||||
detail=[
|
detail=[
|
||||||
"- Exactly one action flag is required.",
|
"- Use -query to run search-file through the unified file command.",
|
||||||
|
"- Otherwise, exactly one non-search action flag is required.",
|
||||||
"- Remaining args are passed through to the selected file cmdlet.",
|
"- Remaining args are passed through to the selected file cmdlet.",
|
||||||
"- Examples: file -add ..., file -delete ..., file -merge ...",
|
"- Examples: file -query ..., file -add ..., file -delete ...",
|
||||||
],
|
],
|
||||||
exec=self.run,
|
exec=self.run,
|
||||||
)
|
)
|
||||||
self.register()
|
self.register()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_query_arg(args: Sequence[str]) -> bool:
|
||||||
|
query_flags = {"-query", "--query"}
|
||||||
|
for token in args or []:
|
||||||
|
text = str(token or "").strip().lower()
|
||||||
|
if text in query_flags:
|
||||||
|
return True
|
||||||
|
if any(text.startswith(f"{flag}=") for flag in query_flags):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _extract_action(cls, args: Sequence[str]) -> tuple[str | None, List[str], List[str]]:
|
def _extract_action(cls, args: Sequence[str]) -> tuple[str | None, List[str], List[str]]:
|
||||||
matched_actions: List[str] = []
|
matched_actions: List[str] = []
|
||||||
@@ -93,6 +104,9 @@ class File(Cmdlet):
|
|||||||
if action not in unique_actions:
|
if action not in unique_actions:
|
||||||
unique_actions.append(action)
|
unique_actions.append(action)
|
||||||
|
|
||||||
|
if not unique_actions and cls._has_query_arg(passthrough):
|
||||||
|
return "search", passthrough, unique_actions
|
||||||
|
|
||||||
if len(unique_actions) != 1:
|
if len(unique_actions) != 1:
|
||||||
return None, passthrough, unique_actions
|
return None, passthrough, unique_actions
|
||||||
return unique_actions[0], passthrough, unique_actions
|
return unique_actions[0], passthrough, unique_actions
|
||||||
@@ -125,7 +139,7 @@ class File(Cmdlet):
|
|||||||
if action is None:
|
if action is None:
|
||||||
if not seen:
|
if not seen:
|
||||||
log(
|
log(
|
||||||
"file: missing action flag; choose exactly one of -add, -delete, -get, -merge, -download, -search, -convert, -trim, -archive, -screenshot",
|
"file: missing action; use -query for search or choose exactly one of -add, -delete, -get, -merge, -download, -convert, -trim, -archive, -screenshot",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -143,13 +143,13 @@ Selection & download flows
|
|||||||
|
|
||||||
- `download-file` integration: With a file row (http(s) path), `@2 | download-file` will download the file. The `download-file` cmdlet expands AllDebrid magnet folders and will call the provider layer to fetch file bytes as appropriate.
|
- `download-file` integration: With a file row (http(s) path), `@2 | download-file` will download the file. The `download-file` cmdlet expands AllDebrid magnet folders and will call the provider layer to fetch file bytes as appropriate.
|
||||||
|
|
||||||
- `add-file` convenience: Piping a file row into `add-file -path <dest>` will trigger add-file's provider-aware logic. If the piped item has `table == 'alldebrid'` and a http(s) `path`, `add-file` will call `provider.download()` into a temporary directory and then ingest the downloaded file, cleaning up the temp when done. Example:
|
- `add-file` convenience: Piping a file row into `add-file -plugin local -instance <dest>` will trigger add-file's provider-aware logic. If the piped item has `table == 'alldebrid'` and a http(s) `path`, `add-file` will call `provider.download()` into a temporary directory and then ingest the downloaded file, cleaning up the temp when done. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
# Expand magnet and add first file to local directory
|
# Expand magnet and add first file to local directory
|
||||||
search-file -plugin alldebrid "*"
|
search-file -plugin alldebrid "*"
|
||||||
@3 # view files
|
@3 # view files
|
||||||
@1 | add-file -path C:\mydir
|
@1 | add-file -plugin local -instance C:\mydir
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes & troubleshooting
|
Notes & troubleshooting
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from ProviderCore.base import Provider
|
||||||
|
from SYS.metadata import write_metadata, write_tags
|
||||||
|
from SYS.utils import sanitize_filename, sha256_file, unique_path
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_sidecars(source_path: Path, target_path: Path) -> None:
|
||||||
|
possible_sidecars = [
|
||||||
|
source_path.with_suffix(source_path.suffix + ".json"),
|
||||||
|
source_path.with_name(source_path.name + ".tag"),
|
||||||
|
source_path.with_name(source_path.name + ".metadata"),
|
||||||
|
source_path.with_name(source_path.name + ".notes"),
|
||||||
|
]
|
||||||
|
for sidecar in possible_sidecars:
|
||||||
|
try:
|
||||||
|
if not sidecar.exists():
|
||||||
|
continue
|
||||||
|
suffix_part = sidecar.name.replace(source_path.name, "", 1)
|
||||||
|
target_sidecar = target_path.parent / f"{target_path.name}{suffix_part}"
|
||||||
|
target_sidecar.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
shutil.copy2(str(sidecar), target_sidecar)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
class Local(Provider):
|
||||||
|
PLUGIN_NAME = "local"
|
||||||
|
PLUGIN_ALIASES = ("filesystem", "fs")
|
||||||
|
MULTI_INSTANCE = True
|
||||||
|
SUPPORTED_CMDLETS = frozenset({"add-file"})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
return "Local Filesystem"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config_schema(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "path",
|
||||||
|
"label": "Destination Path",
|
||||||
|
"type": "path",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"placeholder": r"C:\Users\Me\Downloads",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "create_dirs",
|
||||||
|
"label": "Create Missing Directories",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
def config_helper_text(self) -> str:
|
||||||
|
return "Configure named local export destinations and use add-file -plugin local -instance <name|path>."
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_path(value: Any) -> bool:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
if text.startswith((".", "~")):
|
||||||
|
return True
|
||||||
|
if "\\" in text or "/" in text:
|
||||||
|
return True
|
||||||
|
if len(text) >= 2 and text[1] == ":":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _settings_from_config(
|
||||||
|
self,
|
||||||
|
conf: Optional[Dict[str, Any]],
|
||||||
|
*,
|
||||||
|
instance_name: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
entry = dict(conf or {})
|
||||||
|
path_value = str(entry.get("path") or entry.get("PATH") or "").strip()
|
||||||
|
return {
|
||||||
|
"instance": str(instance_name or entry.get("_instance_name") or "").strip() or None,
|
||||||
|
"path": path_value,
|
||||||
|
"create_dirs": bool(entry.get("create_dirs", entry.get("createDirs", True))),
|
||||||
|
}
|
||||||
|
|
||||||
|
def resolve_destination(
|
||||||
|
self,
|
||||||
|
instance_name: Optional[str] = None,
|
||||||
|
*,
|
||||||
|
require_explicit: bool = False,
|
||||||
|
) -> Tuple[Optional[str], Dict[str, Any]]:
|
||||||
|
requested = str(instance_name or "").strip()
|
||||||
|
if requested:
|
||||||
|
resolved_name, conf = self.resolve_plugin_instance(requested, require_explicit=True)
|
||||||
|
settings = self._settings_from_config(conf, instance_name=resolved_name)
|
||||||
|
if settings.get("path"):
|
||||||
|
return resolved_name or requested, settings
|
||||||
|
if self._looks_like_path(requested):
|
||||||
|
return requested, {
|
||||||
|
"instance": requested,
|
||||||
|
"path": requested,
|
||||||
|
"create_dirs": True,
|
||||||
|
}
|
||||||
|
if require_explicit:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
resolved_name, conf = self.resolve_plugin_instance(None, require_explicit=False)
|
||||||
|
settings = self._settings_from_config(conf, instance_name=resolved_name)
|
||||||
|
if settings.get("path"):
|
||||||
|
return resolved_name, settings
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
def validate(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def upload(self, file_path: str, **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
source_path = Path(str(file_path or "")).expanduser()
|
||||||
|
if not source_path.exists() or not source_path.is_file():
|
||||||
|
raise FileNotFoundError(f"File not found: {source_path}")
|
||||||
|
|
||||||
|
requested_instance = str(kwargs.get("instance") or kwargs.get("store") or "").strip() or None
|
||||||
|
resolved_name, settings = self.resolve_destination(
|
||||||
|
requested_instance,
|
||||||
|
require_explicit=bool(requested_instance),
|
||||||
|
)
|
||||||
|
destination_text = str(settings.get("path") or "").strip()
|
||||||
|
if not destination_text:
|
||||||
|
requested_label = requested_instance or "<default>"
|
||||||
|
raise ValueError(
|
||||||
|
f"Local destination '{requested_label}' is not configured. Use -plugin local -instance <name|path>."
|
||||||
|
)
|
||||||
|
|
||||||
|
destination_root = Path(destination_text).expanduser()
|
||||||
|
create_dirs = bool(settings.get("create_dirs", True))
|
||||||
|
if create_dirs:
|
||||||
|
destination_root.mkdir(parents=True, exist_ok=True)
|
||||||
|
elif not destination_root.exists():
|
||||||
|
raise FileNotFoundError(f"Destination directory does not exist: {destination_root}")
|
||||||
|
elif not destination_root.is_dir():
|
||||||
|
raise NotADirectoryError(f"Destination is not a directory: {destination_root}")
|
||||||
|
|
||||||
|
title = str(kwargs.get("title") or "").strip()
|
||||||
|
if not title:
|
||||||
|
title = source_path.stem.replace("_", " ").strip()
|
||||||
|
base_name = sanitize_filename(title or source_path.stem)
|
||||||
|
|
||||||
|
file_ext = source_path.suffix
|
||||||
|
if file_ext and base_name.lower().endswith(file_ext.lower()):
|
||||||
|
target_name = base_name
|
||||||
|
else:
|
||||||
|
target_name = base_name + file_ext
|
||||||
|
|
||||||
|
direct_export_download = bool(kwargs.get("direct_export_download", False))
|
||||||
|
target_path = source_path if direct_export_download else destination_root / target_name
|
||||||
|
|
||||||
|
if not direct_export_download:
|
||||||
|
if target_path.exists():
|
||||||
|
target_path = unique_path(target_path)
|
||||||
|
shutil.copy2(str(source_path), target_path)
|
||||||
|
_copy_sidecars(source_path, target_path)
|
||||||
|
|
||||||
|
tags = list(kwargs.get("tags") or [])
|
||||||
|
urls = list(kwargs.get("urls") or [])
|
||||||
|
hash_value = str(kwargs.get("hash_value") or "").strip() or None
|
||||||
|
if not hash_value:
|
||||||
|
try:
|
||||||
|
hash_value = sha256_file(target_path)
|
||||||
|
except Exception:
|
||||||
|
hash_value = None
|
||||||
|
|
||||||
|
relationships = kwargs.get("relationships")
|
||||||
|
try:
|
||||||
|
write_tags(target_path, tags, urls, hash_value=hash_value)
|
||||||
|
write_metadata(
|
||||||
|
target_path,
|
||||||
|
hash_value=hash_value,
|
||||||
|
url=urls,
|
||||||
|
relationships=relationships or [],
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
extra_updates: Dict[str, Any] = {
|
||||||
|
"url": urls,
|
||||||
|
"export_path": str(destination_root),
|
||||||
|
}
|
||||||
|
if resolved_name:
|
||||||
|
extra_updates["instance"] = resolved_name
|
||||||
|
if relationships:
|
||||||
|
extra_updates["relationships"] = relationships
|
||||||
|
|
||||||
|
return {
|
||||||
|
"hash": hash_value or "unknown",
|
||||||
|
"store": "local",
|
||||||
|
"provider": self.name,
|
||||||
|
"path": str(target_path),
|
||||||
|
"tag": tags,
|
||||||
|
"title": title or target_path.name,
|
||||||
|
"relationships": relationships,
|
||||||
|
"extra": extra_updates,
|
||||||
|
}
|
||||||
@@ -5903,7 +5903,7 @@ mp.register_script_message('medios-download-pick-path', function()
|
|||||||
|
|
||||||
local pipeline_cmd = 'file -download -url ' .. quote_pipeline_arg(url)
|
local pipeline_cmd = 'file -download -url ' .. quote_pipeline_arg(url)
|
||||||
.. ' -query ' .. quote_pipeline_arg(query)
|
.. ' -query ' .. quote_pipeline_arg(query)
|
||||||
.. ' | file -add -path ' .. quote_pipeline_arg(folder)
|
.. ' | file -add -plugin local -instance ' .. quote_pipeline_arg(folder)
|
||||||
|
|
||||||
_queue_pipeline_in_repl(
|
_queue_pipeline_in_repl(
|
||||||
pipeline_cmd,
|
pipeline_cmd,
|
||||||
|
|||||||
@@ -474,7 +474,7 @@ class MPV:
|
|||||||
if store:
|
if store:
|
||||||
pipeline += f" | file -add -instance {_q(store)}"
|
pipeline += f" | file -add -instance {_q(store)}"
|
||||||
else:
|
else:
|
||||||
pipeline += f" | file -add -path {_q(path or '')}"
|
pipeline += f" | file -add -plugin local -instance {_q(path or '')}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from TUI.pipeline_runner import PipelineRunner # noqa: WPS433
|
from TUI.pipeline_runner import PipelineRunner # noqa: WPS433
|
||||||
|
|||||||
Reference in New Issue
Block a user