re
Some checks failed
smoke-mm / Install & smoke test mm --help (push) Has been cancelled

This commit is contained in:
nose
2025-12-25 04:49:22 -08:00
parent 2542a68479
commit 43afa4e3fa
19 changed files with 2766 additions and 234 deletions

195
CLI.py
View File

@@ -1710,6 +1710,51 @@ class PipelineExecutor:
return False
@staticmethod
def _maybe_open_url_selection(current_table: Any, selected_items: list, *, stage_is_last: bool) -> bool:
if not stage_is_last:
return False
if not selected_items or len(selected_items) != 1:
return False
table_type = ""
source_cmd = ""
try:
table_type = str(getattr(current_table, "table", "") or "").strip().lower()
except Exception:
table_type = ""
try:
source_cmd = str(getattr(current_table, "source_command", "") or "").strip().replace("_", "-").lower()
except Exception:
source_cmd = ""
if table_type != "url" and source_cmd != "get-url":
return False
item = selected_items[0]
url = None
try:
from cmdlet._shared import get_field
url = get_field(item, "url")
except Exception:
try:
url = item.get("url") if isinstance(item, dict) else getattr(item, "url", None)
except Exception:
url = None
url_text = str(url or "").strip()
if not url_text:
return False
try:
import webbrowser
webbrowser.open(url_text, new=2)
return True
except Exception:
return False
def _maybe_enable_background_notifier(self, worker_manager: Any, config: Any, pipeline_session: Any) -> None:
if not (pipeline_session and worker_manager and isinstance(config, dict)):
return
@@ -1798,12 +1843,15 @@ class PipelineExecutor:
else:
selected_row_args: List[str] = []
skip_pipe_expansion = source_cmd == ".pipe" and len(stages) > 0
if source_cmd and not skip_pipe_expansion:
for idx in selection_indices:
row_args = ctx.get_current_stage_table_row_selection_args(idx)
if row_args:
selected_row_args.extend(row_args)
break
# Only perform @N command expansion for *single-item* selections.
# For multi-item selections (e.g. @*, @1-5), expanding to a single
# row would silently drop items. In those cases we pipe the selected
# items downstream instead.
if source_cmd and not skip_pipe_expansion and len(selection_indices) == 1:
idx = selection_indices[0]
row_args = ctx.get_current_stage_table_row_selection_args(idx)
if row_args:
selected_row_args.extend(row_args)
if selected_row_args:
if isinstance(source_cmd, list):
@@ -1834,30 +1882,42 @@ class PipelineExecutor:
command_expanded = True
if (not command_expanded) and selection_indices:
last_piped_items = None
try:
last_piped_items = ctx.get_last_result_items()
except Exception:
last_piped_items = None
stage_table = None
try:
stage_table = ctx.get_current_stage_table()
except Exception:
stage_table = None
if not stage_table and hasattr(ctx, "get_display_table"):
try:
stage_table = ctx.get_display_table()
except Exception:
stage_table = None
display_table = None
try:
display_table = ctx.get_display_table() if hasattr(ctx, "get_display_table") else None
except Exception:
display_table = None
if not stage_table and display_table is not None:
stage_table = display_table
if not stage_table:
try:
stage_table = ctx.get_last_result_table()
except Exception:
stage_table = None
resolved_items = last_piped_items if last_piped_items else []
if last_piped_items:
# Prefer selecting from the last selectable *table* (search/playlist)
# rather than from display-only emitted items, unless we're explicitly
# selecting from an overlay table.
try:
if display_table is not None and stage_table is display_table:
items_list = ctx.get_last_result_items() or []
else:
if hasattr(ctx, "get_last_selectable_result_items"):
items_list = ctx.get_last_selectable_result_items() or []
else:
items_list = ctx.get_last_result_items() or []
except Exception:
items_list = []
resolved_items = items_list if items_list else []
if items_list:
filtered = [resolved_items[i] for i in selection_indices if 0 <= i < len(resolved_items)]
if not filtered:
print("No items matched selection in pipeline\n")
@@ -2003,6 +2063,14 @@ class PipelineExecutor:
try:
self._try_clear_pipeline_stop(ctx)
# Preflight (URL-duplicate prompts, etc.) should be cached within a single
# pipeline run, not across independent pipelines.
try:
ctx.store_value("preflight", {})
except Exception:
pass
stages = self._split_stages(tokens)
if not stages:
print("Invalid pipeline syntax\n")
@@ -2066,11 +2134,39 @@ class PipelineExecutor:
stage_args = stage_tokens[1:]
if cmd_name == "@":
# Prefer piping the last emitted/visible items (e.g. add-file results)
# over the result-table subject. The subject can refer to older context
# (e.g. a playlist row) and may not contain store+hash.
last_items = None
try:
last_items = ctx.get_last_result_items()
except Exception:
last_items = None
if last_items:
from cmdlet._shared import coerce_to_pipe_object
try:
pipe_items = [coerce_to_pipe_object(x) for x in list(last_items)]
except Exception:
pipe_items = list(last_items)
piped_result = pipe_items if len(pipe_items) > 1 else pipe_items[0]
try:
ctx.set_last_items(pipe_items)
except Exception:
pass
if pipeline_session and worker_manager:
try:
worker_manager.log_step(pipeline_session.worker_id, "@ used last result items")
except Exception:
pass
continue
subject = ctx.get_last_result_subject()
if subject is None:
print("No current result context available for '@'\n")
pipeline_status = "failed"
pipeline_error = "No result subject for @"
pipeline_error = "No result items/subject for @"
return
piped_result = subject
try:
@@ -2095,18 +2191,34 @@ class PipelineExecutor:
return
selected_indices = []
# Prefer selecting from the last selectable *table* (search/playlist)
# rather than from display-only emitted items, unless we're explicitly
# selecting from an overlay table.
display_table = None
try:
display_table = ctx.get_display_table() if hasattr(ctx, "get_display_table") else None
except Exception:
display_table = None
stage_table = ctx.get_current_stage_table()
if not stage_table and display_table is not None:
stage_table = display_table
if not stage_table:
stage_table = ctx.get_last_result_table()
if display_table is not None and stage_table is display_table:
items_list = ctx.get_last_result_items() or []
else:
if hasattr(ctx, "get_last_selectable_result_items"):
items_list = ctx.get_last_selectable_result_items() or []
else:
items_list = ctx.get_last_result_items() or []
if is_select_all:
last_items = ctx.get_last_result_items() or []
selected_indices = list(range(len(last_items)))
selected_indices = list(range(len(items_list)))
else:
selected_indices = sorted([i - 1 for i in selection]) # type: ignore[arg-type]
stage_table = ctx.get_current_stage_table()
if not stage_table and hasattr(ctx, "get_display_table"):
stage_table = ctx.get_display_table()
if not stage_table:
stage_table = ctx.get_last_result_table()
items_list = ctx.get_last_result_items() or []
resolved_items = items_list if items_list else []
filtered = [resolved_items[i] for i in selected_indices if 0 <= i < len(resolved_items)]
if not filtered:
@@ -2115,6 +2227,20 @@ class PipelineExecutor:
pipeline_error = "Empty selection"
return
# UX: selecting a single URL row from get-url tables should open it.
# Only do this when the selection stage is terminal to avoid surprising
# side-effects in pipelines like `@1 | download-file`.
current_table = ctx.get_current_stage_table() or ctx.get_last_result_table()
if (not is_select_all) and (len(filtered) == 1):
try:
PipelineExecutor._maybe_open_url_selection(
current_table,
filtered,
stage_is_last=(stage_index + 1 >= len(stages)),
)
except Exception:
pass
if PipelineExecutor._maybe_run_class_selector(ctx, config, filtered, stage_is_last=(stage_index + 1 >= len(stages))):
return
@@ -2366,12 +2492,19 @@ class PipelineExecutor:
# the table and pause the pipeline so the user can pick @N.
stage_table = ctx.get_current_stage_table() if hasattr(ctx, "get_current_stage_table") else None
stage_table_type = str(getattr(stage_table, "table", "") or "").strip().lower() if stage_table else ""
try:
stage_table_source = str(getattr(stage_table, "source_command", "") or "").strip().replace("_", "-").lower() if stage_table else ""
except Exception:
stage_table_source = ""
if (
(not stage_is_last)
and (not emits)
and cmd_name in {"download-media", "download_media"}
and stage_table is not None
and stage_table_type in {"ytdlp.formatlist", "download-media", "download_media"}
and (
stage_table_type in {"ytdlp.formatlist", "download-media", "download_media", "bandcamp", "youtube"}
or stage_table_source in {"download-media", "download_media"}
)
):
try:
is_selectable = not bool(getattr(stage_table, "no_choice", False))
@@ -2407,6 +2540,10 @@ class PipelineExecutor:
stdout_console().print()
stdout_console().print(stage_table)
# Always pause the pipeline when a selectable table was produced.
# The user will continue by running @N/@* which will re-attach the
# pending downstream stages.
try:
remaining = stages[stage_index + 1 :]
source_cmd = (