diff --git a/MPV/__init__.py b/MPV/__init__.py deleted file mode 100644 index 7a3fd1d..0000000 --- a/MPV/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from plugins.mpv import MPV - -__all__ = ["MPV"] \ No newline at end of file diff --git a/MPV/format_probe.py b/MPV/format_probe.py deleted file mode 100644 index 804b139..0000000 --- a/MPV/format_probe.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/MPV/lyric.py b/MPV/lyric.py deleted file mode 100644 index d5e83c4..0000000 --- a/MPV/lyric.py +++ /dev/null @@ -1,6 +0,0 @@ -from plugins.mpv.lyric import * -from plugins.mpv.lyric import main as _main - - -if __name__ == "__main__": - raise SystemExit(_main()) diff --git a/MPV/mpv_ipc.py b/MPV/mpv_ipc.py deleted file mode 100644 index 87b3204..0000000 --- a/MPV/mpv_ipc.py +++ /dev/null @@ -1 +0,0 @@ -from plugins.mpv.mpv_ipc import * \ No newline at end of file diff --git a/MPV/pipeline_helper.py b/MPV/pipeline_helper.py deleted file mode 100644 index 678f9d1..0000000 --- a/MPV/pipeline_helper.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/SYS/detail_view_helpers.py b/SYS/detail_view_helpers.py index 5480c64..7052e9f 100644 --- a/SYS/detail_view_helpers.py +++ b/SYS/detail_view_helpers.py @@ -72,12 +72,16 @@ def prepare_detail_metadata( from SYS.result_table import extract_item_metadata 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): for key, value in subject.items(): if str(key).startswith("_") or key in {"selection_action", "selection_args"}: continue label = _labelize_key(str(key)) + if label == "Store": + label = "Instance" if label not in metadata and _has_display_value(value): metadata[label] = value @@ -86,7 +90,7 @@ def prepare_detail_metadata( if hash_value: metadata["Hash"] = hash_value if store: - metadata["Store"] = store + metadata["Instance"] = store if path: metadata["Path"] = path @@ -122,7 +126,14 @@ def create_detail_view( if exclude_tags: kwargs["exclude_tags"] = True 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) if table_name: diff --git a/Store/registry.py b/Store/registry.py index dd119d7..22f1033 100644 --- a/Store/registry.py +++ b/Store/registry.py @@ -456,36 +456,37 @@ def list_configured_backend_names(config: Optional[Dict[str, Any]]) -> list[str] (case-insensitive) when present, otherwise the instance key. """ try: - store_cfg = (config or {}).get("store") or {} - if not isinstance(store_cfg, dict): - return [] - classes_by_type = _discover_store_classes() names: list[str] = [] - for raw_store_type, instances in store_cfg.items(): - if not isinstance(instances, dict): - continue - store_type = _normalize_store_type(str(raw_store_type)) - if store_type == "folder" or store_type in _PROVIDER_ONLY_STORE_NAMES: + for section_name in ("store", "plugin", "provider"): + section_cfg = (config or {}).get(section_name) or {} + if not isinstance(section_cfg, dict): continue - store_cls = _resolve_store_class(store_type, classes_by_type) - if store_cls is None: - continue - - for instance_name, instance_config in instances.items(): - try: - _build_kwargs(store_cls, str(instance_name), instance_config) - except Exception: + for raw_store_type, instances in section_cfg.items(): + if not isinstance(instances, dict): continue - if isinstance(instance_config, dict): - override_name = _get_case_insensitive(dict(instance_config), "NAME") - if override_name: - names.append(str(override_name)) + store_type = _normalize_store_type(str(raw_store_type)) + if store_type == "folder" or store_type in _PROVIDER_ONLY_STORE_NAMES: + continue + + store_cls = _resolve_store_class(store_type, classes_by_type) + if store_cls is None: + continue + + for instance_name, instance_config in instances.items(): + try: + _build_kwargs(store_cls, str(instance_name), instance_config) + except Exception: + continue + if isinstance(instance_config, dict): + override_name = _get_case_insensitive(dict(instance_config), "NAME") + if override_name: + names.append(str(override_name)) + else: + names.append(str(instance_name)) else: names.append(str(instance_name)) - else: - names.append(str(instance_name)) return sorted(set(names)) except Exception: @@ -503,34 +504,37 @@ def get_backend_instance(config: Optional[Dict[str, Any]], backend_name: str, *, """ if not backend_name: return None - store_cfg = (config or {}).get("store") or {} - if not isinstance(store_cfg, dict): - return None classes_by_type = _discover_store_classes() desired = str(backend_name or "").strip().lower() - for raw_store_type, instances in store_cfg.items(): - if not isinstance(instances, dict): - continue - store_type = _normalize_store_type(str(raw_store_type)) - store_cls = _resolve_store_class(store_type, classes_by_type) - if store_cls is None: + for section_name in ("store", "plugin", "provider"): + section_cfg = (config or {}).get(section_name) or {} + if not isinstance(section_cfg, dict): continue - # Fast path: match using raw 'NAME' or 'name' in config without building full kwargs - for instance_name, instance_cfg in instances.items(): - candidate_alias = None - if isinstance(instance_cfg, dict): - candidate_alias = ( - instance_cfg.get("NAME") or instance_cfg.get("name") - ) - candidate_alias = str(candidate_alias or instance_name).strip() - if candidate_alias.lower() == desired: + for raw_store_type, instances in section_cfg.items(): + if not isinstance(instances, dict): + continue + store_type = _normalize_store_type(str(raw_store_type)) + store_cls = _resolve_store_class(store_type, classes_by_type) + if store_cls is None: + continue + + # Fast path: match using raw 'NAME' or 'name' in config without building full kwargs + for instance_name, instance_cfg in instances.items(): + candidate_alias = None + if isinstance(instance_cfg, dict): + candidate_alias = ( + instance_cfg.get("NAME") or instance_cfg.get("name") + ) + candidate_alias = str(candidate_alias or instance_name).strip() + if candidate_alias.lower() != desired: + continue try: kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg) except Exception as exc: 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 try: for key in list(kwargs.keys()): @@ -546,28 +550,28 @@ def get_backend_instance(config: Optional[Dict[str, Any]], backend_name: str, *, debug(f"[Store] Failed to instantiate backend '{candidate_alias}': {exc}") return None - # Fallback: build kwargs for each instance and compare resolved NAME - for instance_name, instance_cfg in instances.items(): - try: - kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg) - except Exception: - continue - alias = str(kwargs.get("NAME") or instance_name).strip() - if alias.lower() != desired: - continue - try: - for key in list(kwargs.keys()): - if _normalize_config_key(key) in {"PATH", "LOCATION"}: - kwargs[key] = str(expand_path(kwargs[key])) - except Exception: - pass - try: - backend = store_cls(**kwargs) - return backend - except Exception as exc: - if not suppress_debug: - debug(f"[Store] Failed to instantiate backend '{alias}': {exc}") - return None + # Fallback: build kwargs for each instance and compare resolved NAME + for instance_name, instance_cfg in instances.items(): + try: + kwargs = _build_kwargs(store_cls, str(instance_name), instance_cfg) + except Exception: + continue + alias = str(kwargs.get("NAME") or instance_name).strip() + if alias.lower() != desired: + continue + try: + for key in list(kwargs.keys()): + if _normalize_config_key(key) in {"PATH", "LOCATION"}: + kwargs[key] = str(expand_path(kwargs[key])) + except Exception: + pass + try: + backend = store_cls(**kwargs) + return backend + except Exception as exc: + if not suppress_debug: + debug(f"[Store] Failed to instantiate backend '{alias}': {exc}") + return None if not suppress_debug: debug(f"[Store] Backend '{backend_name}' not found in config") diff --git a/cmdlet/add_tag.py b/cmdlet/add_tag.py index 18470a9..2f89cea 100644 --- a/cmdlet/add_tag.py +++ b/cmdlet/add_tag.py @@ -13,6 +13,7 @@ from SYS.result_publication import publish_result_table from SYS import models from SYS import pipeline as ctx from . import _shared as sh +from Store import Store # retained for test monkeypatch compatibility normalize_result_input = sh.normalize_result_input 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 should_show_help = sh.should_show_help get_field = sh.get_field -from Store import Store _FIELD_NAME_RE = re.compile(r"^[A-Za-z0-9_]+$") @@ -663,8 +663,40 @@ class Add_Tag(Cmdlet): total_added = 0 total_modified = 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_no_match_items = 0 @@ -697,9 +729,8 @@ class Add_Tag(Cmdlet): is_known_backend = False try: - is_known_backend = bool(store_name_str) and store_registry.is_available( - store_name_str - ) + backend_probe, store_registry, _probe_exc = _resolve_backend(store_name_str) + is_known_backend = backend_probe is not None except Exception: 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 # metadata edit, then it's an error. 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, ) return 1 @@ -883,12 +914,7 @@ class Add_Tag(Cmdlet): ctx.emit(res) continue - backend, store_registry, exc = sh.get_store_backend( - config, - str(store_name), - store_registry=store_registry, - suppress_debug=True, - ) + backend, store_registry, exc = _resolve_backend(str(store_name)) if backend is None: log( f"[add_tag] Error: Unknown store '{store_name}': {exc}", @@ -1027,18 +1053,40 @@ class Add_Tag(Cmdlet): tags_to_add = [] merged_tags = list(existing_tag_list) - try: - ok_add = backend.add_tag( - resolved_hash, - item_tag_to_add, - config=config, - existing_tags=existing_tag_list, - ) - if not ok_add: - log("[add_tag] Warning: Store rejected tag update", file=sys.stderr) - except Exception as exc: - log(f"[add_tag] Warning: Failed adding tag: {exc}", file=sys.stderr) - ok_add = False + 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: + ok_add = backend.add_tag( + resolved_hash, + item_tag_to_add, + config=config, + existing_tags=existing_tag_list, + ) + if not ok_add: + log("[add_tag] Warning: Store rejected tag update", file=sys.stderr) + except Exception as exc: + log(f"[add_tag] Warning: Failed adding tag: {exc}", file=sys.stderr) + ok_add = False if ok_add and merged_tags: refreshed_list = list(merged_tags) @@ -1075,6 +1123,33 @@ class Add_Tag(Cmdlet): 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( f"[add_tag] Added {total_added} new tag(s) across {len(results)} item(s); modified {total_modified} item(s)", file=sys.stderr, diff --git a/cmdlet/delete_tag.py b/cmdlet/delete_tag.py index 780826c..3004eaf 100644 --- a/cmdlet/delete_tag.py +++ b/cmdlet/delete_tag.py @@ -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") 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 - # but we might need the piped result for the file context if @ selection was from a Tag table - # Actually, the @ selection logic above already extracted tags. + def _get_backend(store_name_str: str) -> Any | None: + if store_name_str in _backend_cache: + 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: - tags_to_delete: list[str] = [] item_hash = ( normalize_hash(override_hash) - if override_hash else normalize_hash(get_field(item, - "hash")) + if override_hash else normalize_hash(get_field(item, "hash")) ) item_path = get_field(item, "path") or get_field(item, "target") item_store = override_store or get_field(item, "store") @@ -575,27 +598,74 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: tags_to_delete = tags_arg else: tag_name = get_field(item, "tag_name") - if tag_name: - tags_to_delete = [str(tag_name)] + tags_to_delete = [str(tag_name)] if tag_name else [] else: - if tags_arg: - tags_to_delete = tags_arg - else: - continue + tags_to_delete = tags_arg or [] - if tags_to_delete: - if _process_deletion(tags_to_delete, - item_hash, - item_path, - item_store, - config, - result=item): - success_count += 1 + if not tags_to_delete or not item_hash or not item_store: + continue + + store_str = str(item_store) + + # Namespace wildcards (e.g. "album:") and template tags (e.g. "title:#(track)") + # need existing tags to expand — handle individually. + if needs_individual: + items_needing_individual.append((item, item_hash, item_path, store_str)) + 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 + try: + ctx.emit(item) + except Exception: + pass + if success_count > 0: return 0 return 1 @@ -631,13 +701,28 @@ def _process_deletion( ) return False - def _fetch_existing_tags() -> list[str]: + def _resolve_backend() -> tuple[Any | None, Any, Exception | None]: try: - backend, _store_registry, _exc = sh.get_store_backend( + return sh.get_preferred_store_backend( config, store_name, 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: return [] existing, _src = backend.get_tag(resolved_hash, config=config) @@ -687,11 +772,7 @@ def _process_deletion( return False try: - backend, _store_registry, exc = sh.get_store_backend( - config, - store_name, - suppress_debug=True, - ) + backend, _store_registry, exc = _resolve_backend() if backend is None: raise exc or KeyError(store_name) ok = backend.delete_tag(resolved_hash, list(tags), config=config) diff --git a/cmdlet/get_file.py b/cmdlet/get_file.py index 51bb78b..c80fc92 100644 --- a/cmdlet/get_file.py +++ b/cmdlet/get_file.py @@ -18,7 +18,7 @@ from urllib.request import pathname2url from SYS import pipeline as ctx from . import _shared as sh 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 API.HTTP import _download_direct_file 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: """Export file via hash+store backend.""" - debug(f"[get-file] run() called with result type: {type(result)}") 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( parsed.get("query"), @@ -70,8 +79,6 @@ class Get_File(sh.Cmdlet): output_path = parsed.get("path") output_name = parsed.get("name") - debug(f"[get-file] file_hash={file_hash} store_name={store_name}") - if not file_hash: log( 'Error: No file hash provided (pipe an item or use -query "hash:")' @@ -88,7 +95,19 @@ class Get_File(sh.Cmdlet): log("Error: Invalid hash format") 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 ""), + ("output_name", output_name or ""), + ], + border_style="blue", + ) + except Exception: + pass backend, _store_registry, _exc = sh.get_preferred_store_backend( config, @@ -99,17 +118,23 @@ class Get_File(sh.Cmdlet): log(f"Error: Storage backend '{store_name}' not found", file=sys.stderr) return 1 - debug(f"[get-file] Backend retrieved: {type(backend).__name__}") - # Get file metadata to determine name and extension - debug("[get-file] Getting metadata for hash...") metadata = backend.get_metadata(file_hash) if not metadata: log(f"Error: File metadata not found for hash {file_hash}") return 1 - debug( - f"[get-file] Metadata retrieved: title={metadata.get('title')}, ext={metadata.get('ext')}" - ) + try: + 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: candidates = [ @@ -124,17 +149,12 @@ class Get_File(sh.Cmdlet): return text return "" - debug(f"[get-file] Calling backend.get_file({file_hash})") - # 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 # (specifically Hydrus) to return a browser-friendly URL instead of a local path. 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) - debug(f"[get-file] backend.get_file returned: {source_path}") - download_url = None if isinstance(source_path, str): if source_path.startswith("http://") or source_path.startswith("https://"): @@ -142,6 +162,19 @@ class Get_File(sh.Cmdlet): else: 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 ""), + ], + border_style="magenta", + ) + except Exception: + pass + if download_url and output_path is None: # Hydrus backend returns a URL; open it only when no output path try: @@ -149,7 +182,18 @@ class Get_File(sh.Cmdlet): except Exception as exc: log(f"Error opening browser: {exc}", file=sys.stderr) 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( build_file_result_payload( @@ -172,7 +216,6 @@ class Get_File(sh.Cmdlet): else: output_dir = resolve_output_dir(config) - debug(f"[get-file] Output dir: {output_dir}") output_dir.mkdir(parents=True, exist_ok=True) # Determine output filename (only when exporting) @@ -202,13 +245,25 @@ class Get_File(sh.Cmdlet): suggested_filename=filename, ) dest_path = downloaded.path - debug(f"[get-file] Downloaded remote file to {dest_path}", file=sys.stderr) else: dest_path = self._unique_path(output_dir / filename) # Copy file to destination - debug(f"[get-file] Copying {source_path} -> {dest_path}", file=sys.stderr) 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) # Emit result for pipeline @@ -221,7 +276,6 @@ class Get_File(sh.Cmdlet): ) ) - debug("[get-file] Completed successfully") return 0 def _open_file_default(self, path: Path) -> None: diff --git a/plugins/alldebrid/__init__.py b/plugins/alldebrid/__init__.py index fe8cd4e..5d68563 100644 --- a/plugins/alldebrid/__init__.py +++ b/plugins/alldebrid/__init__.py @@ -1844,7 +1844,7 @@ class AllDebrid(TableProviderMixin, Provider): title=f"AllDebrid Item: {title}", metadata=detail_metadata, 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", ) diff --git a/plugins/alldebrid/alldebrid.json b/plugins/alldebrid/alldebrid.json index 7926790..17957dd 100644 --- a/plugins/alldebrid/alldebrid.json +++ b/plugins/alldebrid/alldebrid.json @@ -37,7 +37,7 @@ "(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": { "name": "turbobit", @@ -71,7 +71,7 @@ "(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": { "name": "hitfile", @@ -375,7 +375,7 @@ "(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": { "name": "filezip", @@ -17869,9 +17869,9 @@ "dl-protect.best" ], "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": { "name": "ed-protect", diff --git a/plugins/ftp/__init__.py b/plugins/ftp/__init__.py index 8805132..7f13da4 100644 --- a/plugins/ftp/__init__.py +++ b/plugins/ftp/__init__.py @@ -459,7 +459,7 @@ class FTP(Provider): title=f"FTP Item: {title}", metadata=detail_metadata, 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", ) diff --git a/plugins/mpv/__init__.py b/plugins/mpv/__init__.py index d3a8451..dfc06c4 100644 --- a/plugins/mpv/__init__.py +++ b/plugins/mpv/__init__.py @@ -1,3 +1,5 @@ +"""Canonical MPV plugin package.""" + from plugins.mpv.mpv_ipc import MPV __all__ = [ diff --git a/plugins/mpv/pipeline_helper.py b/plugins/mpv/pipeline_helper.py index 1ca6362..6f892ba 100644 --- a/plugins/mpv/pipeline_helper.py +++ b/plugins/mpv/pipeline_helper.py @@ -49,7 +49,7 @@ def _runtime_config_root() -> Path: """Best-effort config root for runtime execution. 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: cwd = Path.cwd().resolve() diff --git a/plugins/mpv/portable_config/script-opts/medeia-store-cache.json b/plugins/mpv/portable_config/script-opts/medeia-store-cache.json index 6390e69..3e279df 100644 --- a/plugins/mpv/portable_config/script-opts/medeia-store-cache.json +++ b/plugins/mpv/portable_config/script-opts/medeia-store-cache.json @@ -1 +1 @@ -{"choices":["local","rpi"]} \ No newline at end of file +{"choices":["rpi"]} \ No newline at end of file diff --git a/plugins/scp/__init__.py b/plugins/scp/__init__.py index 0ba1309..c78b76a 100644 --- a/plugins/scp/__init__.py +++ b/plugins/scp/__init__.py @@ -490,7 +490,7 @@ class SCP(Provider): title=f"SCP Item: {title}", metadata=detail_metadata, 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", )