This commit is contained in:
2026-02-02 19:49:07 -08:00
parent 8d22ec5a81
commit 1e0000ae19
13 changed files with 297 additions and 988 deletions

View File

@@ -584,8 +584,20 @@ def set_last_result_table(
items: Optional[List[Any]] = None,
subject: Optional[Any] = None
) -> None:
"""
Store the last result table and items for @ selection syntax.
"""Store the last result table and items for @ selection syntax.
Persists result table and items across command invocations, enabling
subsequent commands to reference and operate on previous results using @N syntax.
Example:
search-file hash:<...> # Returns table with 3 results
@1 | get-metadata # Gets metadata for result #1
@2 | add-tag foo # Adds tag to result #2
Args:
result_table: Table object with results (can be None to clear)
items: List of item objects corresponding to table rows
subject: Optional context object (first item or full list)
"""
state = _get_pipeline_state()
@@ -662,6 +674,16 @@ def set_last_result_table_overlay(
Used by action cmdlets (get-metadata, get-tag, get-url) to display detail
panels or filtered results without disrupting the primary search-result history.
Difference from set_last_result_table():
- Overlay tables are transient (in-process memory only)
- Don't persist across command invocations
- Used for "live" displays that shouldn't be part of @N selection
Args:
result_table: Table object with transient results
items: List of item objects (not persisted)
subject: Optional context object
"""
state = _get_pipeline_state()
state.display_table = result_table
@@ -833,8 +855,17 @@ def get_last_result_table() -> Optional[Any]:
def get_last_result_items() -> List[Any]:
"""
Get the items available for @N selection.
"""Get the items available for @N selection in current pipeline context.
Returns items in priority order:
1. Display items (from get-tag, get-metadata, etc.) if display table is selectable
2. Last result items (from search-file, etc.) if last result table is selectable
3. Empty list if no selectable tables available
Used to resolve @1, @2, etc. in commands.
Returns:
List of items that can be selected via @N syntax
"""
state = _get_pipeline_state()
# Prioritize items from display commands (get-tag, delete-tag, etc.)

View File

@@ -136,7 +136,19 @@ def _get_first_dict_value(data: Dict[str, Any], keys: List[str]) -> Any:
def _as_dict(item: Any) -> Optional[Dict[str, Any]]:
if isinstance(item, dict):
"""Convert any object to dictionary representation.
Handles:
- Dict objects (returned as-is)
- Objects with __dict__ attribute (converted to dict)
- None or conversion failures (returns None)
Args:
item: Object to convert (dict, dataclass, object, etc.)
Returns:
Dictionary representation or None if conversion fails
"""
return item
try:
if hasattr(item, "__dict__"):
@@ -148,7 +160,17 @@ def _as_dict(item: Any) -> Optional[Dict[str, Any]]:
def extract_store_value(item: Any) -> str:
data = _as_dict(item) or {}
"""Extract storage backend name from item.
Searches item for store identifier using multiple field names:
store, table, source, storage (legacy).
Args:
item: Object or dict with store information
Returns:
Store name as string (e.g., "hydrus", "local", "") if not found
"""
store = _get_first_dict_value(
data,
["store",
@@ -1959,7 +1981,33 @@ def format_result(result: Any, title: str = "") -> str:
def extract_item_metadata(item: Any) -> Dict[str, Any]:
"""Extract a comprehensive set of metadata from an item for the ItemDetailView.
Now supports SYS.result_table_api.ResultModel as a first-class input.
Converts items (ResultModel, dicts, objects) into normalized metadata dict.
Extracts all relevant fields for display: Title, Hash, Store, Path, Ext, Size,
Duration, URL, Relations, Tags.
Optimization:
- Calls _as_dict() only once and reuses throughout
- Handles both ResultModel objects and legacy dicts/objects
Example output:
{
"Title": "video.mp4",
"Hash": "abc123def456...",
"Store": "hydrus",
"Path": "/mnt/media/video.mp4",
"Ext": "mp4",
"Size": "1.2 GB",
"Duration": "1h23m",
"Url": "https://example.com/video.mp4",
"Relations": <null>,
"Tags": "movie, comedy"
}
Args:
item: Object to extract metadata from (ResultModel, dict, or any object)
Returns:
Dictionary with standardized metadata fields (empty dict if None input)
"""
if item is None:
return {}
@@ -1996,13 +2044,15 @@ def extract_item_metadata(item: Any) -> Dict[str, Any]:
return out
# Fallback to existing extraction logic for legacy objects/dicts
# Convert once and reuse throughout to avoid repeated _as_dict() calls
data = _as_dict(item) or {}
# Use existing extractors from match-standard result table columns
title = extract_title_value(item)
if title:
out["Title"] = title
else:
# Fallback for raw dicts
data = _as_dict(item) or {}
t = data.get("title") or data.get("name") or data.get("TITLE")
if t: out["Title"] = t
@@ -2013,7 +2063,6 @@ def extract_item_metadata(item: Any) -> Dict[str, Any]:
if store: out["Store"] = store
# Path/Target
data = _as_dict(item) or {}
path = data.get("path") or data.get("target") or data.get("filename")
if path: out["Path"] = path
@@ -2066,6 +2115,23 @@ class ItemDetailView(Table):
This is used for 'get-tag', 'get-url' and similar cmdlets where we want to contextually show
what is being operated on (the main item) along with the selection list.
Display structure:
┌─ Item Details Panel ─────────────────────────────┐
│ Title: video.mp4 │
│ Hash: abc123def456789... │
│ Store: hydrus │
│ Path: /media/video.mp4 │
│ Ext: mp4 │
│ Url: https://example.com/video.mp4 │
└────────────────────────────────────────────────────┘
# TAGS Value
1 .jpg
2 .png
3 .webp
Used by action cmdlets that operate on an item and show its related details.
"""
def __init__(

View File

@@ -552,6 +552,32 @@ def add_direct_link_to_result(
setattr(result, "original_link", original_link)
def extract_hydrus_hash_from_url(url: str) -> str | None:
"""Extract SHA256 hash from Hydrus API URL.
Handles URLs like:
- http://localhost:45869/get_files/file?hash=abc123...
- URLs with &hash=abc123...
Args:
url: URL string to extract hash from
Returns:
Hash hex string (lowercase, 64 chars) if valid SHA256, None otherwise
"""
try:
import re
match = re.search(r"[?&]hash=([0-9a-fA-F]+)", str(url or ""))
if match:
hash_hex = match.group(1).strip().lower()
# Validate SHA256 (exactly 64 hex chars)
if re.fullmatch(r"[0-9a-f]{64}", hash_hex):
return hash_hex
except Exception:
pass
return None
# ============================================================================
# URL Policy Resolution - Consolidated from url_parser.py
# ============================================================================