refactored and updated tags cmdlet and hydrusnetwork interaction plugin features
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
from plugins.mpv import MPV
|
|
||||||
|
|
||||||
__all__ = ["MPV"]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from plugins.mpv.format_probe import *
|
|
||||||
from plugins.mpv.format_probe import main as _main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(_main())
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from plugins.mpv.lyric import *
|
|
||||||
from plugins.mpv.lyric import main as _main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(_main())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
from plugins.mpv.mpv_ipc import *
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from plugins.mpv.pipeline_helper import *
|
|
||||||
from plugins.mpv.pipeline_helper import main as _main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(_main())
|
|
||||||
@@ -72,12 +72,16 @@ def prepare_detail_metadata(
|
|||||||
from SYS.result_table import extract_item_metadata
|
from SYS.result_table import extract_item_metadata
|
||||||
|
|
||||||
metadata = extract_item_metadata(subject) or {}
|
metadata = extract_item_metadata(subject) or {}
|
||||||
|
if "Store" in metadata and "Instance" not in metadata:
|
||||||
|
metadata["Instance"] = metadata.pop("Store")
|
||||||
|
|
||||||
if include_subject_fields and isinstance(subject, dict):
|
if include_subject_fields and isinstance(subject, dict):
|
||||||
for key, value in subject.items():
|
for key, value in subject.items():
|
||||||
if str(key).startswith("_") or key in {"selection_action", "selection_args"}:
|
if str(key).startswith("_") or key in {"selection_action", "selection_args"}:
|
||||||
continue
|
continue
|
||||||
label = _labelize_key(str(key))
|
label = _labelize_key(str(key))
|
||||||
|
if label == "Store":
|
||||||
|
label = "Instance"
|
||||||
if label not in metadata and _has_display_value(value):
|
if label not in metadata and _has_display_value(value):
|
||||||
metadata[label] = value
|
metadata[label] = value
|
||||||
|
|
||||||
@@ -86,7 +90,7 @@ def prepare_detail_metadata(
|
|||||||
if hash_value:
|
if hash_value:
|
||||||
metadata["Hash"] = hash_value
|
metadata["Hash"] = hash_value
|
||||||
if store:
|
if store:
|
||||||
metadata["Store"] = store
|
metadata["Instance"] = store
|
||||||
if path:
|
if path:
|
||||||
metadata["Path"] = path
|
metadata["Path"] = path
|
||||||
|
|
||||||
@@ -122,7 +126,14 @@ def create_detail_view(
|
|||||||
if exclude_tags:
|
if exclude_tags:
|
||||||
kwargs["exclude_tags"] = True
|
kwargs["exclude_tags"] = True
|
||||||
if detail_order is not None:
|
if detail_order is not None:
|
||||||
kwargs["detail_order"] = list(detail_order)
|
normalized_order: list[str] = []
|
||||||
|
for key in detail_order:
|
||||||
|
text = str(key)
|
||||||
|
if text.lower() == "store":
|
||||||
|
normalized_order.append("Instance")
|
||||||
|
else:
|
||||||
|
normalized_order.append(text)
|
||||||
|
kwargs["detail_order"] = normalized_order
|
||||||
|
|
||||||
table = ItemDetailView(title, **kwargs)
|
table = ItemDetailView(title, **kwargs)
|
||||||
if table_name:
|
if table_name:
|
||||||
|
|||||||
+15
-11
@@ -456,13 +456,14 @@ def list_configured_backend_names(config: Optional[Dict[str, Any]]) -> list[str]
|
|||||||
(case-insensitive) when present, otherwise the instance key.
|
(case-insensitive) when present, otherwise the instance key.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
store_cfg = (config or {}).get("store") or {}
|
|
||||||
if not isinstance(store_cfg, dict):
|
|
||||||
return []
|
|
||||||
|
|
||||||
classes_by_type = _discover_store_classes()
|
classes_by_type = _discover_store_classes()
|
||||||
names: list[str] = []
|
names: list[str] = []
|
||||||
for raw_store_type, instances in store_cfg.items():
|
for section_name in ("store", "plugin", "provider"):
|
||||||
|
section_cfg = (config or {}).get(section_name) or {}
|
||||||
|
if not isinstance(section_cfg, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for raw_store_type, instances in section_cfg.items():
|
||||||
if not isinstance(instances, dict):
|
if not isinstance(instances, dict):
|
||||||
continue
|
continue
|
||||||
store_type = _normalize_store_type(str(raw_store_type))
|
store_type = _normalize_store_type(str(raw_store_type))
|
||||||
@@ -503,13 +504,15 @@ def get_backend_instance(config: Optional[Dict[str, Any]], backend_name: str, *,
|
|||||||
"""
|
"""
|
||||||
if not backend_name:
|
if not backend_name:
|
||||||
return None
|
return None
|
||||||
store_cfg = (config or {}).get("store") or {}
|
|
||||||
if not isinstance(store_cfg, dict):
|
|
||||||
return None
|
|
||||||
classes_by_type = _discover_store_classes()
|
classes_by_type = _discover_store_classes()
|
||||||
desired = str(backend_name or "").strip().lower()
|
desired = str(backend_name or "").strip().lower()
|
||||||
|
|
||||||
for raw_store_type, instances in store_cfg.items():
|
for section_name in ("store", "plugin", "provider"):
|
||||||
|
section_cfg = (config or {}).get(section_name) or {}
|
||||||
|
if not isinstance(section_cfg, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for raw_store_type, instances in section_cfg.items():
|
||||||
if not isinstance(instances, dict):
|
if not isinstance(instances, dict):
|
||||||
continue
|
continue
|
||||||
store_type = _normalize_store_type(str(raw_store_type))
|
store_type = _normalize_store_type(str(raw_store_type))
|
||||||
@@ -525,12 +528,13 @@ def get_backend_instance(config: Optional[Dict[str, Any]], backend_name: str, *,
|
|||||||
instance_cfg.get("NAME") or instance_cfg.get("name")
|
instance_cfg.get("NAME") or instance_cfg.get("name")
|
||||||
)
|
)
|
||||||
candidate_alias = str(candidate_alias or instance_name).strip()
|
candidate_alias = str(candidate_alias or instance_name).strip()
|
||||||
if candidate_alias.lower() == desired:
|
if candidate_alias.lower() != desired:
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg)
|
kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if not suppress_debug:
|
if not suppress_debug:
|
||||||
debug(f"[Store] Can't build kwargs for '{instance_name}' ({store_type}): {exc}")
|
debug(f"[Store] Can't build kwargs for '{instance_name}' ({store_type}/{section_name}): {exc}")
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
for key in list(kwargs.keys()):
|
for key in list(kwargs.keys()):
|
||||||
|
|||||||
+87
-12
@@ -13,6 +13,7 @@ from SYS.result_publication import publish_result_table
|
|||||||
from SYS import models
|
from SYS import models
|
||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from . import _shared as sh
|
from . import _shared as sh
|
||||||
|
from Store import Store # retained for test monkeypatch compatibility
|
||||||
|
|
||||||
normalize_result_input = sh.normalize_result_input
|
normalize_result_input = sh.normalize_result_input
|
||||||
filter_results_by_temp = sh.filter_results_by_temp
|
filter_results_by_temp = sh.filter_results_by_temp
|
||||||
@@ -28,7 +29,6 @@ parse_cmdlet_args = sh.parse_cmdlet_args
|
|||||||
collapse_namespace_tag = sh.collapse_namespace_tag
|
collapse_namespace_tag = sh.collapse_namespace_tag
|
||||||
should_show_help = sh.should_show_help
|
should_show_help = sh.should_show_help
|
||||||
get_field = sh.get_field
|
get_field = sh.get_field
|
||||||
from Store import Store
|
|
||||||
|
|
||||||
_FIELD_NAME_RE = re.compile(r"^[A-Za-z0-9_]+$")
|
_FIELD_NAME_RE = re.compile(r"^[A-Za-z0-9_]+$")
|
||||||
|
|
||||||
@@ -663,8 +663,40 @@ class Add_Tag(Cmdlet):
|
|||||||
total_added = 0
|
total_added = 0
|
||||||
total_modified = 0
|
total_modified = 0
|
||||||
unresolved_template_count = 0
|
unresolved_template_count = 0
|
||||||
|
store_registry: Any = None
|
||||||
|
|
||||||
store_registry = Store(config, suppress_debug=True)
|
def _resolve_backend(name: Optional[str]) -> tuple[Any | None, Any, Exception | None]:
|
||||||
|
nonlocal store_registry
|
||||||
|
backend_name = str(name or "").strip()
|
||||||
|
if not backend_name:
|
||||||
|
return None, store_registry, KeyError("Missing store name")
|
||||||
|
if backend_name in _backend_instance_cache:
|
||||||
|
return _backend_instance_cache[backend_name], store_registry, None
|
||||||
|
try:
|
||||||
|
backend, registry, exc = sh.get_preferred_store_backend(
|
||||||
|
config,
|
||||||
|
backend_name,
|
||||||
|
store_registry=store_registry,
|
||||||
|
suppress_debug=True,
|
||||||
|
)
|
||||||
|
except TypeError as exc2:
|
||||||
|
# Tests may monkeypatch get_store_backend with a reduced signature.
|
||||||
|
if "store_registry" in str(exc2):
|
||||||
|
backend, registry, exc = sh.get_store_backend(
|
||||||
|
config,
|
||||||
|
backend_name,
|
||||||
|
suppress_debug=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
if registry is not None:
|
||||||
|
store_registry = registry
|
||||||
|
if backend is not None:
|
||||||
|
_backend_instance_cache[backend_name] = backend
|
||||||
|
return backend, store_registry, exc
|
||||||
|
|
||||||
|
pending_bulk_add: Dict[tuple[int, tuple[str, ...], tuple[str, ...]], Dict[str, Any]] = {}
|
||||||
|
_backend_instance_cache: Dict[str, Any] = {}
|
||||||
|
|
||||||
extract_matched_items = 0
|
extract_matched_items = 0
|
||||||
extract_no_match_items = 0
|
extract_no_match_items = 0
|
||||||
@@ -697,9 +729,8 @@ class Add_Tag(Cmdlet):
|
|||||||
|
|
||||||
is_known_backend = False
|
is_known_backend = False
|
||||||
try:
|
try:
|
||||||
is_known_backend = bool(store_name_str) and store_registry.is_available(
|
backend_probe, store_registry, _probe_exc = _resolve_backend(store_name_str)
|
||||||
store_name_str
|
is_known_backend = backend_probe is not None
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -864,7 +895,7 @@ class Add_Tag(Cmdlet):
|
|||||||
# If it's not a known backend and we didn't handle it above as a local/pipeline
|
# If it's not a known backend and we didn't handle it above as a local/pipeline
|
||||||
# metadata edit, then it's an error.
|
# metadata edit, then it's an error.
|
||||||
log(
|
log(
|
||||||
f"[add_tag] Error: Unknown store '{store_name_str}'. Available: {store_registry.list_backends()}",
|
f"[add_tag] Error: Unknown store '{store_name_str}'",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
@@ -883,12 +914,7 @@ class Add_Tag(Cmdlet):
|
|||||||
ctx.emit(res)
|
ctx.emit(res)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
backend, store_registry, exc = sh.get_store_backend(
|
backend, store_registry, exc = _resolve_backend(str(store_name))
|
||||||
config,
|
|
||||||
str(store_name),
|
|
||||||
store_registry=store_registry,
|
|
||||||
suppress_debug=True,
|
|
||||||
)
|
|
||||||
if backend is None:
|
if backend is None:
|
||||||
log(
|
log(
|
||||||
f"[add_tag] Error: Unknown store '{store_name}': {exc}",
|
f"[add_tag] Error: Unknown store '{store_name}': {exc}",
|
||||||
@@ -1027,6 +1053,28 @@ class Add_Tag(Cmdlet):
|
|||||||
tags_to_add = []
|
tags_to_add = []
|
||||||
merged_tags = list(existing_tag_list)
|
merged_tags = list(existing_tag_list)
|
||||||
|
|
||||||
|
queued_bulk = False
|
||||||
|
ok_add = False
|
||||||
|
add_tags_bulk_fn = getattr(backend, "add_tags_bulk", None)
|
||||||
|
if tags_to_add and callable(add_tags_bulk_fn):
|
||||||
|
add_key = tuple(sorted({str(t).strip().lower() for t in tags_to_add if str(t).strip()}))
|
||||||
|
remove_key = tuple(sorted({str(t).strip().lower() for t in tags_to_remove if str(t).strip()}))
|
||||||
|
if add_key:
|
||||||
|
batch_key = (id(backend), add_key, remove_key)
|
||||||
|
bucket = pending_bulk_add.get(batch_key)
|
||||||
|
if bucket is None:
|
||||||
|
bucket = {
|
||||||
|
"backend": backend,
|
||||||
|
"add_tags": list(add_key),
|
||||||
|
"remove_tags": list(remove_key),
|
||||||
|
"hashes": [],
|
||||||
|
}
|
||||||
|
pending_bulk_add[batch_key] = bucket
|
||||||
|
bucket["hashes"].append(resolved_hash)
|
||||||
|
queued_bulk = True
|
||||||
|
ok_add = True
|
||||||
|
|
||||||
|
if not queued_bulk:
|
||||||
try:
|
try:
|
||||||
ok_add = backend.add_tag(
|
ok_add = backend.add_tag(
|
||||||
resolved_hash,
|
resolved_hash,
|
||||||
@@ -1075,6 +1123,33 @@ class Add_Tag(Cmdlet):
|
|||||||
|
|
||||||
ctx.emit(res)
|
ctx.emit(res)
|
||||||
|
|
||||||
|
for bucket in pending_bulk_add.values():
|
||||||
|
backend = bucket.get("backend")
|
||||||
|
add_tags_for_batch = list(bucket.get("add_tags") or [])
|
||||||
|
remove_tags_for_batch = list(bucket.get("remove_tags") or [])
|
||||||
|
hashes_for_batch = [str(h).strip().lower() for h in (bucket.get("hashes") or []) if str(h).strip()]
|
||||||
|
if backend is None or not hashes_for_batch:
|
||||||
|
continue
|
||||||
|
|
||||||
|
batch_items = [(h, list(add_tags_for_batch), list(remove_tags_for_batch)) for h in hashes_for_batch]
|
||||||
|
add_tags_bulk_fn = getattr(backend, "add_tags_bulk", None)
|
||||||
|
applied = False
|
||||||
|
if callable(add_tags_bulk_fn):
|
||||||
|
try:
|
||||||
|
applied = bool(add_tags_bulk_fn(batch_items))
|
||||||
|
except Exception:
|
||||||
|
applied = False
|
||||||
|
|
||||||
|
if applied:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fallback path: retain correctness if backend bulk call fails.
|
||||||
|
for h in hashes_for_batch:
|
||||||
|
try:
|
||||||
|
backend.add_tag(h, list(add_tags_for_batch), config=config)
|
||||||
|
except Exception as exc:
|
||||||
|
log(f"[add_tag] Warning: Failed fallback add_tag for {h}: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
log(
|
log(
|
||||||
f"[add_tag] Added {total_added} new tag(s) across {len(results)} item(s); modified {total_modified} item(s)",
|
f"[add_tag] Added {total_added} new tag(s) across {len(results)} item(s); modified {total_modified} item(s)",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
|
|||||||
+110
-29
@@ -551,21 +551,44 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
log("Requires at least one tag argument when deleting from files")
|
log("Requires at least one tag argument when deleting from files")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Process each item
|
# Collect (store_name, tags_key) -> {backend, hashes, items} groups for bulk dispatch.
|
||||||
|
# Items that need per-item existing-tag resolution (e.g. namespace-wildcard expand)
|
||||||
|
# are handled individually; static literal tag sets are batched.
|
||||||
|
_backend_cache: Dict[str, Any] = {}
|
||||||
|
|
||||||
# If we have tags from @ syntax (e.g. delete-tag @{1,2}), we ignore the piped result for tag selection
|
def _get_backend(store_name_str: str) -> Any | None:
|
||||||
# but we might need the piped result for the file context if @ selection was from a Tag table
|
if store_name_str in _backend_cache:
|
||||||
# Actually, the @ selection logic above already extracted tags.
|
return _backend_cache[store_name_str]
|
||||||
|
try:
|
||||||
|
backend, _reg, _exc = sh.get_preferred_store_backend(
|
||||||
|
config, store_name_str, suppress_debug=True
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
backend, _reg, _exc = sh.get_store_backend(
|
||||||
|
config, store_name_str, suppress_debug=True
|
||||||
|
)
|
||||||
|
if backend is not None:
|
||||||
|
_backend_cache[store_name_str] = backend
|
||||||
|
return backend
|
||||||
|
|
||||||
|
# Bucket: key = (store_name, sorted_tag_tuple) → list of (hash, item, path)
|
||||||
|
bulk_groups: Dict[tuple[str, tuple[str, ...]], list[tuple[str, Any, str | None]]] = {}
|
||||||
|
items_needing_individual: list[tuple[Any, str, str | None, str]] = []
|
||||||
|
|
||||||
|
tags_has_namespace_wildcard = any(
|
||||||
|
(isinstance(t, str) and ":" in t and not t.split(":", 1)[1].strip())
|
||||||
|
for t in tags_arg
|
||||||
|
)
|
||||||
|
tags_has_template = any(
|
||||||
|
(isinstance(t, str) and "#(" in t)
|
||||||
|
for t in tags_arg
|
||||||
|
)
|
||||||
|
needs_individual = tags_has_namespace_wildcard or tags_has_template
|
||||||
|
|
||||||
# Process items from pipe (or single result)
|
|
||||||
# If args are provided, they are the tags to delete from EACH item
|
|
||||||
# If items are TagItems and no args, the tag to delete is the item itself
|
|
||||||
for item in items_to_process:
|
for item in items_to_process:
|
||||||
tags_to_delete: list[str] = []
|
|
||||||
item_hash = (
|
item_hash = (
|
||||||
normalize_hash(override_hash)
|
normalize_hash(override_hash)
|
||||||
if override_hash else normalize_hash(get_field(item,
|
if override_hash else normalize_hash(get_field(item, "hash"))
|
||||||
"hash"))
|
|
||||||
)
|
)
|
||||||
item_path = get_field(item, "path") or get_field(item, "target")
|
item_path = get_field(item, "path") or get_field(item, "target")
|
||||||
item_store = override_store or get_field(item, "store")
|
item_store = override_store or get_field(item, "store")
|
||||||
@@ -575,21 +598,68 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
|||||||
tags_to_delete = tags_arg
|
tags_to_delete = tags_arg
|
||||||
else:
|
else:
|
||||||
tag_name = get_field(item, "tag_name")
|
tag_name = get_field(item, "tag_name")
|
||||||
if tag_name:
|
tags_to_delete = [str(tag_name)] if tag_name else []
|
||||||
tags_to_delete = [str(tag_name)]
|
|
||||||
else:
|
|
||||||
if tags_arg:
|
|
||||||
tags_to_delete = tags_arg
|
|
||||||
else:
|
else:
|
||||||
|
tags_to_delete = tags_arg or []
|
||||||
|
|
||||||
|
if not tags_to_delete or not item_hash or not item_store:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if tags_to_delete:
|
store_str = str(item_store)
|
||||||
if _process_deletion(tags_to_delete,
|
|
||||||
item_hash,
|
# Namespace wildcards (e.g. "album:") and template tags (e.g. "title:#(track)")
|
||||||
item_path,
|
# need existing tags to expand — handle individually.
|
||||||
item_store,
|
if needs_individual:
|
||||||
config,
|
items_needing_individual.append((item, item_hash, item_path, store_str))
|
||||||
result=item):
|
continue
|
||||||
|
|
||||||
|
tag_key = tuple(sorted(str(t).strip().lower() for t in tags_to_delete if str(t).strip()))
|
||||||
|
bulk_groups.setdefault((store_str, tag_key), []).append((item_hash, item, item_path))
|
||||||
|
|
||||||
|
# --- Bulk dispatch ---
|
||||||
|
for (store_str, tag_key), entries in bulk_groups.items():
|
||||||
|
backend = _get_backend(store_str)
|
||||||
|
if backend is None:
|
||||||
|
log(f"Store '{store_str}' not found", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
|
||||||
|
hashes = [h for h, _item, _path in entries]
|
||||||
|
tag_list = list(tag_key)
|
||||||
|
bulk_fn = getattr(backend, "delete_tags_bulk", None)
|
||||||
|
bulk_ok = False
|
||||||
|
if callable(bulk_fn):
|
||||||
|
try:
|
||||||
|
bulk_ok = bool(bulk_fn([(h, tag_list) for h in hashes]))
|
||||||
|
except Exception:
|
||||||
|
bulk_ok = False
|
||||||
|
|
||||||
|
if not bulk_ok:
|
||||||
|
# fallback: individual delete_tag per hash
|
||||||
|
for h in hashes:
|
||||||
|
try:
|
||||||
|
backend.delete_tag(h, tag_list, config=config)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
success_count += 1
|
||||||
|
delete_set = {t.lower() for t in tag_key}
|
||||||
|
for h, item, path in entries:
|
||||||
|
# Update in-memory tag list on each result
|
||||||
|
old_tags = [str(t) for t in (get_field(item, "tag") or []) if t]
|
||||||
|
new_tags = [t for t in old_tags if t.strip().casefold() not in delete_set]
|
||||||
|
_set_result_tags(item, new_tags)
|
||||||
|
title_value = extract_title_tag_value(new_tags)
|
||||||
|
if title_value:
|
||||||
|
_apply_title_to_result(item, title_value)
|
||||||
|
_refresh_result_table_tags(new_tags, h, store_str, path)
|
||||||
|
try:
|
||||||
|
ctx.emit(item)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# --- Individual dispatch (namespace wildcards) ---
|
||||||
|
for item, item_hash, item_path, store_str in items_needing_individual:
|
||||||
|
if _process_deletion(tags_arg, item_hash, item_path, store_str, config, result=item):
|
||||||
success_count += 1
|
success_count += 1
|
||||||
try:
|
try:
|
||||||
ctx.emit(item)
|
ctx.emit(item)
|
||||||
@@ -631,13 +701,28 @@ def _process_deletion(
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _fetch_existing_tags() -> list[str]:
|
def _resolve_backend() -> tuple[Any | None, Any, Exception | None]:
|
||||||
try:
|
try:
|
||||||
backend, _store_registry, _exc = sh.get_store_backend(
|
return sh.get_preferred_store_backend(
|
||||||
config,
|
config,
|
||||||
store_name,
|
store_name,
|
||||||
suppress_debug=True,
|
suppress_debug=True,
|
||||||
)
|
)
|
||||||
|
except TypeError as exc:
|
||||||
|
# Some tests monkeypatch get_store_backend with a reduced signature.
|
||||||
|
# Fall back so runtime still prefers plugin instance resolution while
|
||||||
|
# preserving compatibility with those injected callables.
|
||||||
|
if "store_registry" in str(exc):
|
||||||
|
return sh.get_store_backend(
|
||||||
|
config,
|
||||||
|
store_name,
|
||||||
|
suppress_debug=True,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _fetch_existing_tags() -> list[str]:
|
||||||
|
try:
|
||||||
|
backend, _store_registry, _exc = _resolve_backend()
|
||||||
if backend is None:
|
if backend is None:
|
||||||
return []
|
return []
|
||||||
existing, _src = backend.get_tag(resolved_hash, config=config)
|
existing, _src = backend.get_tag(resolved_hash, config=config)
|
||||||
@@ -687,11 +772,7 @@ def _process_deletion(
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
backend, _store_registry, exc = sh.get_store_backend(
|
backend, _store_registry, exc = _resolve_backend()
|
||||||
config,
|
|
||||||
store_name,
|
|
||||||
suppress_debug=True,
|
|
||||||
)
|
|
||||||
if backend is None:
|
if backend is None:
|
||||||
raise exc or KeyError(store_name)
|
raise exc or KeyError(store_name)
|
||||||
ok = backend.delete_tag(resolved_hash, list(tags), config=config)
|
ok = backend.delete_tag(resolved_hash, list(tags), config=config)
|
||||||
|
|||||||
+75
-21
@@ -18,7 +18,7 @@ from urllib.request import pathname2url
|
|||||||
from SYS import pipeline as ctx
|
from SYS import pipeline as ctx
|
||||||
from . import _shared as sh
|
from . import _shared as sh
|
||||||
from SYS.item_accessors import get_result_title
|
from SYS.item_accessors import get_result_title
|
||||||
from SYS.logger import log, debug
|
from SYS.logger import log, debug, debug_panel
|
||||||
from SYS.config import resolve_output_dir
|
from SYS.config import resolve_output_dir
|
||||||
from API.HTTP import _download_direct_file
|
from API.HTTP import _download_direct_file
|
||||||
from SYS.payload_builders import build_file_result_payload
|
from SYS.payload_builders import build_file_result_payload
|
||||||
@@ -53,9 +53,18 @@ class Get_File(sh.Cmdlet):
|
|||||||
|
|
||||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||||
"""Export file via hash+store backend."""
|
"""Export file via hash+store backend."""
|
||||||
debug(f"[get-file] run() called with result type: {type(result)}")
|
|
||||||
parsed = sh.parse_cmdlet_args(args, self)
|
parsed = sh.parse_cmdlet_args(args, self)
|
||||||
debug(f"[get-file] parsed args: {parsed}")
|
try:
|
||||||
|
debug_panel(
|
||||||
|
"get-file",
|
||||||
|
[
|
||||||
|
("result_type", type(result).__name__),
|
||||||
|
("parsed_args", parsed),
|
||||||
|
],
|
||||||
|
border_style="cyan",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
query_hash, query_valid = sh.require_single_hash_query(
|
query_hash, query_valid = sh.require_single_hash_query(
|
||||||
parsed.get("query"),
|
parsed.get("query"),
|
||||||
@@ -70,8 +79,6 @@ class Get_File(sh.Cmdlet):
|
|||||||
output_path = parsed.get("path")
|
output_path = parsed.get("path")
|
||||||
output_name = parsed.get("name")
|
output_name = parsed.get("name")
|
||||||
|
|
||||||
debug(f"[get-file] file_hash={file_hash} store_name={store_name}")
|
|
||||||
|
|
||||||
if not file_hash:
|
if not file_hash:
|
||||||
log(
|
log(
|
||||||
'Error: No file hash provided (pipe an item or use -query "hash:<sha256>")'
|
'Error: No file hash provided (pipe an item or use -query "hash:<sha256>")'
|
||||||
@@ -88,7 +95,19 @@ class Get_File(sh.Cmdlet):
|
|||||||
log("Error: Invalid hash format")
|
log("Error: Invalid hash format")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
debug(f"[get-file] Getting storage backend: {store_name}")
|
try:
|
||||||
|
debug_panel(
|
||||||
|
"get-file selection",
|
||||||
|
[
|
||||||
|
("hash", file_hash),
|
||||||
|
("instance", store_name),
|
||||||
|
("output_path", output_path or "<default>"),
|
||||||
|
("output_name", output_name or "<auto>"),
|
||||||
|
],
|
||||||
|
border_style="blue",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
backend, _store_registry, _exc = sh.get_preferred_store_backend(
|
backend, _store_registry, _exc = sh.get_preferred_store_backend(
|
||||||
config,
|
config,
|
||||||
@@ -99,17 +118,23 @@ class Get_File(sh.Cmdlet):
|
|||||||
log(f"Error: Storage backend '{store_name}' not found", file=sys.stderr)
|
log(f"Error: Storage backend '{store_name}' not found", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
debug(f"[get-file] Backend retrieved: {type(backend).__name__}")
|
|
||||||
|
|
||||||
# Get file metadata to determine name and extension
|
# Get file metadata to determine name and extension
|
||||||
debug("[get-file] Getting metadata for hash...")
|
|
||||||
metadata = backend.get_metadata(file_hash)
|
metadata = backend.get_metadata(file_hash)
|
||||||
if not metadata:
|
if not metadata:
|
||||||
log(f"Error: File metadata not found for hash {file_hash}")
|
log(f"Error: File metadata not found for hash {file_hash}")
|
||||||
return 1
|
return 1
|
||||||
debug(
|
try:
|
||||||
f"[get-file] Metadata retrieved: title={metadata.get('title')}, ext={metadata.get('ext')}"
|
debug_panel(
|
||||||
|
"get-file backend",
|
||||||
|
[
|
||||||
|
("backend", type(backend).__name__),
|
||||||
|
("title", metadata.get("title") or ""),
|
||||||
|
("ext", metadata.get("ext") or ""),
|
||||||
|
],
|
||||||
|
border_style="green",
|
||||||
)
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def resolve_display_title() -> str:
|
def resolve_display_title() -> str:
|
||||||
candidates = [
|
candidates = [
|
||||||
@@ -124,17 +149,12 @@ class Get_File(sh.Cmdlet):
|
|||||||
return text
|
return text
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
debug(f"[get-file] Calling backend.get_file({file_hash})")
|
|
||||||
|
|
||||||
# Get file from backend (may return Path or URL string depending on backend).
|
# Get file from backend (may return Path or URL string depending on backend).
|
||||||
# We pass url=True if no explicit path was provided, which hints the backend
|
# We pass url=True if no explicit path was provided, which hints the backend
|
||||||
# (specifically Hydrus) to return a browser-friendly URL instead of a local path.
|
# (specifically Hydrus) to return a browser-friendly URL instead of a local path.
|
||||||
want_url = (output_path is None)
|
want_url = (output_path is None)
|
||||||
debug(f"[get-file] Requesting file from backend (url_hint={want_url})...")
|
|
||||||
source_path = backend.get_file(file_hash, url=want_url)
|
source_path = backend.get_file(file_hash, url=want_url)
|
||||||
|
|
||||||
debug(f"[get-file] backend.get_file returned: {source_path}")
|
|
||||||
|
|
||||||
download_url = None
|
download_url = None
|
||||||
if isinstance(source_path, str):
|
if isinstance(source_path, str):
|
||||||
if source_path.startswith("http://") or source_path.startswith("https://"):
|
if source_path.startswith("http://") or source_path.startswith("https://"):
|
||||||
@@ -142,6 +162,19 @@ class Get_File(sh.Cmdlet):
|
|||||||
else:
|
else:
|
||||||
source_path = Path(source_path)
|
source_path = Path(source_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
debug_panel(
|
||||||
|
"get-file fetch",
|
||||||
|
[
|
||||||
|
("url_hint", want_url),
|
||||||
|
("mode", "browser-url" if download_url else "local-path"),
|
||||||
|
("source", download_url or source_path or "<missing>"),
|
||||||
|
],
|
||||||
|
border_style="magenta",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if download_url and output_path is None:
|
if download_url and output_path is None:
|
||||||
# Hydrus backend returns a URL; open it only when no output path
|
# Hydrus backend returns a URL; open it only when no output path
|
||||||
try:
|
try:
|
||||||
@@ -149,7 +182,18 @@ class Get_File(sh.Cmdlet):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"Error opening browser: {exc}", file=sys.stderr)
|
log(f"Error opening browser: {exc}", file=sys.stderr)
|
||||||
else:
|
else:
|
||||||
debug(f"Opened in browser: {download_url}", file=sys.stderr)
|
try:
|
||||||
|
debug_panel(
|
||||||
|
"get-file open",
|
||||||
|
[
|
||||||
|
("action", "browser-open"),
|
||||||
|
("url", download_url),
|
||||||
|
],
|
||||||
|
file=sys.stderr,
|
||||||
|
border_style="green",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
ctx.emit(
|
ctx.emit(
|
||||||
build_file_result_payload(
|
build_file_result_payload(
|
||||||
@@ -172,7 +216,6 @@ class Get_File(sh.Cmdlet):
|
|||||||
else:
|
else:
|
||||||
output_dir = resolve_output_dir(config)
|
output_dir = resolve_output_dir(config)
|
||||||
|
|
||||||
debug(f"[get-file] Output dir: {output_dir}")
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Determine output filename (only when exporting)
|
# Determine output filename (only when exporting)
|
||||||
@@ -202,13 +245,25 @@ class Get_File(sh.Cmdlet):
|
|||||||
suggested_filename=filename,
|
suggested_filename=filename,
|
||||||
)
|
)
|
||||||
dest_path = downloaded.path
|
dest_path = downloaded.path
|
||||||
debug(f"[get-file] Downloaded remote file to {dest_path}", file=sys.stderr)
|
|
||||||
else:
|
else:
|
||||||
dest_path = self._unique_path(output_dir / filename)
|
dest_path = self._unique_path(output_dir / filename)
|
||||||
# Copy file to destination
|
# Copy file to destination
|
||||||
debug(f"[get-file] Copying {source_path} -> {dest_path}", file=sys.stderr)
|
|
||||||
shutil.copy2(source_path, dest_path)
|
shutil.copy2(source_path, dest_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
debug_panel(
|
||||||
|
"get-file export",
|
||||||
|
[
|
||||||
|
("mode", "download" if download_url else "copy"),
|
||||||
|
("destination", dest_path),
|
||||||
|
("filename", filename),
|
||||||
|
],
|
||||||
|
file=sys.stderr,
|
||||||
|
border_style="green",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
log(f"Exported: {dest_path}", file=sys.stderr)
|
log(f"Exported: {dest_path}", file=sys.stderr)
|
||||||
|
|
||||||
# Emit result for pipeline
|
# Emit result for pipeline
|
||||||
@@ -221,7 +276,6 @@ class Get_File(sh.Cmdlet):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
debug("[get-file] Completed successfully")
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _open_file_default(self, path: Path) -> None:
|
def _open_file_default(self, path: Path) -> None:
|
||||||
|
|||||||
@@ -1844,7 +1844,7 @@ class AllDebrid(TableProviderMixin, Provider):
|
|||||||
title=f"AllDebrid Item: {title}",
|
title=f"AllDebrid Item: {title}",
|
||||||
metadata=detail_metadata,
|
metadata=detail_metadata,
|
||||||
table_name=self.name,
|
table_name=self.name,
|
||||||
detail_order=["Title", "Store", "Magnet", "Magnet ID", "Relative Path", "View", "Path", "File", "Folder", "ID", "Direct URL", "Selection URL", "Plugin"],
|
detail_order=["Title", "Instance", "Magnet", "Magnet ID", "Relative Path", "View", "Path", "File", "Folder", "ID", "Direct URL", "Selection URL", "Plugin"],
|
||||||
value_case="preserve",
|
value_case="preserve",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"(rapidgator\\.net/file/[0-9]{7,8})"
|
"(rapidgator\\.net/file/[0-9]{7,8})"
|
||||||
],
|
],
|
||||||
"regexp": "((rapidgator\\.net|rg\\.to|rapidgator\\.asia)/file/([0-9a-zA-Z]{32}))|((rapidgator\\.net/file/[0-9]{7,8}))",
|
"regexp": "((rapidgator\\.net|rg\\.to|rapidgator\\.asia)/file/([0-9a-zA-Z]{32}))|((rapidgator\\.net/file/[0-9]{7,8}))",
|
||||||
"status": false
|
"status": true
|
||||||
},
|
},
|
||||||
"turbobit": {
|
"turbobit": {
|
||||||
"name": "turbobit",
|
"name": "turbobit",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"(wayupload\\.com/[a-z0-9]{12}\\.html)"
|
"(wayupload\\.com/[a-z0-9]{12}\\.html)"
|
||||||
],
|
],
|
||||||
"regexp": "(turbobit5?a?\\.(net|cc|com)/([a-z0-9]{12}))|(turbobif\\.(net|cc|com)/([a-z0-9]{12}))|(turb[o]?\\.(to|cc|pw)\\/([a-z0-9]{12}))|(turbobit\\.(net|cc)/download/free/([a-z0-9]{12}))|((trbbt|tourbobit|torbobit|tbit|turbobita|trbt)\\.(net|cc|com|to)/([a-z0-9]{12}))|((turbobit\\.cloud/turbo/[a-z0-9]+))|((wayupload\\.com/[a-z0-9]{12}\\.html))",
|
"regexp": "(turbobit5?a?\\.(net|cc|com)/([a-z0-9]{12}))|(turbobif\\.(net|cc|com)/([a-z0-9]{12}))|(turb[o]?\\.(to|cc|pw)\\/([a-z0-9]{12}))|(turbobit\\.(net|cc)/download/free/([a-z0-9]{12}))|((trbbt|tourbobit|torbobit|tbit|turbobita|trbt)\\.(net|cc|com|to)/([a-z0-9]{12}))|((turbobit\\.cloud/turbo/[a-z0-9]+))|((wayupload\\.com/[a-z0-9]{12}\\.html))",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"hitfile": {
|
"hitfile": {
|
||||||
"name": "hitfile",
|
"name": "hitfile",
|
||||||
@@ -375,7 +375,7 @@
|
|||||||
"(filespace\\.com/[a-zA-Z0-9]{12})"
|
"(filespace\\.com/[a-zA-Z0-9]{12})"
|
||||||
],
|
],
|
||||||
"regexp": "(filespace\\.com/fd/([a-zA-Z0-9]{12}))|((filespace\\.com/[a-zA-Z0-9]{12}))",
|
"regexp": "(filespace\\.com/fd/([a-zA-Z0-9]{12}))|((filespace\\.com/[a-zA-Z0-9]{12}))",
|
||||||
"status": true
|
"status": false
|
||||||
},
|
},
|
||||||
"filezip": {
|
"filezip": {
|
||||||
"name": "filezip",
|
"name": "filezip",
|
||||||
@@ -17869,9 +17869,9 @@
|
|||||||
"dl-protect.best"
|
"dl-protect.best"
|
||||||
],
|
],
|
||||||
"regexps": [
|
"regexps": [
|
||||||
"dl\\-protect\\.(best|info|net|link|cc)/([^/]+)"
|
"dl\\-protect\\.(best|info|net|link|cc)/([^/\"]+)"
|
||||||
],
|
],
|
||||||
"regexp": "dl\\-protect\\.(best|info|net|link|cc)/([^/]+)"
|
"regexp": "dl\\-protect\\.(best|info|net|link|cc)/([^/\"]+)"
|
||||||
},
|
},
|
||||||
"ed-protect": {
|
"ed-protect": {
|
||||||
"name": "ed-protect",
|
"name": "ed-protect",
|
||||||
|
|||||||
@@ -459,7 +459,7 @@ class FTP(Provider):
|
|||||||
title=f"FTP Item: {title}",
|
title=f"FTP Item: {title}",
|
||||||
metadata=detail_metadata,
|
metadata=detail_metadata,
|
||||||
table_name=self.name,
|
table_name=self.name,
|
||||||
detail_order=["Title", "Store", "Host", "Instance", "Remote Path", "Directory", "Modified", "Path", "Ext", "FTP URL", "Plugin"],
|
detail_order=["Title", "Instance", "Host", "Remote Path", "Directory", "Modified", "Path", "Ext", "FTP URL", "Plugin"],
|
||||||
value_case="preserve",
|
value_case="preserve",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""Canonical MPV plugin package."""
|
||||||
|
|
||||||
from plugins.mpv.mpv_ipc import MPV
|
from plugins.mpv.mpv_ipc import MPV
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ def _runtime_config_root() -> Path:
|
|||||||
"""Best-effort config root for runtime execution.
|
"""Best-effort config root for runtime execution.
|
||||||
|
|
||||||
MPV can spawn this helper from an installed location while setting `cwd` to
|
MPV can spawn this helper from an installed location while setting `cwd` to
|
||||||
the repo root (see MPV.mpv_ipc). Prefer `cwd` when it contains `config.conf`.
|
the repo root (see plugins.mpv.mpv_ipc). Prefer `cwd` when it contains `config.conf`.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cwd = Path.cwd().resolve()
|
cwd = Path.cwd().resolve()
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"choices":["local","rpi"]}
|
{"choices":["rpi"]}
|
||||||
@@ -490,7 +490,7 @@ class SCP(Provider):
|
|||||||
title=f"SCP Item: {title}",
|
title=f"SCP Item: {title}",
|
||||||
metadata=detail_metadata,
|
metadata=detail_metadata,
|
||||||
table_name=self.name,
|
table_name=self.name,
|
||||||
detail_order=["Title", "Store", "Host", "Instance", "Remote Path", "Directory", "Modified", "Path", "Ext", "SCP URL", "Plugin"],
|
detail_order=["Title", "Instance", "Host", "Remote Path", "Directory", "Modified", "Path", "Ext", "SCP URL", "Plugin"],
|
||||||
value_case="preserve",
|
value_case="preserve",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user