ff
This commit is contained in:
@@ -1156,7 +1156,7 @@ class AsyncHTTPClient:
|
||||
if 400 <= e.response.status_code < 500:
|
||||
try:
|
||||
response_text = e.response.text[:500]
|
||||
except:
|
||||
except Exception:
|
||||
response_text = "<unable to read response>"
|
||||
logger.error(
|
||||
f"HTTP {e.response.status_code} from {url}: {response_text}"
|
||||
|
||||
79
MPV/lyric.py
79
MPV/lyric.py
@@ -56,7 +56,7 @@ _LYRIC_VISIBLE_PROP = "user-data/medeia-lyric-visible"
|
||||
|
||||
# Optional overrides set by the playlist controller (.pipe/.mpv) so the lyric
|
||||
# helper can resolve notes even when the local file path cannot be mapped back
|
||||
# to a store via the store DB (common for Folder stores).
|
||||
# to a store via the store DB.
|
||||
_ITEM_STORE_PROP = "user-data/medeia-item-store"
|
||||
_ITEM_HASH_PROP = "user-data/medeia-item-hash"
|
||||
|
||||
@@ -804,11 +804,7 @@ def _resolve_store_backend_for_target(
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
# Prefer the inferred Folder store (fast), but still validate via get_file().
|
||||
preferred = _infer_store_for_target(target=target, config=config)
|
||||
if preferred and preferred in backend_names:
|
||||
backend_names.remove(preferred)
|
||||
backend_names.insert(0, preferred)
|
||||
|
||||
|
||||
for name in backend_names:
|
||||
try:
|
||||
@@ -842,80 +838,9 @@ def _resolve_store_backend_for_target(
|
||||
|
||||
return name, backend
|
||||
|
||||
# Fallback for Folder stores:
|
||||
# If the mpv target is inside a configured Folder store root and the filename
|
||||
# is hash-named, accept the inferred store even if the store DB doesn't map
|
||||
# hash->path (e.g. DB missing entry, external copy, etc.).
|
||||
try:
|
||||
inferred = _infer_store_for_target(target=target, config=config)
|
||||
if inferred and inferred in backend_names:
|
||||
backend = reg[inferred]
|
||||
if type(backend).__name__ == "Folder":
|
||||
p = Path(target)
|
||||
stem = str(p.stem or "").strip().lower()
|
||||
if stem and stem == str(file_hash or "").strip().lower():
|
||||
return inferred, backend
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def _infer_store_for_target(*, target: str, config: dict) -> Optional[str]:
|
||||
"""Infer store name from the current mpv target (local path under a folder root).
|
||||
|
||||
Note: URLs/streams are intentionally not mapped to stores for lyrics.
|
||||
"""
|
||||
if isinstance(target, str) and _is_stream_target(target):
|
||||
return None
|
||||
|
||||
try:
|
||||
from Store import Store as StoreRegistry
|
||||
|
||||
reg = StoreRegistry(config, suppress_debug=True)
|
||||
backends = [(name, reg[name]) for name in reg.list_backends()]
|
||||
except Exception:
|
||||
backends = []
|
||||
|
||||
# Local file path: choose the deepest Folder root that contains it.
|
||||
try:
|
||||
p = Path(target)
|
||||
if not p.exists() or not p.is_file():
|
||||
return None
|
||||
p_str = str(p.resolve()).lower()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
best: Optional[str] = None
|
||||
best_len = -1
|
||||
for name, backend in backends:
|
||||
if type(backend).__name__ != "Folder":
|
||||
continue
|
||||
root = None
|
||||
try:
|
||||
root = (
|
||||
getattr(backend,
|
||||
"_location",
|
||||
None) or getattr(backend,
|
||||
"location", lambda: None)()
|
||||
)
|
||||
except Exception:
|
||||
root = None
|
||||
if not root:
|
||||
continue
|
||||
try:
|
||||
root_path = Path(str(root)).expanduser().resolve()
|
||||
root_str = str(root_path).lower().rstrip("\\/")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if p_str.startswith(root_str) and len(root_str) > best_len:
|
||||
best = name
|
||||
best_len = len(root_str)
|
||||
|
||||
return best
|
||||
|
||||
|
||||
def _infer_hash_for_target(target: str) -> Optional[str]:
|
||||
"""Infer SHA256 hash from Hydrus URL query, hash-named local files, or by hashing local file content."""
|
||||
h = _extract_hash_from_target(target)
|
||||
|
||||
@@ -64,6 +64,7 @@ if _ROOT not in sys.path:
|
||||
from MPV.mpv_ipc import MPVIPCClient # noqa: E402
|
||||
from SYS.config import load_config # noqa: E402
|
||||
from SYS.logger import set_debug, debug, set_thread_stream # noqa: E402
|
||||
from SYS.utils import format_bytes # noqa: E402
|
||||
|
||||
REQUEST_PROP = "user-data/medeia-pipeline-request"
|
||||
RESPONSE_PROP = "user-data/medeia-pipeline-response"
|
||||
@@ -395,20 +396,8 @@ def _run_op(op: str, data: Any) -> Dict[str, Any]:
|
||||
ydl_opts["cookiefile"] = cookiefile
|
||||
|
||||
def _format_bytes(n: Any) -> str:
|
||||
try:
|
||||
v = float(n)
|
||||
except Exception:
|
||||
return ""
|
||||
if v <= 0:
|
||||
return ""
|
||||
units = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
while v >= 1024 and i < len(units) - 1:
|
||||
v /= 1024.0
|
||||
i += 1
|
||||
if i == 0:
|
||||
return f"{int(v)} {units[i]}"
|
||||
return f"{v:.1f} {units[i]}"
|
||||
"""Format bytes using centralized utility."""
|
||||
return format_bytes(n)
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[attr-defined]
|
||||
info = ydl.extract_info(url, download=False)
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ProviderCore.base import Provider, SearchResult
|
||||
from SYS.logger import log
|
||||
from SYS.utils import format_bytes
|
||||
|
||||
|
||||
def _get_podcastindex_credentials(config: Dict[str, Any]) -> Tuple[str, str]:
|
||||
@@ -79,23 +80,8 @@ class PodcastIndex(Provider):
|
||||
|
||||
@staticmethod
|
||||
def _format_bytes(value: Any) -> str:
|
||||
try:
|
||||
n = int(value)
|
||||
except Exception:
|
||||
return ""
|
||||
if n <= 0:
|
||||
return ""
|
||||
units = ["B", "KB", "MB", "GB", "TB"]
|
||||
size = float(n)
|
||||
unit = units[0]
|
||||
for u in units:
|
||||
unit = u
|
||||
if size < 1024.0 or u == units[-1]:
|
||||
break
|
||||
size /= 1024.0
|
||||
if unit == "B":
|
||||
return f"{int(size)}{unit}"
|
||||
return f"{size:.1f}{unit}"
|
||||
"""Format bytes using centralized utility."""
|
||||
return format_bytes(value)
|
||||
|
||||
@staticmethod
|
||||
def _format_date_from_epoch(value: Any) -> str:
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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__(
|
||||
|
||||
26
SYS/utils.py
26
SYS/utils.py
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Sequence, Set
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from SYS.logger import log
|
||||
from SYS.utils import extract_hydrus_hash_from_url
|
||||
|
||||
from SYS import pipeline as ctx
|
||||
from SYS.config import resolve_output_dir
|
||||
@@ -64,17 +65,8 @@ def _extract_url(item: Any) -> str:
|
||||
|
||||
|
||||
def _extract_hash_from_hydrus_file_url(url: str) -> str:
|
||||
try:
|
||||
parsed = urlparse(str(url))
|
||||
if not (parsed.path or "").endswith("/get_files/file"):
|
||||
return ""
|
||||
qs = parse_qs(parsed.query or "")
|
||||
h = (qs.get("hash") or [""])[0]
|
||||
if isinstance(h, str) and _SHA256_RE.fullmatch(h.strip()):
|
||||
return h.strip().lower()
|
||||
except Exception:
|
||||
pass
|
||||
return ""
|
||||
"""Extract hash from Hydrus URL using centralized utility."""
|
||||
return extract_hydrus_hash_from_url(url) or ""
|
||||
|
||||
|
||||
def _hydrus_instance_names(config: Dict[str, Any]) -> Set[str]:
|
||||
|
||||
@@ -43,7 +43,18 @@ class Get_Metadata(Cmdlet):
|
||||
|
||||
@staticmethod
|
||||
def _extract_imported_ts(meta: Dict[str, Any]) -> Optional[int]:
|
||||
"""Extract an imported timestamp from metadata if available."""
|
||||
"""Extract an imported timestamp from metadata if available.
|
||||
|
||||
Attempts to parse imported timestamp from metadata dict in multiple formats:
|
||||
- Numeric Unix timestamp (int/float)
|
||||
- ISO format string (e.g., "2024-01-15T10:30:00")
|
||||
|
||||
Args:
|
||||
meta: Metadata dictionary from backend (e.g., from get_metadata())
|
||||
|
||||
Returns:
|
||||
Unix timestamp as integer if found, None otherwise
|
||||
"""
|
||||
if not isinstance(meta, dict):
|
||||
return None
|
||||
|
||||
@@ -65,7 +76,17 @@ class Get_Metadata(Cmdlet):
|
||||
|
||||
@staticmethod
|
||||
def _format_imported(ts: Optional[int]) -> str:
|
||||
"""Format timestamp as readable string."""
|
||||
"""Format Unix timestamp as human-readable date string (UTC).
|
||||
|
||||
Converts Unix timestamp to YYYY-MM-DD HH:MM:SS format.
|
||||
Used for displaying file import dates to users.
|
||||
|
||||
Args:
|
||||
ts: Unix timestamp (integer) or None
|
||||
|
||||
Returns:
|
||||
Formatted date string (e.g., "2024-01-15 10:30:00") or empty string if invalid
|
||||
"""
|
||||
if not ts:
|
||||
return ""
|
||||
try:
|
||||
@@ -91,7 +112,30 @@ class Get_Metadata(Cmdlet):
|
||||
ext: Optional[str] = None,
|
||||
) -> Dict[str,
|
||||
Any]:
|
||||
"""Build a table row dict with metadata fields."""
|
||||
"""Build a normalized metadata row dict for display and piping.
|
||||
|
||||
Converts raw metadata fields into a standardized row format suitable for:
|
||||
- Display in result tables
|
||||
- Piping to downstream cmdlets
|
||||
- JSON serialization
|
||||
|
||||
Args:
|
||||
title: File or resource title
|
||||
store: Backend store name (e.g., "hydrus", "local")
|
||||
path: File path or resource identifier
|
||||
mime: MIME type (e.g., "image/jpeg", "video/mp4")
|
||||
size_bytes: File size in bytes
|
||||
dur_seconds: Duration in seconds (for video/audio)
|
||||
imported_ts: Unix timestamp when item was imported
|
||||
url: List of known URLs associated with file
|
||||
hash_value: File hash (SHA256 or other)
|
||||
pages: Number of pages (for PDFs)
|
||||
tag: List of tags applied to file
|
||||
ext: File extension (e.g., "jpg", "mp4")
|
||||
|
||||
Returns:
|
||||
Dictionary with normalized metadata fields and display columns
|
||||
"""
|
||||
size_mb = None
|
||||
size_int: Optional[int] = None
|
||||
if size_bytes is not None:
|
||||
@@ -151,7 +195,15 @@ class Get_Metadata(Cmdlet):
|
||||
|
||||
@staticmethod
|
||||
def _add_table_body_row(table: Table, row: Dict[str, Any]) -> None:
|
||||
"""Add a single row to the ResultTable using the prepared columns."""
|
||||
"""Add a single metadata row to the result table.
|
||||
|
||||
Extracts column values from row dict and adds to result table using
|
||||
standard column ordering (Hash, MIME, Size, Duration/Pages).
|
||||
|
||||
Args:
|
||||
table: Result table to add row to
|
||||
row: Metadata row dict (from _build_table_row)
|
||||
"""
|
||||
columns = row.get("columns") if isinstance(row, dict) else None
|
||||
lookup: Dict[str,
|
||||
Any] = {}
|
||||
@@ -173,7 +225,25 @@ class Get_Metadata(Cmdlet):
|
||||
row_obj.add_column("Duration(s)", "")
|
||||
|
||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Main execution entry point."""
|
||||
"""Execute get-metadata cmdlet - retrieve and display file metadata.
|
||||
|
||||
Queries a storage backend (Hydrus, local, etc.) for file metadata using hash.
|
||||
Extracts tags embedded in metadata response (avoiding duplicate API calls).
|
||||
Displays metadata in rich detail panel and result table.
|
||||
Allows piping (@N) to other cmdlets for chaining operations.
|
||||
|
||||
Optimizations:
|
||||
- Extracts tags from metadata response (no separate get_tag() call)
|
||||
- Single HTTP request to backends per file
|
||||
|
||||
Args:
|
||||
result: Piped input (dict with optional hash/store/title/tag fields)
|
||||
args: Command line arguments ([-query "hash:..."] [-store backend])
|
||||
config: Application configuration dict
|
||||
|
||||
Returns:
|
||||
0 on success, 1 on error (no metadata found, backend unavailable, etc.)
|
||||
"""
|
||||
# Parse arguments
|
||||
parsed = parse_cmdlet_args(args, self)
|
||||
|
||||
|
||||
@@ -318,8 +318,24 @@ def _emit_tags_as_table(
|
||||
) -> None:
|
||||
"""Emit tags as TagItem objects and display via ResultTable.
|
||||
|
||||
This replaces _print_tag_list to make tags pipe-able.
|
||||
Stores the table via ctx.set_last_result_table_overlay (or ctx.set_last_result_table) for downstream @ selection.
|
||||
Displays tags in a rich detail panel with file context (hash, title, URL, etc).
|
||||
Creates a table of individual tag items to allow selection and downstream piping.
|
||||
Preserves all metadata from subject (URLs, extensions, etc.) through to display.
|
||||
|
||||
Makes tags @-selectable via ctx.set_last_result_table() for chaining:
|
||||
- get-tag @1 | delete-tag (remove a specific tag)
|
||||
- get-tag @2 | add-url (add URL to tagged file)
|
||||
|
||||
Args:
|
||||
tags_list: List of tag strings to display
|
||||
file_hash: SHA256 hash of file
|
||||
store: Backend name (e.g., "hydrus", "local", "url")
|
||||
service_name: Tag service name (if from Hydrus)
|
||||
config: Application configuration
|
||||
item_title: Optional file title to display
|
||||
path: Optional file path
|
||||
subject: Full context object (should preserve original metadata)
|
||||
quiet: If True, don't display (emit-only mode)
|
||||
"""
|
||||
from SYS.result_table import ItemDetailView, extract_item_metadata
|
||||
|
||||
|
||||
@@ -389,7 +389,27 @@ class search_file(Cmdlet):
|
||||
|
||||
# --- Execution ------------------------------------------------------
|
||||
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
"""Search storage backends for files."""
|
||||
"""Search storage backends for files by various criteria.
|
||||
|
||||
Supports searching by:
|
||||
- Hash (-query "hash:...")
|
||||
- Title (-query "title:...")
|
||||
- Tag (-query "tag:...")
|
||||
- URL (-query "url:...")
|
||||
- Other backend-specific fields
|
||||
|
||||
Optimizations:
|
||||
- Extracts tags from metadata response (avoids duplicate API calls)
|
||||
- Only calls get_tag() separately for backends that don't include tags
|
||||
|
||||
Args:
|
||||
result: Piped input (typically empty for new search)
|
||||
args: Search criteria and options
|
||||
config: Application configuration
|
||||
|
||||
Returns:
|
||||
0 on success, 1 on error
|
||||
"""
|
||||
if should_show_help(args):
|
||||
log(f"Cmdlet: {self.name}\nSummary: {self.summary}\nUsage: {self.usage}")
|
||||
return 0
|
||||
@@ -698,20 +718,43 @@ class search_file(Cmdlet):
|
||||
except Exception:
|
||||
meta_obj = {}
|
||||
|
||||
# Extract tags from metadata response instead of separate get_tag() call
|
||||
# Metadata already includes tags if fetched with include_service_keys_to_tags=True
|
||||
tags_list: List[str] = []
|
||||
try:
|
||||
tag_result = resolved_backend.get_tag(h)
|
||||
if isinstance(tag_result, tuple) and tag_result:
|
||||
maybe_tags = tag_result[0]
|
||||
else:
|
||||
maybe_tags = tag_result
|
||||
if isinstance(maybe_tags, list):
|
||||
tags_list = [
|
||||
str(t).strip() for t in maybe_tags
|
||||
if isinstance(t, str) and str(t).strip()
|
||||
]
|
||||
except Exception:
|
||||
tags_list = []
|
||||
|
||||
# First try to extract from metadata tags dict
|
||||
metadata_tags = meta_obj.get("tags")
|
||||
if isinstance(metadata_tags, dict):
|
||||
for service_data in metadata_tags.values():
|
||||
if isinstance(service_data, dict):
|
||||
display_tags = service_data.get("display_tags", {})
|
||||
if isinstance(display_tags, dict):
|
||||
for tag_list in display_tags.values():
|
||||
if isinstance(tag_list, list):
|
||||
tags_list = [
|
||||
str(t).strip() for t in tag_list
|
||||
if isinstance(t, str) and str(t).strip()
|
||||
]
|
||||
break
|
||||
if tags_list:
|
||||
break
|
||||
|
||||
# Fallback: if metadata didn't include tags, call get_tag() separately
|
||||
# (This maintains compatibility with backends that don't include tags in metadata)
|
||||
if not tags_list:
|
||||
try:
|
||||
tag_result = resolved_backend.get_tag(h)
|
||||
if isinstance(tag_result, tuple) and tag_result:
|
||||
maybe_tags = tag_result[0]
|
||||
else:
|
||||
maybe_tags = tag_result
|
||||
if isinstance(maybe_tags, list):
|
||||
tags_list = [
|
||||
str(t).strip() for t in maybe_tags
|
||||
if isinstance(t, str) and str(t).strip()
|
||||
]
|
||||
except Exception:
|
||||
tags_list = []
|
||||
|
||||
title_from_tag: Optional[str] = None
|
||||
try:
|
||||
|
||||
@@ -12,6 +12,7 @@ from cmdlet._shared import Cmdlet, CmdletArg
|
||||
from SYS.config import load_config, save_config
|
||||
from SYS.logger import log, debug
|
||||
from SYS.result_table import Table
|
||||
from SYS.utils import extract_hydrus_hash_from_url
|
||||
from SYS import pipeline as ctx
|
||||
|
||||
_MATRIX_PENDING_ITEMS_KEY = "matrix_pending_items"
|
||||
@@ -543,17 +544,8 @@ def _extract_sha256_hex(item: Any) -> Optional[str]:
|
||||
|
||||
|
||||
def _extract_hash_from_hydrus_file_url(url: str) -> Optional[str]:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
if not (parsed.path or "").endswith("/get_files/file"):
|
||||
return None
|
||||
qs = parse_qs(parsed.query or "")
|
||||
h = (qs.get("hash") or [None])[0]
|
||||
if isinstance(h, str) and _SHA256_RE.fullmatch(h.strip()):
|
||||
return h.strip().lower()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
"""Extract hash from Hydrus URL using centralized utility."""
|
||||
return extract_hydrus_hash_from_url(url)
|
||||
|
||||
|
||||
def _maybe_download_hydrus_file(item: Any,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user