df
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled
This commit is contained in:
@@ -29,12 +29,28 @@ CMDLET = Cmdlet(
|
||||
summary="Associate file relationships (king/alt/related) in Hydrus based on relationship tags in sidecar.",
|
||||
usage="@1-3 | add-relationship -king @4 OR add-relationship -path <file> OR @1,@2,@3 | add-relationship",
|
||||
arg=[
|
||||
CmdletArg("path", type="string", description="Specify the local file path (if not piping a result)."),
|
||||
CmdletArg(
|
||||
"path",
|
||||
type="string",
|
||||
description="Specify the local file path (if not piping a result).",
|
||||
),
|
||||
SharedArgs.STORE,
|
||||
SharedArgs.QUERY,
|
||||
CmdletArg("-king", type="string", description="Explicitly set the king hash/file for relationships (e.g., -king @4 or -king hash)"),
|
||||
CmdletArg("-alt", type="string", description="Explicitly select alt item(s) by @ selection or hash list (e.g., -alt @3-5 or -alt <hash>,<hash>)"),
|
||||
CmdletArg("-type", type="string", description="Relationship type for piped items (default: 'alt', options: 'king', 'alt', 'related')"),
|
||||
CmdletArg(
|
||||
"-king",
|
||||
type="string",
|
||||
description="Explicitly set the king hash/file for relationships (e.g., -king @4 or -king hash)",
|
||||
),
|
||||
CmdletArg(
|
||||
"-alt",
|
||||
type="string",
|
||||
description="Explicitly select alt item(s) by @ selection or hash list (e.g., -alt @3-5 or -alt <hash>,<hash>)",
|
||||
),
|
||||
CmdletArg(
|
||||
"-type",
|
||||
type="string",
|
||||
description="Relationship type for piped items (default: 'alt', options: 'king', 'alt', 'related')",
|
||||
),
|
||||
],
|
||||
detail=[
|
||||
"- Mode 1: Pipe multiple items, first becomes king, rest become alts (default)",
|
||||
@@ -54,7 +70,7 @@ def _normalise_hash_hex(value: Optional[str]) -> Optional[str]:
|
||||
if not value or not isinstance(value, str):
|
||||
return None
|
||||
normalized = value.strip().lower()
|
||||
if len(normalized) == 64 and all(c in '0123456789abcdef' for c in normalized):
|
||||
if len(normalized) == 64 and all(c in "0123456789abcdef" for c in normalized):
|
||||
return normalized
|
||||
return None
|
||||
|
||||
@@ -71,9 +87,9 @@ def _extract_relationships_from_tag(tag_value: str) -> Dict[str, list[str]]:
|
||||
result: Dict[str, list[str]] = {}
|
||||
if not isinstance(tag_value, str):
|
||||
return result
|
||||
|
||||
|
||||
# Match patterns like hash(king)HASH or hash(type)<HASH>
|
||||
pattern = r'hash\((\w+)\)<?([a-fA-F0-9]{64})>?'
|
||||
pattern = r"hash\((\w+)\)<?([a-fA-F0-9]{64})>?"
|
||||
matches = re.findall(pattern, tag_value)
|
||||
|
||||
if matches:
|
||||
@@ -118,7 +134,11 @@ def _apply_relationships_from_tags(
|
||||
- Treat the first hash (king) as the king.
|
||||
- Store directional alt -> king relationships (no reverse edge).
|
||||
"""
|
||||
rel_tags = [t for t in relationship_tags if isinstance(t, str) and t.strip().lower().startswith("relationship:")]
|
||||
rel_tags = [
|
||||
t
|
||||
for t in relationship_tags
|
||||
if isinstance(t, str) and t.strip().lower().startswith("relationship:")
|
||||
]
|
||||
if not rel_tags:
|
||||
return 0
|
||||
|
||||
@@ -166,7 +186,9 @@ def _apply_relationships_from_tags(
|
||||
# For local DB we treat all non-king hashes as alts.
|
||||
alt_hashes: list[str] = []
|
||||
for bucket in ("alt", "related"):
|
||||
alt_hashes.extend([h for h in (rels.get(bucket) or []) if isinstance(h, str)])
|
||||
alt_hashes.extend(
|
||||
[h for h in (rels.get(bucket) or []) if isinstance(h, str)]
|
||||
)
|
||||
|
||||
for alt in alt_hashes:
|
||||
alt_norm = _normalise_hash_hex(alt)
|
||||
@@ -191,7 +213,7 @@ def _parse_at_selection(token: str) -> Optional[list[int]]:
|
||||
if not isinstance(token, str):
|
||||
return None
|
||||
t = token.strip()
|
||||
if not t.startswith('@'):
|
||||
if not t.startswith("@"):
|
||||
return None
|
||||
if t == "@*":
|
||||
return [] # special sentinel: caller interprets as "all"
|
||||
@@ -293,27 +315,28 @@ def _resolve_king_reference(king_arg: str) -> Optional[str]:
|
||||
"""
|
||||
if not king_arg:
|
||||
return None
|
||||
|
||||
|
||||
# Check if it's already a valid hash
|
||||
normalized = _normalise_hash_hex(king_arg)
|
||||
if normalized:
|
||||
return normalized
|
||||
|
||||
|
||||
# Try to resolve as @ selection from pipeline context
|
||||
if king_arg.startswith('@'):
|
||||
if king_arg.startswith("@"):
|
||||
selected = _resolve_items_from_at(king_arg)
|
||||
if not selected:
|
||||
log(f"Cannot resolve {king_arg}: no selection context", file=sys.stderr)
|
||||
return None
|
||||
if len(selected) != 1:
|
||||
log(f"{king_arg} selects {len(selected)} items; -king requires exactly 1", file=sys.stderr)
|
||||
log(
|
||||
f"{king_arg} selects {len(selected)} items; -king requires exactly 1",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return None
|
||||
|
||||
item = selected[0]
|
||||
item_hash = (
|
||||
get_field(item, 'hash_hex')
|
||||
or get_field(item, 'hash')
|
||||
or get_field(item, 'file_hash')
|
||||
get_field(item, "hash_hex") or get_field(item, "hash") or get_field(item, "file_hash")
|
||||
)
|
||||
|
||||
if item_hash:
|
||||
@@ -323,11 +346,16 @@ def _resolve_king_reference(king_arg: str) -> Optional[str]:
|
||||
|
||||
log(f"Item {king_arg} has no hash information", file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _refresh_relationship_view_if_current(target_hash: Optional[str], target_path: Optional[str], other: Optional[str], config: Dict[str, Any]) -> None:
|
||||
def _refresh_relationship_view_if_current(
|
||||
target_hash: Optional[str],
|
||||
target_path: Optional[str],
|
||||
other: Optional[str],
|
||||
config: Dict[str, Any],
|
||||
) -> None:
|
||||
"""If the current subject matches the target, refresh relationships via get-relationship."""
|
||||
try:
|
||||
from cmdlet import get as get_cmdlet # type: ignore
|
||||
@@ -356,11 +384,32 @@ def _refresh_relationship_view_if_current(target_hash: Optional[str], target_pat
|
||||
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]
|
||||
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)]
|
||||
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_hashes and any(h in subj_hashes for h in target_hashes):
|
||||
@@ -380,7 +429,7 @@ def _refresh_relationship_view_if_current(target_hash: Optional[str], target_pat
|
||||
|
||||
def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Associate file relationships in Hydrus.
|
||||
|
||||
|
||||
Two modes of operation:
|
||||
1. Read from sidecar: Looks for relationship tags in the file's sidecar (format: "relationship: hash(king)<HASH>,hash(alt)<HASH>")
|
||||
2. Pipeline mode: When piping multiple results, the first becomes "king" and subsequent items become "alt"
|
||||
@@ -391,7 +440,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if should_show_help(_args):
|
||||
log(f"Cmdlet: {CMDLET.name}\nSummary: {CMDLET.summary}\nUsage: {CMDLET.usage}")
|
||||
return 0
|
||||
|
||||
|
||||
# Parse arguments using CMDLET spec
|
||||
parsed = parse_cmdlet_args(_args, CMDLET)
|
||||
arg_path: Optional[Path] = None
|
||||
@@ -420,7 +469,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if alt_arg:
|
||||
alt_text = str(alt_arg).strip()
|
||||
resolved_alt_items: list[Any] = []
|
||||
if alt_text.startswith('@'):
|
||||
if alt_text.startswith("@"):
|
||||
selected = _resolve_items_from_at(alt_text)
|
||||
if not selected:
|
||||
log(f"Failed to resolve -alt {alt_text}: no selection context", file=sys.stderr)
|
||||
@@ -431,7 +480,10 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
parts = [p.strip() for p in alt_text.replace(";", ",").split(",") if p.strip()]
|
||||
hashes = [h for h in (_normalise_hash_hex(p) for p in parts) if h]
|
||||
if not hashes:
|
||||
log("Invalid -alt value (expected @ selection or 64-hex sha256 hash list)", file=sys.stderr)
|
||||
log(
|
||||
"Invalid -alt value (expected @ selection or 64-hex sha256 hash list)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not override_store:
|
||||
log("-store is required when using -alt with a raw hash list", file=sys.stderr)
|
||||
@@ -445,11 +497,11 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
log("-store is required when using -query without piped items", file=sys.stderr)
|
||||
return 1
|
||||
items_to_process = [{"hash": h, "store": str(override_store)} for h in override_hashes]
|
||||
|
||||
|
||||
if not items_to_process and not arg_path:
|
||||
log("No items provided to add-relationship (no piped result and no -path)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
# If no items from pipeline, just process the -path arg
|
||||
if not items_to_process and arg_path:
|
||||
items_to_process = [{"file_path": arg_path}]
|
||||
@@ -459,13 +511,16 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
king_store: Optional[str] = None
|
||||
if king_arg:
|
||||
king_text = str(king_arg).strip()
|
||||
if king_text.startswith('@'):
|
||||
if king_text.startswith("@"):
|
||||
selected = _resolve_items_from_at(king_text)
|
||||
if not selected:
|
||||
log(f"Cannot resolve {king_text}: no selection context", file=sys.stderr)
|
||||
return 1
|
||||
if len(selected) != 1:
|
||||
log(f"{king_text} selects {len(selected)} items; -king requires exactly 1", file=sys.stderr)
|
||||
log(
|
||||
f"{king_text} selects {len(selected)} items; -king requires exactly 1",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
king_hash, king_store = _extract_hash_and_store(selected[0])
|
||||
if not king_hash:
|
||||
@@ -490,18 +545,27 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if len(stores) == 1:
|
||||
store_name = next(iter(stores))
|
||||
elif len(stores) > 1:
|
||||
log("Multiple stores detected (king/alt across stores); use -store and ensure all selections are from the same store", file=sys.stderr)
|
||||
log(
|
||||
"Multiple stores detected (king/alt across stores); use -store and ensure all selections are from the same store",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Enforce same-store relationships when store context is available.
|
||||
if king_store and store_name and str(king_store) != str(store_name):
|
||||
log(f"Cross-store relationship blocked: king is in store '{king_store}' but -store is '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: king is in store '{king_store}' but -store is '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if store_name:
|
||||
for item in items_to_process:
|
||||
s = get_field(item, "store")
|
||||
if s and str(s) != str(store_name):
|
||||
log(f"Cross-store relationship blocked: alt item store '{s}' != '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: alt item store '{s}' != '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Resolve backend for store/hash operations
|
||||
@@ -564,7 +628,11 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
sidecar_path = find_sidecar(arg_path)
|
||||
if sidecar_path is not None and sidecar_path.exists():
|
||||
_, tags, _ = read_sidecar(sidecar_path)
|
||||
relationship_tags = [t for t in (tags or []) if isinstance(t, str) and t.lower().startswith("relationship:")]
|
||||
relationship_tags = [
|
||||
t
|
||||
for t in (tags or [])
|
||||
if isinstance(t, str) and t.lower().startswith("relationship:")
|
||||
]
|
||||
if relationship_tags:
|
||||
code = _apply_relationships_from_tags(
|
||||
relationship_tags,
|
||||
@@ -587,7 +655,13 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
else:
|
||||
tags_val = getattr(item, "tag", None)
|
||||
if isinstance(tags_val, list):
|
||||
rel_tags_from_pipe.extend([t for t in tags_val if isinstance(t, str) and t.lower().startswith("relationship:")])
|
||||
rel_tags_from_pipe.extend(
|
||||
[
|
||||
t
|
||||
for t in tags_val
|
||||
if isinstance(t, str) and t.lower().startswith("relationship:")
|
||||
]
|
||||
)
|
||||
elif isinstance(tags_val, str) and tags_val.lower().startswith("relationship:"):
|
||||
rel_tags_from_pipe.append(tags_val)
|
||||
|
||||
@@ -613,7 +687,10 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
for item in items_to_process:
|
||||
h, item_store = _extract_hash_and_store(item)
|
||||
if item_store and store_name and str(item_store) != str(store_name):
|
||||
log(f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not h:
|
||||
continue
|
||||
@@ -622,19 +699,26 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
continue
|
||||
# directional alt -> king by default for local DB
|
||||
bidirectional = str(rel_type).lower() != "alt"
|
||||
db.set_relationship_by_hash(h, first_hash, str(rel_type), bidirectional=bidirectional)
|
||||
db.set_relationship_by_hash(
|
||||
h, first_hash, str(rel_type), bidirectional=bidirectional
|
||||
)
|
||||
return 0
|
||||
|
||||
# Mode 2: explicit king
|
||||
for item in items_to_process:
|
||||
h, item_store = _extract_hash_and_store(item)
|
||||
if item_store and store_name and str(item_store) != str(store_name):
|
||||
log(f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not h or h == king_hash:
|
||||
continue
|
||||
bidirectional = str(rel_type).lower() != "alt"
|
||||
db.set_relationship_by_hash(h, king_hash, str(rel_type), bidirectional=bidirectional)
|
||||
db.set_relationship_by_hash(
|
||||
h, king_hash, str(rel_type), bidirectional=bidirectional
|
||||
)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
log(f"Failed to set store relationships: {exc}", file=sys.stderr)
|
||||
@@ -648,7 +732,10 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
|
||||
# Verify hashes exist in this Hydrus backend to prevent cross-store edges.
|
||||
if king_hash and (not _hydrus_hash_exists(hydrus_client, king_hash)):
|
||||
log(f"Cross-store relationship blocked: king hash not found in store '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: king hash not found in store '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Mode 1: first is king
|
||||
@@ -657,19 +744,28 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
for item in items_to_process:
|
||||
h, item_store = _extract_hash_and_store(item)
|
||||
if item_store and store_name and str(item_store) != str(store_name):
|
||||
log(f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not h:
|
||||
continue
|
||||
if not first_hash:
|
||||
first_hash = h
|
||||
if not _hydrus_hash_exists(hydrus_client, first_hash):
|
||||
log(f"Cross-store relationship blocked: hash not found in store '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: hash not found in store '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
continue
|
||||
if h != first_hash:
|
||||
if not _hydrus_hash_exists(hydrus_client, h):
|
||||
log(f"Cross-store relationship blocked: hash not found in store '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: hash not found in store '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
hydrus_client.set_relationship(h, first_hash, str(rel_type))
|
||||
return 0
|
||||
@@ -678,12 +774,18 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
for item in items_to_process:
|
||||
h, item_store = _extract_hash_and_store(item)
|
||||
if item_store and store_name and str(item_store) != str(store_name):
|
||||
log(f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: item store '{item_store}' != '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not h or h == king_hash:
|
||||
continue
|
||||
if not _hydrus_hash_exists(hydrus_client, h):
|
||||
log(f"Cross-store relationship blocked: hash not found in store '{store_name}'", file=sys.stderr)
|
||||
log(
|
||||
f"Cross-store relationship blocked: hash not found in store '{store_name}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
hydrus_client.set_relationship(h, king_hash, str(rel_type))
|
||||
return 0
|
||||
@@ -693,18 +795,19 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Extract hash and path from current item
|
||||
file_hash = None
|
||||
file_path_from_result = None
|
||||
|
||||
|
||||
if isinstance(item, dict):
|
||||
file_hash = item.get("hash_hex") or item.get("hash")
|
||||
file_path_from_result = item.get("file_path") or item.get("path") or item.get("target")
|
||||
else:
|
||||
file_hash = getattr(item, "hash_hex", None) or getattr(item, "hash", None)
|
||||
file_path_from_result = getattr(item, "file_path", None) or getattr(item, "path", None)
|
||||
|
||||
|
||||
# Legacy LOCAL STORAGE MODE: Handle relationships for local files
|
||||
# (kept for -path sidecar workflows; store/hash mode above is preferred)
|
||||
from API.folder import LocalLibrarySearchOptimizer
|
||||
from config import get_local_storage_path
|
||||
|
||||
local_storage_path = get_local_storage_path(config) if config else None
|
||||
use_local_storage = bool(local_storage_path)
|
||||
local_storage_root: Optional[Path] = None
|
||||
@@ -743,13 +846,22 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
return 1
|
||||
king_file_path = opt.db.search_hash(normalized_king)
|
||||
if not king_file_path:
|
||||
log(f"King hash not found in local DB: {king_hash}", file=sys.stderr)
|
||||
log(
|
||||
f"King hash not found in local DB: {king_hash}", file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
|
||||
bidirectional = str(rel_type).lower() != "alt"
|
||||
opt.db.set_relationship(file_path_obj, king_file_path, rel_type, bidirectional=bidirectional)
|
||||
log(f"Set {rel_type} relationship: {file_path_obj.name} -> {king_file_path.name}", file=sys.stderr)
|
||||
_refresh_relationship_view_if_current(None, str(file_path_obj), str(king_file_path), config)
|
||||
opt.db.set_relationship(
|
||||
file_path_obj, king_file_path, rel_type, bidirectional=bidirectional
|
||||
)
|
||||
log(
|
||||
f"Set {rel_type} relationship: {file_path_obj.name} -> {king_file_path.name}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
_refresh_relationship_view_if_current(
|
||||
None, str(file_path_obj), str(king_file_path), config
|
||||
)
|
||||
else:
|
||||
# Original behavior: first becomes king, rest become alts
|
||||
try:
|
||||
@@ -760,16 +872,29 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if not king_path:
|
||||
try:
|
||||
ctx.store_value("relationship_king_path", str(file_path_obj))
|
||||
log(f"Established king file: {file_path_obj.name}", file=sys.stderr)
|
||||
log(
|
||||
f"Established king file: {file_path_obj.name}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if king_path and king_path != str(file_path_obj):
|
||||
bidirectional = str(rel_type).lower() != "alt"
|
||||
opt.db.set_relationship(file_path_obj, Path(king_path), rel_type, bidirectional=bidirectional)
|
||||
log(f"Set {rel_type} relationship: {file_path_obj.name} -> {Path(king_path).name}", file=sys.stderr)
|
||||
_refresh_relationship_view_if_current(None, str(file_path_obj), str(king_path), config)
|
||||
opt.db.set_relationship(
|
||||
file_path_obj,
|
||||
Path(king_path),
|
||||
rel_type,
|
||||
bidirectional=bidirectional,
|
||||
)
|
||||
log(
|
||||
f"Set {rel_type} relationship: {file_path_obj.name} -> {Path(king_path).name}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
_refresh_relationship_view_if_current(
|
||||
None, str(file_path_obj), str(king_path), config
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"Local storage error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -781,16 +906,21 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if not file_hash:
|
||||
log("Invalid file hash format", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
# If explicit -king provided, use it
|
||||
if king_hash:
|
||||
try:
|
||||
hydrus_client.set_relationship(file_hash, king_hash, rel_type)
|
||||
log(
|
||||
f"[add-relationship] Set {rel_type} relationship: {file_hash} <-> {king_hash}",
|
||||
file=sys.stderr
|
||||
file=sys.stderr,
|
||||
)
|
||||
_refresh_relationship_view_if_current(
|
||||
file_hash,
|
||||
str(file_path_from_result) if file_path_from_result is not None else None,
|
||||
king_hash,
|
||||
config,
|
||||
)
|
||||
_refresh_relationship_view_if_current(file_hash, str(file_path_from_result) if file_path_from_result is not None else None, king_hash, config)
|
||||
except Exception as exc:
|
||||
log(f"Failed to set relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -800,7 +930,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
existing_king = ctx.load_value("relationship_king")
|
||||
except Exception:
|
||||
existing_king = None
|
||||
|
||||
|
||||
# If this is the first item, make it the king
|
||||
if not existing_king:
|
||||
try:
|
||||
@@ -809,34 +939,48 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
continue # Move to next item
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# If we already have a king and this is a different hash, link them
|
||||
if existing_king and existing_king != file_hash:
|
||||
try:
|
||||
hydrus_client.set_relationship(file_hash, existing_king, rel_type)
|
||||
log(
|
||||
f"[add-relationship] Set {rel_type} relationship: {file_hash} <-> {existing_king}",
|
||||
file=sys.stderr
|
||||
file=sys.stderr,
|
||||
)
|
||||
_refresh_relationship_view_if_current(
|
||||
file_hash,
|
||||
(
|
||||
str(file_path_from_result)
|
||||
if file_path_from_result is not None
|
||||
else None
|
||||
),
|
||||
existing_king,
|
||||
config,
|
||||
)
|
||||
_refresh_relationship_view_if_current(file_hash, str(file_path_from_result) if file_path_from_result is not None else None, existing_king, config)
|
||||
except Exception as exc:
|
||||
log(f"Failed to set relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
# If we get here, we didn't have a usable local path and Hydrus isn't available/usable.
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
# FILE MODE: Read relationships from sidecar (legacy mode - for -path arg only)
|
||||
log("Note: Use piping mode for easier relationships. Example: 1,2,3 | add-relationship", file=sys.stderr)
|
||||
|
||||
log(
|
||||
"Note: Use piping mode for easier relationships. Example: 1,2,3 | add-relationship",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# Resolve media path from -path arg or result target
|
||||
target = getattr(result, "target", None) or getattr(result, "path", None)
|
||||
media_path = arg_path if arg_path is not None else Path(str(target)) if isinstance(target, str) else None
|
||||
media_path = (
|
||||
arg_path if arg_path is not None else Path(str(target)) if isinstance(target, str) else None
|
||||
)
|
||||
if media_path is None:
|
||||
log("Provide -path <file> or pipe a local file result", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
# Validate local file
|
||||
if str(media_path).lower().startswith(("http://", "https://")):
|
||||
log("This cmdlet requires a local file path, not a URL", file=sys.stderr)
|
||||
@@ -851,7 +995,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
except Exception as exc:
|
||||
log(f"Hydrus client unavailable: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if hydrus_client is None:
|
||||
log("Hydrus client unavailable", file=sys.stderr)
|
||||
return 1
|
||||
@@ -861,7 +1005,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
if sidecar_path is None:
|
||||
log(f"No sidecar found for {media_path.name}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
try:
|
||||
_, tags, _ = read_sidecar(sidecar_path)
|
||||
except Exception as exc:
|
||||
@@ -869,63 +1013,68 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
return 1
|
||||
|
||||
# Find relationship tags (format: "relationship: hash(king)<HASH>,hash(alt)<HASH>,hash(related)<HASH>")
|
||||
relationship_tags = [t for t in tags if isinstance(t, str) and t.lower().startswith("relationship:")]
|
||||
|
||||
relationship_tags = [
|
||||
t for t in tags if isinstance(t, str) and t.lower().startswith("relationship:")
|
||||
]
|
||||
|
||||
if not relationship_tags:
|
||||
log(f"No relationship tags found in sidecar", file=sys.stderr)
|
||||
return 0 # Not an error, just nothing to do
|
||||
|
||||
|
||||
# Get the file hash from result (should have been set by add-file)
|
||||
file_hash = getattr(result, "hash_hex", None)
|
||||
if not file_hash:
|
||||
log("File hash not available (run add-file first)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
file_hash = _normalise_hash_hex(file_hash)
|
||||
if not file_hash:
|
||||
log("Invalid file hash format", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
# Parse relationships from tags and apply them
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
|
||||
for rel_tag in relationship_tags:
|
||||
try:
|
||||
# Parse: "relationship: hash(king)<HASH>,hash(alt)<HASH>,hash(related)<HASH>"
|
||||
rel_str = rel_tag.split(":", 1)[1].strip() # Get part after "relationship:"
|
||||
|
||||
|
||||
# Parse relationships
|
||||
rels = _extract_relationships_from_tag(f"relationship: {rel_str}")
|
||||
|
||||
|
||||
# Set the relationships in Hydrus
|
||||
for rel_type, related_hashes in rels.items():
|
||||
if not related_hashes:
|
||||
continue
|
||||
|
||||
|
||||
for related_hash in related_hashes:
|
||||
# Don't set relationship between hash and itself
|
||||
if file_hash == related_hash:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
hydrus_client.set_relationship(file_hash, related_hash, rel_type)
|
||||
log(
|
||||
f"[add-relationship] Set {rel_type} relationship: "
|
||||
f"{file_hash} <-> {related_hash}",
|
||||
file=sys.stderr
|
||||
file=sys.stderr,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as exc:
|
||||
log(f"Failed to set {rel_type} relationship: {exc}", file=sys.stderr)
|
||||
error_count += 1
|
||||
|
||||
|
||||
except Exception as exc:
|
||||
log(f"Failed to parse relationship tag: {exc}", file=sys.stderr)
|
||||
error_count += 1
|
||||
|
||||
|
||||
if success_count > 0:
|
||||
log(f"Successfully set {success_count} relationship(s) for {media_path.name}", file=sys.stderr)
|
||||
log(
|
||||
f"Successfully set {success_count} relationship(s) for {media_path.name}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
ctx.emit(f"add-relationship: {media_path.name} ({success_count} relationships set)")
|
||||
return 0
|
||||
elif error_count == 0:
|
||||
@@ -940,5 +1089,3 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
CMDLET.exec = _run
|
||||
CMDLET.alias = ["add-rel"]
|
||||
CMDLET.register()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user