update local and mpv plugins, add file cmdlet, update docs

This commit is contained in:
2026-05-14 17:15:13 -07:00
parent 9f0eb29289
commit 036977832b
10 changed files with 653 additions and 297 deletions
+171 -13
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+2 -2
View File
@@ -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
+205
View File
@@ -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,
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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