from __future__ import annotations from typing import Any, Dict, Sequence import json from . import register import models import pipeline as ctx from helper import hydrus as hydrus_wrapper from ._shared import Cmdlet, CmdletArg, normalize_hash, parse_tag_arguments from helper.logger import log CMDLET = Cmdlet( name="delete-tags", summary="Remove tags from a Hydrus file.", usage="del-tags [-hash ] [,...]", aliases=["del-tag", "del-tags", "delete-tag"], args=[ CmdletArg("-hash", description="Override the Hydrus file hash (SHA256) to target instead of the selected result."), CmdletArg("[,...]", required=True, description="One or more tags to remove. Comma- or space-separated."), ], details=[ "- Requires a Hydrus file (hash present) or explicit -hash override.", "- Multiple tags can be comma-separated or space-separated.", ], ) @register(["del-tag", "del-tags", "delete-tag", "delete-tags"]) # Still needed for backward compatibility def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: # Help try: if any(str(a).lower() in {"-?", "/?", "--help", "-h", "help", "--cmdlet"} for a in args): log(json.dumps(CMDLET, ensure_ascii=False, indent=2)) return 0 except Exception: pass # Check if we have a piped TagItem with no args (i.e., from @1 | delete-tag) has_piped_tag = (result and hasattr(result, '__class__') and result.__class__.__name__ == 'TagItem' and hasattr(result, 'tag_name')) # Check if we have a piped list of TagItems (from @N selection) has_piped_tag_list = (isinstance(result, list) and result and hasattr(result[0], '__class__') and result[0].__class__.__name__ == 'TagItem') if not args and not has_piped_tag and not has_piped_tag_list: log("Requires at least one tag argument") return 1 # Parse -hash override and collect tags from remaining args override_hash: str | None = None rest: list[str] = [] i = 0 while i < len(args): a = args[i] low = str(a).lower() if low in {"-hash", "--hash", "hash"} and i + 1 < len(args): override_hash = str(args[i + 1]).strip() i += 2 continue rest.append(a) i += 1 # Check if first argument is @ syntax (result table selection) # @5 or @{2,5,8} to delete tags from ResultTable by index tags_from_at_syntax = [] hash_from_at_syntax = None if rest and str(rest[0]).startswith("@"): selector_arg = str(rest[0]) pipe_selector = selector_arg[1:].strip() # Parse @N or @{N,M,K} syntax if pipe_selector.startswith("{") and pipe_selector.endswith("}"): # @{2,5,8} pipe_selector = pipe_selector[1:-1] try: indices = [int(tok.strip()) for tok in pipe_selector.split(',') if tok.strip()] except ValueError: log("Invalid selection syntax. Use @2 or @{2,5,8}") return 1 # Get the last ResultTable from pipeline context try: last_table = ctx._LAST_RESULT_TABLE if last_table: # Extract tags from selected rows for idx in indices: if 1 <= idx <= len(last_table.rows): # Look for a TagItem in _LAST_RESULT_ITEMS by index if idx - 1 < len(ctx._LAST_RESULT_ITEMS): item = ctx._LAST_RESULT_ITEMS[idx - 1] if hasattr(item, '__class__') and item.__class__.__name__ == 'TagItem': tag_name = getattr(item, 'tag_name', None) if tag_name: log(f"[delete_tag] Extracted tag from @{idx}: {tag_name}") tags_from_at_syntax.append(tag_name) # Also get hash from first item for consistency if not hash_from_at_syntax: hash_from_at_syntax = getattr(item, 'hash_hex', None) if not tags_from_at_syntax: log(f"No tags found at indices: {indices}") return 1 else: log("No ResultTable in pipeline (use @ after running get-tag)") return 1 except Exception as exc: log(f"Error processing @ selection: {exc}", file=__import__('sys').stderr) return 1 # Handle @N selection which creates a list - extract the first item if isinstance(result, list) and len(result) > 0: # If we have a list of TagItems, we want to process ALL of them if no args provided # This handles: delete-tag @1 (where @1 expands to a list containing one TagItem) if not args and hasattr(result[0], '__class__') and result[0].__class__.__name__ == 'TagItem': # We will extract tags from the list later pass else: result = result[0] # Determine tags and hash to use tags: list[str] = [] hash_hex = None if tags_from_at_syntax: # Use tags extracted from @ syntax tags = tags_from_at_syntax hash_hex = normalize_hash(override_hash) if override_hash else normalize_hash(hash_from_at_syntax) log(f"[delete_tag] Using @ syntax extraction: {len(tags)} tag(s) to delete: {tags}") elif isinstance(result, list) and result and hasattr(result[0], '__class__') and result[0].__class__.__name__ == 'TagItem': # Got a list of TagItems (e.g. from delete-tag @1) tags = [getattr(item, 'tag_name') for item in result if getattr(item, 'tag_name', None)] # Use hash from first item hash_hex = normalize_hash(override_hash) if override_hash else normalize_hash(getattr(result[0], "hash_hex", None)) elif result and hasattr(result, '__class__') and result.__class__.__name__ == 'TagItem': # Got a piped TagItem - delete this specific tag tag_name = getattr(result, 'tag_name', None) if tag_name: tags = [tag_name] hash_hex = normalize_hash(override_hash) if override_hash else normalize_hash(getattr(result, "hash_hex", None)) else: # Traditional mode - parse tag arguments tags = parse_tag_arguments(rest) hash_hex = normalize_hash(override_hash) if override_hash else normalize_hash(getattr(result, "hash_hex", None)) if not tags: log("No valid tags were provided") return 1 if not hash_hex: log("Selected result does not include a hash") return 1 try: service_name = hydrus_wrapper.get_tag_service_name(config) except Exception as exc: log(f"Failed to resolve tag service: {exc}") return 1 try: client = hydrus_wrapper.get_client(config) except Exception as exc: log(f"Hydrus client unavailable: {exc}") return 1 if client is None: log("Hydrus client unavailable") return 1 log(f"[delete_tag] Sending deletion request: hash={hash_hex}, tags={tags}, service={service_name}") try: result = client.delete_tags(hash_hex, tags, service_name) log(f"[delete_tag] Hydrus response: {result}") except Exception as exc: log(f"Hydrus del-tag failed: {exc}") return 1 preview = hash_hex[:12] + ('…' if len(hash_hex) > 12 else '') log(f"Removed {len(tags)} tag(s) from {preview} via '{service_name}'.") # Re-fetch and emit updated tags after deletion try: payload = client.fetch_file_metadata(hashes=[str(hash_hex)], include_service_keys_to_tags=True, include_file_urls=False) items = payload.get("metadata") if isinstance(payload, dict) else None if isinstance(items, list) and items: meta = items[0] if isinstance(items[0], dict) else None if isinstance(meta, dict): # Extract tags from updated metadata from cmdlets.get_tag import _extract_my_tags_from_hydrus_meta, TagItem service_key = hydrus_wrapper.get_tag_service_key(client, service_name) updated_tags = _extract_my_tags_from_hydrus_meta(meta, service_key, service_name) # Emit updated tags as TagItem objects from result_table import ResultTable table = ResultTable("Tags", max_columns=2) tag_items = [] for idx, tag_name in enumerate(updated_tags, start=1): tag_item = TagItem( tag_name=tag_name, tag_index=idx, hash_hex=hash_hex, source="hydrus", service_name=service_name, ) tag_items.append(tag_item) table.add_result(tag_item) ctx.emit(tag_item) # Store items for @ selection in next command (CLI will handle table management) # Don't call set_last_result_table so we don't pollute history or table context except Exception as exc: log(f"Warning: Could not fetch updated tags after deletion: {exc}", file=__import__('sys').stderr) return 0