This commit is contained in:
2026-01-12 04:05:52 -08:00
parent 6076ea307b
commit 9981424397
11 changed files with 646 additions and 682 deletions

View File

@@ -188,13 +188,6 @@ class SharedArgs:
query_key="store",
)
PATH = CmdletArg(
name="path",
type="string",
choices=[], # Dynamically populated via get_store_choices()
description="selects store",
)
URL = CmdletArg(
name="url",
type="string",
@@ -206,7 +199,6 @@ class SharedArgs:
description="selects provider",
)
@staticmethod
@staticmethod
def get_store_choices(config: Optional[Dict[str, Any]] = None, force: bool = False) -> List[str]:
"""Get list of available store backend names.
@@ -765,6 +757,166 @@ def parse_cmdlet_args(args: Sequence[str],
return result
def resolve_target_dir(
parsed: Dict[str, Any],
config: Dict[str, Any],
*,
handle_creations: bool = True
) -> Optional[Path]:
"""Resolve a target directory from -path, -output, -storage, or config fallback.
Args:
parsed: Parsed cmdlet arguments dict.
config: System configuration dict.
handle_creations: Whether to create the directory if it doesn't exist.
Returns:
Path to the resolved directory, or None if invalid.
"""
# Priority 1: Explicit -path or -output
target = parsed.get("path") or parsed.get("output")
if target:
try:
p = Path(str(target)).expanduser().resolve()
if handle_creations:
p.mkdir(parents=True, exist_ok=True)
return p
except Exception as e:
log(f"Cannot use target path {target}: {e}", file=sys.stderr)
return None
# Priority 2: --storage flag
storage_val = parsed.get("storage")
if storage_val:
try:
return SharedArgs.resolve_storage(storage_val)
except Exception as e:
log(f"Invalid storage location: {e}", file=sys.stderr)
return None
# Priority 3: Config fallback via single source of truth
try:
from SYS.config import resolve_output_dir
out_dir = resolve_output_dir(config)
if handle_creations:
out_dir.mkdir(parents=True, exist_ok=True)
return out_dir
except Exception:
import tempfile
p = Path(tempfile.gettempdir()) / "Medios-Macina"
if handle_creations:
p.mkdir(parents=True, exist_ok=True)
return p
def coerce_to_path(value: Any) -> Path:
"""Extract a Path from common provider result shapes (Path, str, dict, object)."""
if isinstance(value, Path):
return value
if isinstance(value, str):
return Path(value)
# Try attribute
p = getattr(value, "path", None)
if p:
return Path(str(p))
# Try dict
if isinstance(value, dict):
p = value.get("path")
if p:
return Path(str(p))
raise ValueError(f"Cannot coerce {type(value).__name__} to Path (missing 'path' field)")
def resolve_media_kind_by_extension(path: Path) -> str:
"""Resolve media kind (audio, video, image, document, other) from file extension."""
if not isinstance(path, Path):
try:
path = Path(str(path))
except Exception:
return "other"
suffix = path.suffix.lower()
if suffix in {".mp3",
".flac",
".wav",
".m4a",
".aac",
".ogg",
".opus",
".wma",
".mka"}:
return "audio"
if suffix in {
".mp4",
".mkv",
".webm",
".mov",
".avi",
".flv",
".mpg",
".mpeg",
".ts",
".m4v",
".wmv",
}:
return "video"
if suffix in {".jpg",
".jpeg",
".png",
".gif",
".webp",
".bmp",
".tiff"}:
return "image"
if suffix in {".pdf",
".epub",
".txt",
".mobi",
".azw3",
".cbz",
".cbr",
".doc",
".docx"}:
return "document"
return "other"
def build_pipeline_preview(raw_urls: Sequence[str], piped_items: Sequence[Any]) -> List[str]:
"""Construct a short preview list for pipeline/cmdlet progress UI."""
preview: List[str] = []
# 1. Add raw URLs
try:
for u in (raw_urls or [])[:3]:
if u:
preview.append(str(u))
except Exception:
pass
# 2. Add titles from piped items
if len(preview) < 5:
try:
for item in (piped_items or [])[:5]:
if len(preview) >= 5:
break
title = get_field(item, "title") or get_field(item, "target") or "Piped item"
preview.append(str(title))
except Exception:
pass
# 3. Handle empty case
if not preview:
total = len(raw_urls or []) + len(piped_items or [])
if total:
preview.append(f"Processing {total} item(s)...")
return preview
def normalize_hash(hash_hex: Optional[str]) -> Optional[str]:
"""Normalize a hash string to lowercase, or return None if invalid.
@@ -2034,36 +2186,45 @@ def collapse_namespace_tag(
def extract_tag_from_result(result: Any) -> list[str]:
"""Extract all tags from a result dict or PipeObject.
Handles mixed types (lists, sets, strings) and various field names.
"""
tag: list[str] = []
def _extend(candidate: Any) -> None:
if not candidate:
return
if isinstance(candidate, (list, set, tuple)):
tag.extend(str(t) for t in candidate if t is not None)
elif isinstance(candidate, str):
tag.append(candidate)
if isinstance(result, models.PipeObject):
tag.extend(result.tag or [])
if isinstance(result.extra, dict):
extra_tag = result.extra.get("tag")
if isinstance(extra_tag, list):
tag.extend(extra_tag)
elif isinstance(extra_tag, str):
tag.append(extra_tag)
_extend(result.extra.get("tag"))
if isinstance(result.metadata, dict):
_extend(result.metadata.get("tag"))
_extend(result.metadata.get("tags"))
elif hasattr(result, "tag"):
# Handle objects with tag attribute (e.g. SearchResult)
val = getattr(result, "tag")
if isinstance(val, (list, set, tuple)):
tag.extend(val)
elif isinstance(val, str):
tag.append(val)
_extend(getattr(result, "tag"))
if isinstance(result, dict):
raw_tag = result.get("tag")
if isinstance(raw_tag, list):
tag.extend(raw_tag)
elif isinstance(raw_tag, str):
tag.append(raw_tag)
_extend(result.get("tag"))
_extend(result.get("tags"))
extra = result.get("extra")
if isinstance(extra, dict):
extra_tag = extra.get("tag")
if isinstance(extra_tag, list):
tag.extend(extra_tag)
elif isinstance(extra_tag, str):
tag.append(extra_tag)
_extend(extra.get("tag"))
_extend(extra.get("tags"))
fm = result.get("full_metadata") or result.get("metadata")
if isinstance(fm, dict):
_extend(fm.get("tag"))
_extend(fm.get("tags"))
return merge_sequences(tag, case_sensitive=True)
@@ -2079,6 +2240,11 @@ def extract_title_from_result(result: Any) -> Optional[str]:
def extract_url_from_result(result: Any) -> list[str]:
"""Extract all unique URLs from a result dict or PipeObject.
Handles mixed types (lists, strings) and various field names (url, source_url, webpage_url).
Centralizes extraction logic for cmdlets like download-file, add-file, get-url.
"""
url: list[str] = []
def _extend(candidate: Any) -> None:
@@ -2089,40 +2255,48 @@ def extract_url_from_result(result: Any) -> list[str]:
elif isinstance(candidate, str):
url.append(candidate)
# Priority 1: PipeObject (structured data)
if isinstance(result, models.PipeObject):
_extend(result.extra.get("url"))
_extend(result.extra.get("url")) # Also check singular url
_extend(result.url)
_extend(result.source_url)
# Also check extra and metadata for legacy or rich captures
if isinstance(result.extra, dict):
_extend(result.extra.get("url"))
_extend(result.extra.get("source_url"))
if isinstance(result.metadata, dict):
_extend(result.metadata.get("url"))
_extend(result.metadata.get("url"))
_extend(result.metadata.get("url"))
_extend(result.metadata.get("source_url"))
_extend(result.metadata.get("webpage_url"))
if isinstance(getattr(result, "full_metadata", None), dict):
fm = getattr(result, "full_metadata", None)
if isinstance(fm, dict):
_extend(fm.get("url"))
_extend(fm.get("url"))
_extend(fm.get("url"))
elif hasattr(result, "url") or hasattr(result, "url"):
# Handle objects with url/url attribute
_extend(getattr(result, "url", None))
_extend(getattr(result, "url", None))
_extend(fm.get("source_url"))
_extend(fm.get("webpage_url"))
# Priority 2: Generic objects with .url or .source_url attribute
elif hasattr(result, "url") or hasattr(result, "source_url"):
_extend(getattr(result, "url", None))
_extend(getattr(result, "source_url", None))
# Priority 3: Dictionary
if isinstance(result, dict):
_extend(result.get("url"))
_extend(result.get("url"))
_extend(result.get("url"))
fm = result.get("full_metadata")
if isinstance(fm, dict):
_extend(fm.get("url"))
_extend(fm.get("url"))
_extend(fm.get("url"))
_extend(result.get("source_url"))
_extend(result.get("webpage_url"))
extra = result.get("extra")
if isinstance(extra, dict):
_extend(extra.get("url"))
_extend(extra.get("url"))
_extend(extra.get("url"))
fm = result.get("full_metadata") or result.get("metadata")
if isinstance(fm, dict):
_extend(fm.get("url"))
_extend(fm.get("source_url"))
_extend(fm.get("webpage_url"))
return merge_sequences(url, case_sensitive=True)
from SYS.metadata import normalize_urls
return normalize_urls(url)
def extract_relationships(result: Any) -> Optional[Dict[str, Any]]: