This commit is contained in:
nose
2025-12-11 19:04:02 -08:00
parent 6863c6c7ea
commit 16d8a763cd
103 changed files with 4759 additions and 9156 deletions

View File

@@ -1,7 +1,7 @@
"""Get tags from Hydrus or local sidecar metadata.
This cmdlet retrieves tags for a selected result, supporting both:
- Hydrus Network (for files with hash_hex)
- Hydrus Network (for files with hash)
- Local sidecar files (.tags)
In interactive mode: navigate with numbers, add/delete tags
@@ -12,15 +12,15 @@ from __future__ import annotations
import sys
from helper.logger import log, debug
from helper.metadata_search import get_metadata_provider, list_metadata_providers
from SYS.logger import log, debug
from Provider.metadata_provider import get_metadata_provider, list_metadata_providers
import subprocess
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple
import pipeline as ctx
from helper import hydrus
from helper.folder_store import read_sidecar, write_sidecar, find_sidecar, FolderDB
from API import HydrusNetwork
from API.folder import read_sidecar, write_sidecar, find_sidecar, API_folder_store
from ._shared import normalize_hash, looks_like_hash, Cmdlet, CmdletArg, SharedArgs, parse_cmdlet_args, get_field
from config import get_local_storage_path
@@ -47,15 +47,14 @@ class TagItem:
"""
tag_name: str
tag_index: int # 1-based index for user reference
hash_hex: Optional[str] = None
source: str = "hydrus"
hash: Optional[str] = None
store: str = "hydrus"
service_name: Optional[str] = None
file_path: Optional[str] = None
path: Optional[str] = None
def __post_init__(self):
# Make ResultTable happy by adding standard fields
# NOTE: Don't set 'title' - we want only the tag column in ResultTable
self.origin = self.source
self.detail = f"Tag #{self.tag_index}"
self.target = self.tag_name
self.media_kind = "tag"
@@ -65,20 +64,21 @@ class TagItem:
return {
"tag_name": self.tag_name,
"tag_index": self.tag_index,
"hash_hex": self.hash_hex,
"source": self.source,
"hash": self.hash,
"store": self.store,
"path": self.path,
"service_name": self.service_name,
}
def _emit_tags_as_table(
tags_list: List[str],
hash_hex: Optional[str],
source: str = "hydrus",
file_hash: Optional[str],
store: str = "hydrus",
service_name: Optional[str] = None,
config: Dict[str, Any] = None,
item_title: Optional[str] = None,
file_path: Optional[str] = None,
path: Optional[str] = None,
subject: Optional[Any] = None,
) -> None:
"""Emit tags as TagItem objects and display via ResultTable.
@@ -92,8 +92,8 @@ def _emit_tags_as_table(
table_title = "Tags"
if item_title:
table_title = f"Tags: {item_title}"
if hash_hex:
table_title += f" [{hash_hex[:8]}]"
if file_hash:
table_title += f" [{file_hash[:8]}]"
table = ResultTable(table_title, max_columns=1)
table.set_source_command("get-tag", [])
@@ -104,10 +104,10 @@ def _emit_tags_as_table(
tag_item = TagItem(
tag_name=tag_name,
tag_index=idx,
hash_hex=hash_hex,
source=source,
hash=file_hash,
store=store,
service_name=service_name,
file_path=file_path,
path=path,
)
tag_items.append(tag_item)
table.add_result(tag_item)
@@ -401,8 +401,8 @@ def _emit_tag_payload(source: str, tags_list: List[str], *, hash_value: Optional
tag_item = TagItem(
tag_name=tag_name,
tag_index=idx,
hash_hex=hash_value,
source=source,
hash=hash_value,
store=source,
service_name=None
)
ctx.emit(tag_item)
@@ -698,7 +698,7 @@ def _scrape_isbn_metadata(isbn: str) -> List[str]:
"""Scrape metadata for an ISBN using Open Library API."""
new_tags = []
try:
from ..helper.http_client import HTTPClient
from ..API.HTTP import HTTPClient
import json as json_module
isbn_clean = isbn.replace('-', '').strip()
@@ -822,7 +822,7 @@ def _scrape_openlibrary_metadata(olid: str) -> List[str]:
"""
new_tags = []
try:
from ..helper.http_client import HTTPClient
from ..API.HTTP import HTTPClient
import json as json_module
# Format: OL9674499M or just 9674499M
@@ -995,7 +995,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
get-tag -scrape <url|provider>
Options:
-hash <sha256>: Override hash to use instead of result's hash_hex
-hash <sha256>: Override hash to use instead of result's hash
--store <key>: Store result to this key for pipeline
--emit: Emit result without interactive prompt (quiet mode)
-scrape <url|provider>: Scrape metadata from URL or provider name (itunes, openlibrary, googlebooks)
@@ -1150,7 +1150,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
table = ResultTable(f"Metadata: {provider.name}")
table.set_source_command("get-tag", [])
selection_payload = []
hash_for_payload = normalize_hash(hash_override) or normalize_hash(get_field(result, "hash_hex", None))
hash_for_payload = normalize_hash(hash_override) or normalize_hash(get_field(result, "hash", None))
store_for_payload = get_field(result, "store", None)
for idx, item in enumerate(items):
tags = provider.to_tags(item)
row = table.add_row()
@@ -1165,13 +1166,12 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"artist": item.get("artist"),
"album": item.get("album"),
"year": item.get("year"),
"hash": hash_for_payload,
"store": store_for_payload,
"extra": {
"tags": tags,
"provider": provider.name,
"hydrus_hash": hash_for_payload,
"storage_source": get_field(result, "source", None) or get_field(result, "origin", None),
},
"file_hash": hash_for_payload,
}
selection_payload.append(payload)
table.set_row_selection_args(idx, [str(idx + 1)])
@@ -1192,30 +1192,29 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
if isinstance(result, list) and len(result) > 0:
result = result[0]
hash_from_result = normalize_hash(get_field(result, "hash_hex", None))
hash_hex = hash_override or hash_from_result
hash_from_result = normalize_hash(get_field(result, "hash", None))
file_hash = hash_override or hash_from_result
# Only use emit mode if explicitly requested with --emit flag, not just because we're in a pipeline
# This allows interactive REPL to work even in pipelines
emit_mode = emit_requested or bool(store_key)
store_label = (store_key.strip() if store_key and store_key.strip() else None)
# Get hash and store from result
file_hash = hash_hex
storage_source = get_field(result, "store") or get_field(result, "storage") or get_field(result, "origin")
store_name = get_field(result, "store")
if not file_hash:
log("No hash available in result", file=sys.stderr)
return 1
if not storage_source:
log("No storage backend specified in result", file=sys.stderr)
if not store_name:
log("No store specified in result", file=sys.stderr)
return 1
# Get tags using storage backend
try:
from helper.store import FileStorage
storage = FileStorage(config)
backend = storage[storage_source]
from Store import Store
storage = Store(config)
backend = storage[store_name]
current, source = backend.get_tag(file_hash, config=config)
if not current:
@@ -1224,7 +1223,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
service_name = ""
except KeyError:
log(f"Storage backend '{storage_source}' not found", file=sys.stderr)
log(f"Store '{store_name}' not found", file=sys.stderr)
return 1
except Exception as exc:
log(f"Failed to get tags: {exc}", file=sys.stderr)
@@ -1235,48 +1234,42 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
item_title = get_field(result, "title", None) or get_field(result, "name", None) or get_field(result, "filename", None)
# Build a subject payload representing the file whose tags are being shown
subject_origin = get_field(result, "origin", None) or get_field(result, "source", None) or source
subject_store = get_field(result, "store", None) or store_name
subject_payload: Dict[str, Any] = {
"tags": list(current),
"title": item_title,
"name": item_title,
"origin": subject_origin,
"source": subject_origin,
"storage_source": subject_origin,
"store": subject_store,
"service_name": service_name,
"extra": {
"tags": list(current),
"storage_source": subject_origin,
"hydrus_hash": hash_hex,
},
}
if hash_hex:
subject_payload.update({
"hash": hash_hex,
"hash_hex": hash_hex,
"file_hash": hash_hex,
"hydrus_hash": hash_hex,
})
if file_hash:
subject_payload["hash"] = file_hash
if local_path:
try:
path_text = str(local_path)
subject_payload.update({
"file_path": path_text,
"path": path_text,
"target": path_text,
})
subject_payload["extra"]["file_path"] = path_text
except Exception:
pass
if source == "hydrus":
_emit_tags_as_table(current, hash_hex=hash_hex, source="hydrus", service_name=service_name, config=config, item_title=item_title, subject=subject_payload)
else:
_emit_tags_as_table(current, hash_hex=hash_hex, source="local", service_name=None, config=config, item_title=item_title, file_path=str(local_path) if local_path else None, subject=subject_payload)
_emit_tags_as_table(
current,
file_hash=file_hash,
store=subject_store,
service_name=service_name if source == "hydrus" else None,
config=config,
item_title=item_title,
path=str(local_path) if local_path else None,
subject=subject_payload,
)
# If emit requested or store key provided, emit payload
if emit_mode:
_emit_tag_payload(source, current, hash_value=hash_hex, store_label=store_label)
_emit_tag_payload(source, current, hash_value=file_hash, store_label=store_label)
return 0
@@ -1341,22 +1334,22 @@ class Get_Tag(Cmdlet):
# Get hash and store from parsed args or result
hash_override = parsed.get("hash")
file_hash = hash_override or get_field(result, "hash") or get_field(result, "file_hash") or get_field(result, "hash_hex")
storage_source = parsed.get("store") or get_field(result, "store") or get_field(result, "storage") or get_field(result, "origin")
file_hash = normalize_hash(hash_override) or normalize_hash(get_field(result, "hash"))
store_name = parsed.get("store") or get_field(result, "store")
if not file_hash:
log("No hash available in result", file=sys.stderr)
return 1
if not storage_source:
log("No storage backend specified in result", file=sys.stderr)
if not store_name:
log("No store specified in result", file=sys.stderr)
return 1
# Get tags using storage backend
try:
from helper.store import FileStorage
storage_obj = FileStorage(config)
backend = storage_obj[storage_source]
from Store import Store
storage_obj = Store(config)
backend = storage_obj[store_name]
current, source = backend.get_tag(file_hash, config=config)
if not current:
@@ -1367,18 +1360,18 @@ class Get_Tag(Cmdlet):
item_title = get_field(result, "title") or file_hash[:16]
_emit_tags_as_table(
tags_list=current,
hash_hex=file_hash,
source=source,
file_hash=file_hash,
store=store_name,
service_name="",
config=config,
item_title=item_title,
file_path=None,
path=None,
subject=result,
)
return 0
except KeyError:
log(f"Storage backend '{storage_source}' not found", file=sys.stderr)
log(f"Store '{store_name}' not found", file=sys.stderr)
return 1
except Exception as exc:
log(f"Failed to get tags: {exc}", file=sys.stderr)