from __future__ import annotations from pathlib import Path from typing import Any, Dict, List, Optional, Sequence, Tuple import sys from SYS.logger import log import pipeline as ctx from . import _shared as sh Cmdlet = sh.Cmdlet CmdletArg = sh.CmdletArg SharedArgs = sh.SharedArgs normalize_hash = sh.normalize_hash parse_cmdlet_args = sh.parse_cmdlet_args normalize_result_input = sh.normalize_result_input should_show_help = sh.should_show_help from Store import Store from SYS.utils import sha256_file class Add_Note(Cmdlet): def __init__(self) -> None: super().__init__( name="add-note", summary="Add file store note", usage="add-note -store [-query \"hash:\"] ", alias=[""], arg=[ SharedArgs.STORE, SharedArgs.QUERY, CmdletArg("name", type="string", required=True, description="The note name/key to set (e.g. 'comment', 'lyric')."), CmdletArg("text", type="string", required=True, description="Note text/content to store.", variadic=True), ], detail=[ """ dde """ ], exec=self.run, ) # Populate dynamic store choices for autocomplete try: SharedArgs.STORE.choices = SharedArgs.get_store_choices(None) except Exception: pass self.register() def _resolve_hash(self, raw_hash: Optional[str], raw_path: Optional[str], override_hash: Optional[str]) -> Optional[str]: resolved = normalize_hash(override_hash) if override_hash else normalize_hash(raw_hash) if resolved: return resolved if raw_path: try: p = Path(str(raw_path)) stem = p.stem if len(stem) == 64 and all(c in "0123456789abcdef" for c in stem.lower()): return stem.lower() if p.exists() and p.is_file(): return sha256_file(p) except Exception: return None return None def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: if should_show_help(args): log(f"Cmdlet: {self.name}\nSummary: {self.summary}\nUsage: {self.usage}") return 0 parsed = parse_cmdlet_args(args, self) store_override = parsed.get("store") query_hash = sh.parse_single_hash_query(parsed.get("query")) if parsed.get("query") and not query_hash: log("[add_note] Error: -query must be of the form hash:", file=sys.stderr) return 1 note_name = str(parsed.get("name") or "").strip() text_parts = parsed.get("text") if not note_name: log("[add_note] Error: Requires ", file=sys.stderr) return 1 if isinstance(text_parts, list): note_text = " ".join([str(p) for p in text_parts]).strip() else: note_text = str(text_parts or "").strip() # Note text can be omitted when upstream stages provide it (e.g. download-media --write-sub # attaches notes.sub). In that case we resolve per-item below. user_provided_text = bool(note_text) results = normalize_result_input(result) if not results: if store_override and query_hash: results = [{"store": str(store_override), "hash": query_hash}] else: log("[add_note] Error: Requires piped item(s) or -store and -query \"hash:\"", file=sys.stderr) return 1 store_registry = Store(config) updated = 0 # Batch write plan: store -> [(hash, name, text), ...] note_ops: Dict[str, List[Tuple[str, str, str]]] = {} # Optional global fallback for note text from pipeline values. # Allows patterns like: ... | add-note sub pipeline_default_text = None if not user_provided_text: try: pipeline_default_text = ctx.load_value(note_name) except Exception: pipeline_default_text = None if isinstance(pipeline_default_text, list): pipeline_default_text = " ".join([str(x) for x in pipeline_default_text]).strip() elif pipeline_default_text is not None: pipeline_default_text = str(pipeline_default_text).strip() for res in results: if not isinstance(res, dict): ctx.emit(res) continue # Resolve note text for this item when not provided explicitly. item_note_text = note_text if not user_provided_text: # Prefer item-scoped notes dict. candidate = None try: notes = res.get("notes") if isinstance(notes, dict): candidate = notes.get(note_name) except Exception: candidate = None # Also allow direct field fallback: res["sub"], etc. if candidate is None: try: candidate = res.get(note_name) except Exception: candidate = None if candidate is None: candidate = pipeline_default_text if isinstance(candidate, list): item_note_text = " ".join([str(x) for x in candidate]).strip() else: item_note_text = str(candidate or "").strip() if not item_note_text: log(f"[add_note] Warning: No note text found for '{note_name}'; skipping", file=sys.stderr) ctx.emit(res) continue store_name = str(store_override or res.get("store") or "").strip() raw_hash = res.get("hash") raw_path = res.get("path") if not store_name: log("[add_note] Error: Missing -store and item has no store field", file=sys.stderr) return 1 resolved_hash = self._resolve_hash( raw_hash=str(raw_hash) if raw_hash else None, raw_path=str(raw_path) if raw_path else None, override_hash=str(query_hash) if query_hash else None, ) if not resolved_hash: log("[add_note] Warning: Item missing usable hash; skipping", file=sys.stderr) ctx.emit(res) continue try: backend = store_registry[store_name] except Exception as exc: log(f"[add_note] Error: Unknown store '{store_name}': {exc}", file=sys.stderr) return 1 # Queue for bulk write per store. We still emit items immediately; # the pipeline only advances after this cmdlet returns. note_ops.setdefault(store_name, []).append((resolved_hash, note_name, item_note_text)) updated += 1 ctx.emit(res) # Execute bulk writes per store. wrote_any = False for store_name, ops in note_ops.items(): if not ops: continue try: backend = store_registry[store_name] except Exception: continue bulk_fn = getattr(backend, "set_note_bulk", None) if callable(bulk_fn): try: ok = bool(bulk_fn(list(ops), config=config)) wrote_any = wrote_any or ok or True ctx.print_if_visible(f"✓ add-note: {len(ops)} item(s) in '{store_name}'", file=sys.stderr) continue except Exception as exc: log(f"[add_note] Warning: bulk set_note failed for '{store_name}': {exc}; falling back", file=sys.stderr) # Fallback: per-item writes for file_hash, name, text in ops: try: ok = bool(backend.set_note(file_hash, name, text, config=config)) wrote_any = wrote_any or ok except Exception: continue log(f"[add_note] Updated {updated} item(s)", file=sys.stderr) return 0 if (updated > 0 and wrote_any) else (0 if updated > 0 else 1) CMDLET = Add_Note()