This commit is contained in:
nose
2025-12-06 00:10:19 -08:00
parent 5482ee5586
commit f29709d951
20 changed files with 1353 additions and 419 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from typing import Any, Dict, Sequence
from pathlib import Path
import json
import sys
@@ -12,6 +13,49 @@ from ._shared import Cmdlet, CmdletArg, normalize_hash, parse_tag_arguments
from helper.logger import debug, log
def _refresh_tag_view_if_current(hash_hex: str | None, file_path: str | None, config: Dict[str, Any]) -> None:
"""If the current subject matches the target, refresh tags via get-tag."""
try:
from cmdlets import get_tag as get_tag_cmd # type: ignore
except Exception:
return
try:
subject = ctx.get_last_result_subject()
if subject is None:
return
def norm(val: Any) -> str:
return str(val).lower()
target_hash = norm(hash_hex) if hash_hex else None
target_path = norm(file_path) if file_path else None
subj_hashes: list[str] = []
subj_paths: list[str] = []
if isinstance(subject, dict):
subj_hashes = [norm(v) for v in [subject.get("hydrus_hash"), subject.get("hash"), subject.get("hash_hex"), subject.get("file_hash")] if v]
subj_paths = [norm(v) for v in [subject.get("file_path"), subject.get("path"), subject.get("target")] if v]
else:
subj_hashes = [norm(getattr(subject, f, None)) for f in ("hydrus_hash", "hash", "hash_hex", "file_hash") if getattr(subject, f, None)]
subj_paths = [norm(getattr(subject, f, None)) for f in ("file_path", "path", "target") if getattr(subject, f, None)]
is_match = False
if target_hash and target_hash in subj_hashes:
is_match = True
if target_path and target_path in subj_paths:
is_match = True
if not is_match:
return
refresh_args: list[str] = []
if hash_hex:
refresh_args.extend(["-hash", hash_hex])
get_tag_cmd._run(subject, refresh_args, config)
except Exception:
pass
CMDLET = Cmdlet(
name="delete-tags",
summary="Remove tags from a Hydrus file.",
@@ -220,12 +264,69 @@ def _process_deletion(tags: list[str], hash_hex: str | None, file_path: str | No
if not tags:
return False
def _fetch_existing_tags() -> list[str]:
existing: list[str] = []
# Prefer local DB when we have a path and not explicitly hydrus
if file_path and (source == "local" or (source != "hydrus" and not hash_hex)):
try:
from helper.local_library import LocalLibraryDB
from config import get_local_storage_path
path_obj = Path(file_path)
local_root = get_local_storage_path(config) or path_obj.parent
with LocalLibraryDB(local_root) as db:
existing = db.get_tags(path_obj) or []
except Exception:
existing = []
elif hash_hex:
try:
client = hydrus_wrapper.get_client(config)
payload = client.fetch_file_metadata(
hashes=[hash_hex],
include_service_keys_to_tags=True,
include_file_urls=False,
)
items = payload.get("metadata") if isinstance(payload, dict) else None
meta = items[0] if isinstance(items, list) and items else None
if isinstance(meta, dict):
tags_payload = meta.get("tags")
if isinstance(tags_payload, dict):
seen: set[str] = set()
for svc_data in tags_payload.values():
if not isinstance(svc_data, dict):
continue
display = svc_data.get("display_tags")
if isinstance(display, list):
for t in display:
if isinstance(t, (str, bytes)):
val = str(t).strip()
if val and val not in seen:
seen.add(val)
existing.append(val)
storage = svc_data.get("storage_tags")
if isinstance(storage, dict):
current_list = storage.get("0") or storage.get(0)
if isinstance(current_list, list):
for t in current_list:
if isinstance(t, (str, bytes)):
val = str(t).strip()
if val and val not in seen:
seen.add(val)
existing.append(val)
except Exception:
existing = []
return existing
# Safety: block deleting title: without replacement to avoid untitled files
# Safety: only block if this deletion would remove the final title tag
title_tags = [t for t in tags if isinstance(t, str) and t.lower().startswith("title:")]
if title_tags:
log("Cannot delete title: tag without replacement. Use add-tag \"title:new title\" instead.", file=sys.stderr)
return False
existing_tags = _fetch_existing_tags()
current_titles = [t for t in existing_tags if isinstance(t, str) and t.lower().startswith("title:")]
del_title_set = {t.lower() for t in title_tags}
remaining_titles = [t for t in current_titles if t.lower() not in del_title_set]
if current_titles and not remaining_titles:
log("Cannot delete the last title: tag. Add a replacement title first (add-tag \"title:new title\").", file=sys.stderr)
return False
if not hash_hex and not file_path:
log("Item does not include a hash or file path")
@@ -253,6 +354,7 @@ def _process_deletion(tags: list[str], hash_hex: str | None, file_path: str | No
with LocalLibraryDB(local_root) as db:
db.remove_tags(path_obj, tags)
debug(f"Removed {len(tags)} tag(s) from {path_obj.name} (local)")
_refresh_tag_view_if_current(hash_hex, file_path, config)
return True
except Exception as exc:
@@ -276,6 +378,7 @@ def _process_deletion(tags: list[str], hash_hex: str | None, file_path: str | No
preview = hash_hex[:12] + ('' if len(hash_hex) > 12 else '')
debug(f"Removed {len(tags)} tag(s) from {preview} via '{service_name}'.")
_refresh_tag_view_if_current(hash_hex, None, config)
return True
except Exception as exc: