j
This commit is contained in:
@@ -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]:
|
||||
|
||||
2025
cmdlet/add_file.py
2025
cmdlet/add_file.py
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user