upk
This commit is contained in:
@@ -14,22 +14,25 @@ from . import register
|
||||
import models
|
||||
import pipeline as ctx
|
||||
from helper import hydrus as hydrus_wrapper
|
||||
from ._shared import Cmdlet, CmdletArg, parse_cmdlet_args
|
||||
from ._shared import Cmdlet, CmdletArg, parse_cmdlet_args, normalize_result_input
|
||||
from helper.local_library import read_sidecar, find_sidecar
|
||||
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name="add-relationship",
|
||||
summary="Associate file relationships (king/alt/related) in Hydrus based on relationship tags in sidecar.",
|
||||
usage="add-relationship OR add-relationship -path <file>",
|
||||
usage="@1-3 | add-relationship -king @4 OR add-relationship -path <file> OR @1,@2,@3 | add-relationship",
|
||||
args=[
|
||||
CmdletArg("path", type="string", description="Specify the local file path (if not piping a result)."),
|
||||
CmdletArg("-king", type="string", description="Explicitly set the king hash/file for relationships (e.g., -king @4 or -king hash)"),
|
||||
CmdletArg("-type", type="string", description="Relationship type for piped items (default: 'alt', options: 'king', 'alt', 'related')"),
|
||||
],
|
||||
details=[
|
||||
"- Reads relationship tags from sidecar (format: 'relationship: hash(king)<HASH>,hash(alt)<HASH>,hash(related)<HASH>')",
|
||||
"- Calls Hydrus API to associate the hashes as relationships",
|
||||
"- Mode 1: Pipe multiple items, first becomes king, rest become alts (default)",
|
||||
"- Mode 2: Use -king to explicitly set which item/hash is the king: @1-3 | add-relationship -king @4",
|
||||
"- Mode 3: Read relationships from sidecar (format: 'relationship: hash(king)<HASH>,hash(alt)<HASH>...')",
|
||||
"- Supports three relationship types: king (primary), alt (alternative), related (other versions)",
|
||||
"- Works with piped file results or -path argument for direct invocation",
|
||||
"- When using -king, all piped items become the specified relationship type to the king",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -67,6 +70,81 @@ def _extract_relationships_from_tag(tag_value: str) -> Dict[str, list[str]]:
|
||||
return result
|
||||
|
||||
|
||||
def _resolve_king_reference(king_arg: str) -> Optional[str]:
|
||||
"""Resolve a king reference like '@4' to its actual hash or path.
|
||||
|
||||
Supports:
|
||||
- Direct hash: '0123456789abcdef...' (64 chars)
|
||||
- Selection reference: '@4' (resolves from pipeline context)
|
||||
|
||||
Returns:
|
||||
- For Hydrus items: normalized hash
|
||||
- For local storage items: file path
|
||||
- None if not found
|
||||
"""
|
||||
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 @N selection from pipeline context
|
||||
if king_arg.startswith('@'):
|
||||
try:
|
||||
# Get the result items from the pipeline context
|
||||
from pipeline import get_last_result_items
|
||||
items = get_last_result_items()
|
||||
if not items:
|
||||
log(f"Cannot resolve {king_arg}: no search results in context", file=sys.stderr)
|
||||
return None
|
||||
|
||||
# Parse @N to get the index (1-based)
|
||||
index_str = king_arg[1:] # Remove '@'
|
||||
index = int(index_str) - 1 # Convert to 0-based
|
||||
|
||||
if 0 <= index < len(items):
|
||||
item = items[index]
|
||||
|
||||
# Try to extract hash from the item (could be dict or object)
|
||||
item_hash = None
|
||||
if isinstance(item, dict):
|
||||
# Dictionary: try common hash field names
|
||||
item_hash = item.get('hash_hex') or item.get('hash') or item.get('file_hash')
|
||||
else:
|
||||
# Object: use getattr
|
||||
item_hash = getattr(item, 'hash_hex', None) or getattr(item, 'hash', None)
|
||||
|
||||
if item_hash:
|
||||
normalized = _normalise_hash_hex(item_hash)
|
||||
if normalized:
|
||||
return normalized
|
||||
|
||||
# If no hash, try to get file path (for local storage)
|
||||
file_path = None
|
||||
if isinstance(item, dict):
|
||||
# Dictionary: try common path field names
|
||||
file_path = item.get('file_path') or item.get('path') or item.get('target')
|
||||
else:
|
||||
# Object: use getattr
|
||||
file_path = getattr(item, 'file_path', None) or getattr(item, 'path', None) or getattr(item, 'target', None)
|
||||
|
||||
if file_path:
|
||||
return str(file_path)
|
||||
|
||||
log(f"Item {king_arg} has no hash or path information", file=sys.stderr)
|
||||
return None
|
||||
else:
|
||||
log(f"Index {king_arg} out of range", file=sys.stderr)
|
||||
return None
|
||||
except (ValueError, IndexError) as e:
|
||||
log(f"Cannot resolve {king_arg}: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@register(["add-relationship", "add-rel"]) # primary name and alias
|
||||
def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Associate file relationships in Hydrus.
|
||||
@@ -88,6 +166,9 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# Parse arguments using CMDLET spec
|
||||
parsed = parse_cmdlet_args(_args, CMDLET)
|
||||
arg_path: Optional[Path] = None
|
||||
king_arg = parsed.get("king") # New: explicit king argument
|
||||
rel_type = parsed.get("type", "alt") # New: relationship type (default: alt)
|
||||
|
||||
if parsed:
|
||||
# Get the first arg value (e.g., -path)
|
||||
first_arg_name = CMDLET.get("args", [{}])[0].get("name") if CMDLET.get("args") else None
|
||||
@@ -98,62 +179,160 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
except Exception:
|
||||
arg_path = Path(str(arg_value))
|
||||
|
||||
# Get Hydrus client
|
||||
try:
|
||||
client = hydrus_wrapper.get_client(config)
|
||||
except Exception as exc:
|
||||
log(f"Hydrus client unavailable: {exc}", file=sys.stderr)
|
||||
# Handle @N selection which creates a list
|
||||
# Use normalize_result_input to handle both single items and lists
|
||||
items_to_process = normalize_result_input(result)
|
||||
|
||||
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 client is None:
|
||||
log("Hydrus client unavailable", 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}]
|
||||
|
||||
# Handle @N selection which creates a list - extract the first item
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
result = result[0]
|
||||
|
||||
# Check if we're in pipeline mode (have a hash) or file mode
|
||||
file_hash = getattr(result, "hash_hex", None)
|
||||
# Import local storage utilities
|
||||
from helper.local_library import LocalLibrarySearchOptimizer
|
||||
from config import get_local_storage_path
|
||||
|
||||
# PIPELINE MODE: Track relationships across multiple items
|
||||
if file_hash:
|
||||
file_hash = _normalise_hash_hex(file_hash)
|
||||
if not file_hash:
|
||||
log("Invalid file hash format", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Load or initialize king hash from pipeline context
|
||||
local_storage_path = get_local_storage_path(config) if config else None
|
||||
|
||||
# Check if any items have Hydrus hashes (file_hash or hash_hex fields)
|
||||
has_hydrus_hashes = any(
|
||||
(isinstance(item, dict) and (item.get('hash_hex') or item.get('hash')))
|
||||
or (hasattr(item, 'hash_hex') or hasattr(item, 'hash'))
|
||||
for item in items_to_process
|
||||
)
|
||||
|
||||
# Only try to initialize Hydrus if we actually have Hydrus hashes to work with
|
||||
hydrus_client = None
|
||||
if has_hydrus_hashes:
|
||||
try:
|
||||
king_hash = ctx.load_value("relationship_king")
|
||||
except Exception:
|
||||
king_hash = None
|
||||
|
||||
# If this is the first item, make it the king
|
||||
if not king_hash:
|
||||
try:
|
||||
ctx.store_value("relationship_king", file_hash)
|
||||
log(f"Established king hash: {file_hash}", file=sys.stderr)
|
||||
return 0 # First item just becomes the king, no relationships yet
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If we already have a king and this is a different hash, link them
|
||||
if king_hash and king_hash != file_hash:
|
||||
try:
|
||||
client.set_relationship(file_hash, king_hash, "alt")
|
||||
log(
|
||||
f"[add-relationship] Set alt relationship: {file_hash} <-> {king_hash}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 0
|
||||
except Exception as exc:
|
||||
log(f"Failed to set relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
hydrus_client = hydrus_wrapper.get_client(config)
|
||||
except Exception as exc:
|
||||
log(f"Hydrus unavailable, will use local storage: {exc}", file=sys.stderr)
|
||||
|
||||
# FILE MODE: Read relationships from sidecar
|
||||
# Use local storage if it's available and either Hydrus is not available or items are local files
|
||||
use_local_storage = local_storage_path and (not has_hydrus_hashes or (arg_path and arg_path.exists()))
|
||||
|
||||
# Resolve the king reference once (if provided)
|
||||
king_hash = None
|
||||
if king_arg:
|
||||
# Resolve the king reference (could be @4 or a direct hash)
|
||||
king_hash = _resolve_king_reference(king_arg)
|
||||
if not king_hash:
|
||||
log(f"Failed to resolve king argument: {king_arg}", file=sys.stderr)
|
||||
return 1
|
||||
log(f"Using king hash: {king_hash}", file=sys.stderr)
|
||||
|
||||
# Process each item in the list
|
||||
for item_idx, item in enumerate(items_to_process):
|
||||
# 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)
|
||||
|
||||
# PIPELINE MODE with Hydrus: Track relationships using hash
|
||||
if file_hash and hydrus_client:
|
||||
file_hash = _normalise_hash_hex(file_hash)
|
||||
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
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"Failed to set relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
# Original behavior: no explicit king, first becomes king, rest become alts
|
||||
try:
|
||||
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:
|
||||
ctx.store_value("relationship_king", file_hash)
|
||||
log(f"Established king hash: {file_hash}", file=sys.stderr)
|
||||
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
|
||||
)
|
||||
except Exception as exc:
|
||||
log(f"Failed to set relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# LOCAL STORAGE MODE: Handle relationships for local files
|
||||
elif use_local_storage and file_path_from_result:
|
||||
try:
|
||||
file_path_obj = Path(str(file_path_from_result))
|
||||
|
||||
if not file_path_obj.exists():
|
||||
log(f"File not found: {file_path_obj}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if king_hash:
|
||||
# king_hash is a file path from _resolve_king_reference (or a Hydrus hash)
|
||||
king_file_path = Path(str(king_hash)) if king_hash else None
|
||||
if king_file_path and king_file_path.exists():
|
||||
with LocalLibrarySearchOptimizer(local_storage_path) as db:
|
||||
db.set_relationship(file_path_obj, king_file_path, rel_type)
|
||||
log(f"Set {rel_type} relationship: {file_path_obj.name} -> {king_file_path.name}", file=sys.stderr)
|
||||
else:
|
||||
log(f"King file not found or invalid: {king_hash}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
# Original behavior: first becomes king, rest become alts
|
||||
try:
|
||||
king_path = ctx.load_value("relationship_king_path")
|
||||
except Exception:
|
||||
king_path = None
|
||||
|
||||
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)
|
||||
continue # Move to next item
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if king_path and king_path != str(file_path_obj):
|
||||
try:
|
||||
with LocalLibrarySearchOptimizer(local_storage_path) as db:
|
||||
db.set_relationship(file_path_obj, Path(king_path), rel_type)
|
||||
log(f"Set {rel_type} relationship: {file_path_obj.name} -> {Path(king_path).name}", file=sys.stderr)
|
||||
except Exception as exc:
|
||||
log(f"Failed to set relationship: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as exc:
|
||||
log(f"Local storage error: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
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)
|
||||
|
||||
# Resolve media path from -path arg or result target
|
||||
@@ -235,7 +414,7 @@ def _run(result: Any, _args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
continue
|
||||
|
||||
try:
|
||||
client.set_relationship(file_hash, related_hash, rel_type)
|
||||
hydrus_client.set_relationship(file_hash, related_hash, rel_type)
|
||||
log(
|
||||
f"[add-relationship] Set {rel_type} relationship: "
|
||||
f"{file_hash} <-> {related_hash}",
|
||||
|
||||
Reference in New Issue
Block a user