This commit is contained in:
2026-01-02 02:28:59 -08:00
parent deb05c0d44
commit 6e9a0c28ff
13 changed files with 1402 additions and 2334 deletions

View File

@@ -186,7 +186,7 @@ class SharedArgs:
name="path",
type="string",
choices=[], # Dynamically populated via get_store_choices()
description="Selects store",
description="selects store",
)
URL = CmdletArg(
@@ -194,6 +194,11 @@ class SharedArgs:
type="string",
description="http parser",
)
PROVIDER = CmdletArg(
name="provider",
type="string",
description="selects provider",
)
@staticmethod
def get_store_choices(config: Optional[Dict[str, Any]] = None) -> List[str]:

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,14 @@ from SYS.utils import sha256_file
class Add_Note(Cmdlet):
DEFAULT_QUERY_HINTS = (
"title:",
"text:",
"hash:",
"caption:",
"sub:",
"subtitle:",
)
def __init__(self) -> None:
super().__init__(
@@ -124,6 +132,45 @@ class Add_Note(Cmdlet):
note_text = text_match.group(1).strip() if text_match else ""
return (note_name or None, note_text or None)
@classmethod
def _looks_like_note_query_token(cls, token: Any) -> bool:
text = str(token or "").strip().lower()
if not text:
return False
return any(hint in text for hint in cls.DEFAULT_QUERY_HINTS)
@classmethod
def _default_query_args(cls, args: Sequence[str]) -> List[str]:
tokens: List[str] = list(args or [])
lower_tokens = {str(tok).lower() for tok in tokens if tok is not None}
if "-query" in lower_tokens or "--query" in lower_tokens:
return tokens
for idx, tok in enumerate(tokens):
token_text = str(tok or "")
if not token_text or token_text.startswith("-"):
continue
if not cls._looks_like_note_query_token(token_text):
continue
combined_parts = [token_text]
end = idx + 1
while end < len(tokens):
next_text = str(tokens[end] or "")
if not next_text or next_text.startswith("-"):
break
if not cls._looks_like_note_query_token(next_text):
break
combined_parts.append(next_text)
end += 1
combined_query = " ".join(combined_parts)
tokens[idx:end] = [combined_query]
tokens.insert(idx, "-query")
return tokens
return tokens
def _resolve_hash(
self,
raw_hash: Optional[str],
@@ -153,11 +200,14 @@ class Add_Note(Cmdlet):
log(f"Cmdlet: {self.name}\nSummary: {self.summary}\nUsage: {self.usage}")
return 0
parsed = parse_cmdlet_args(args, self)
parsed_args = self._default_query_args(args)
parsed = parse_cmdlet_args(parsed_args, self)
store_override = parsed.get("store")
hash_override = normalize_hash(parsed.get("hash"))
note_name, note_text = self._parse_note_query(str(parsed.get("query") or ""))
note_name = str(note_name or "").strip()
note_text = str(note_text or "").strip()
if not note_name or not note_text:
log(
"[add_note] Error: -query must include title:<title> and text:<text>",
@@ -173,7 +223,6 @@ class Add_Note(Cmdlet):
return 1
explicit_target = bool(hash_override and store_override)
results = normalize_result_input(result)
if results and explicit_target:
# Direct targeting mode: apply note once to the explicit target and
@@ -194,14 +243,22 @@ class Add_Note(Cmdlet):
f"✓ add-note: 1 item in '{store_override}'",
file=sys.stderr
)
log(
"[add_note] Updated 1/1 item(s)",
file=sys.stderr
)
for res in results:
ctx.emit(res)
return 0
log(
"[add_note] Warning: Note write reported failure",
file=sys.stderr
)
return 1
except Exception as exc:
log(f"[add_note] Error: Failed to set note: {exc}", file=sys.stderr)
return 1
for res in results:
ctx.emit(res)
return 0
if not results:
if explicit_target:
# Allow standalone use (no piped input) and enable piping the target forward.
@@ -217,7 +274,7 @@ class Add_Note(Cmdlet):
return 1
store_registry = Store(config)
updated = 0
planned_ops = 0
# Batch write plan: store -> [(hash, name, text), ...]
note_ops: Dict[str,
@@ -271,12 +328,12 @@ class Add_Note(Cmdlet):
[]).append((resolved_hash,
note_name,
item_note_text))
updated += 1
planned_ops += 1
ctx.emit(res)
# Execute bulk writes per store.
wrote_any = False
successful_writes = 0
for store_name, ops in note_ops.items():
if not ops:
continue
@@ -285,16 +342,23 @@ class Add_Note(Cmdlet):
except Exception:
continue
store_success = 0
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
if ok:
store_success += len(ops)
ctx.print_if_visible(
f"✓ add-note: {len(ops)} item(s) in '{store_name}'",
file=sys.stderr
)
successful_writes += store_success
continue
log(
f"[add_note] Warning: bulk set_note returned False for '{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",
@@ -305,12 +369,23 @@ class Add_Note(Cmdlet):
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
if ok:
store_success += 1
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)
if store_success:
successful_writes += store_success
ctx.print_if_visible(
f"✓ add-note: {store_success} item(s) in '{store_name}'",
file=sys.stderr
)
log(
f"[add_note] Updated {successful_writes}/{planned_ops} item(s)",
file=sys.stderr
)
return 0 if successful_writes > 0 else 1
CMDLET = Add_Note()

View File

@@ -33,6 +33,7 @@ from rich.prompt import Confirm
from tool.ytdlp import (
YtDlpTool,
_best_subtitle_sidecar,
_SUBTITLE_EXTS,
_download_with_timeout,
_format_chapters_note,
_read_text_file,
@@ -2413,7 +2414,7 @@ class Download_File(Cmdlet):
except Exception:
continue
try:
if p_path.suffix.lower() in _best_subtitle_sidecar.__defaults__[0]:
if p_path.suffix.lower() in _SUBTITLE_EXTS:
continue
except Exception:
pass
@@ -2936,6 +2937,223 @@ class Download_File(Cmdlet):
"media_kind": "video" if opts.mode == "video" else "audio",
}
@staticmethod
def download_streaming_url_as_pipe_objects(
url: str,
config: Dict[str, Any],
*,
mode_hint: Optional[str] = None,
ytdl_format_hint: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Download a yt-dlp-supported URL and return PipeObject-style dict(s).
This is a lightweight helper intended for cmdlets that need to expand streaming URLs
into local files without re-implementing yt-dlp glue.
"""
url_str = str(url or "").strip()
if not url_str:
return []
if not is_url_supported_by_ytdlp(url_str):
return []
try:
from SYS.config import resolve_output_dir
out_dir = resolve_output_dir(config)
if out_dir is None:
return []
except Exception:
return []
cookies_path = None
try:
cookie_candidate = YtDlpTool(config).resolve_cookiefile()
if cookie_candidate is not None and cookie_candidate.is_file():
cookies_path = cookie_candidate
except Exception:
cookies_path = None
quiet_download = False
try:
quiet_download = bool((config or {}).get("_quiet_background_output"))
except Exception:
quiet_download = False
mode = str(mode_hint or "").strip().lower() if mode_hint else ""
if mode not in {"audio", "video"}:
mode = "video"
try:
cf = (
str(cookies_path)
if cookies_path is not None and cookies_path.is_file() else None
)
fmts_probe = list_formats(
url_str,
no_playlist=False,
playlist_items=None,
cookiefile=cf,
)
if isinstance(fmts_probe, list) and fmts_probe:
has_video = False
for f in fmts_probe:
if not isinstance(f, dict):
continue
vcodec = str(f.get("vcodec", "none") or "none").strip().lower()
if vcodec and vcodec != "none":
has_video = True
break
mode = "video" if has_video else "audio"
except Exception:
mode = "video"
fmt_hint = str(ytdl_format_hint).strip() if ytdl_format_hint else ""
chosen_format: Optional[str]
if fmt_hint:
chosen_format = fmt_hint
else:
chosen_format = None
if mode == "audio":
chosen_format = "bestaudio/best"
opts = DownloadOptions(
url=url_str,
mode=mode,
output_dir=Path(out_dir),
cookies_path=cookies_path,
ytdl_format=chosen_format,
quiet=quiet_download,
embed_chapters=True,
write_sub=True,
)
try:
result_obj = _download_with_timeout(opts, timeout_seconds=300)
except Exception as exc:
log(f"[download-file] Download failed for {url_str}: {exc}", file=sys.stderr)
return []
results: List[Any]
if isinstance(result_obj, list):
results = list(result_obj)
else:
paths = getattr(result_obj, "paths", None)
if isinstance(paths, list) and paths:
results = []
for p in paths:
try:
p_path = Path(p)
except Exception:
continue
if not p_path.exists() or p_path.is_dir():
continue
try:
hv = sha256_file(p_path)
except Exception:
hv = None
try:
results.append(
DownloadMediaResult(
path=p_path,
info=getattr(result_obj, "info", {}) or {},
tag=list(getattr(result_obj, "tag", []) or []),
source_url=getattr(result_obj, "source_url", None) or url_str,
hash_value=hv,
)
)
except Exception:
continue
else:
results = [result_obj]
out: List[Dict[str, Any]] = []
for downloaded in results:
try:
info = (
downloaded.info
if isinstance(getattr(downloaded, "info", None), dict) else {}
)
except Exception:
info = {}
try:
media_path = Path(str(getattr(downloaded, "path", "") or ""))
except Exception:
continue
if not media_path.exists() or media_path.is_dir():
continue
try:
hash_value = getattr(downloaded, "hash_value", None) or sha256_file(media_path)
except Exception:
hash_value = None
title = None
try:
title = info.get("title")
except Exception:
title = None
title = title or media_path.stem
tags = list(getattr(downloaded, "tag", []) or [])
if title and f"title:{title}" not in tags:
tags.insert(0, f"title:{title}")
final_url = None
try:
page_url = info.get("webpage_url") or info.get("original_url") or info.get("url")
if page_url:
final_url = str(page_url)
except Exception:
final_url = None
if not final_url:
final_url = url_str
po: Dict[str, Any] = {
"path": str(media_path),
"hash": hash_value,
"title": title,
"url": final_url,
"tag": tags,
"action": "cmdlet:download-file",
"is_temp": True,
"ytdl_format": getattr(opts, "ytdl_format", None),
"store": getattr(opts, "storage_name", None) or getattr(opts, "storage_location", None) or "PATH",
"media_kind": "video" if opts.mode == "video" else "audio",
}
try:
chapters_text = _format_chapters_note(info)
except Exception:
chapters_text = None
if chapters_text:
notes = po.get("notes")
if not isinstance(notes, dict):
notes = {}
notes.setdefault("chapters", chapters_text)
po["notes"] = notes
try:
sub_path = _best_subtitle_sidecar(media_path)
except Exception:
sub_path = None
if sub_path is not None:
sub_text = _read_text_file(sub_path)
if sub_text:
notes = po.get("notes")
if not isinstance(notes, dict):
notes = {}
notes["sub"] = sub_text
po["notes"] = notes
try:
sub_path.unlink()
except Exception:
pass
out.append(po)
return out
@staticmethod
def _normalise_hash_hex(value: Optional[str]) -> Optional[str]:
if not value or not isinstance(value, str):

View File

@@ -191,7 +191,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
mode_hint = None
forced_format = None
from cmdlet.add_file import Add_File
from cmdlet.download_file import Download_File
expanded: List[Dict[str, Any]] = []
downloaded_any = False
@@ -204,7 +204,7 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
expanded.append(it)
continue
downloaded = Add_File._download_streaming_url_as_pipe_objects(
downloaded = Download_File.download_streaming_url_as_pipe_objects(
u,
config,
mode_hint=mode_hint,