j
This commit is contained in:
378
CLI.py
378
CLI.py
@@ -68,6 +68,27 @@ from SYS.cmdlet_catalog import (
|
||||
from SYS.config import get_local_storage_path, load_config
|
||||
from SYS.result_table import ResultTable
|
||||
|
||||
HELP_EXAMPLE_SOURCE_COMMANDS = {
|
||||
".help-example",
|
||||
"help-example",
|
||||
}
|
||||
|
||||
|
||||
def _split_pipeline_tokens(tokens: Sequence[str]) -> List[List[str]]:
|
||||
"""Split example tokens into per-stage command sequences using pipe separators."""
|
||||
|
||||
stages: List[List[str]] = []
|
||||
current: List[str] = []
|
||||
for token in tokens:
|
||||
if token == "|":
|
||||
if current:
|
||||
stages.append(current)
|
||||
current = []
|
||||
continue
|
||||
current.append(str(token))
|
||||
if current:
|
||||
stages.append(current)
|
||||
return [stage for stage in stages if stage]
|
||||
|
||||
class SelectionSyntax:
|
||||
"""Parses @ selection syntax into 1-based indices."""
|
||||
@@ -1397,6 +1418,7 @@ class CmdletExecutor:
|
||||
"get-relationship",
|
||||
"get-rel",
|
||||
".pipe",
|
||||
".mpv",
|
||||
".matrix",
|
||||
".telegram",
|
||||
"telegram",
|
||||
@@ -2418,96 +2440,117 @@ class PipelineExecutor:
|
||||
)
|
||||
|
||||
command_expanded = False
|
||||
example_selector_triggered = False
|
||||
normalized_source_cmd = str(source_cmd or "").replace("_", "-").strip().lower()
|
||||
|
||||
if table_type in {"youtube",
|
||||
"soulseek"}:
|
||||
command_expanded = False
|
||||
elif source_cmd == "search-file" and source_args and "youtube" in source_args:
|
||||
command_expanded = False
|
||||
else:
|
||||
selected_row_args: List[str] = []
|
||||
skip_pipe_expansion = source_cmd == ".pipe" and len(stages) > 0
|
||||
# Command expansion via @N:
|
||||
# - Default behavior: expand ONLY for single-row selections.
|
||||
# - Special case: allow multi-row expansion for add-file directory tables by
|
||||
# combining selected rows into a single `-path file1,file2,...` argument.
|
||||
if source_cmd and not skip_pipe_expansion:
|
||||
src = str(source_cmd).replace("_", "-").strip().lower()
|
||||
if normalized_source_cmd in HELP_EXAMPLE_SOURCE_COMMANDS and selection_indices:
|
||||
try:
|
||||
idx = selection_indices[0]
|
||||
row_args = ctx.get_current_stage_table_row_selection_args(idx)
|
||||
except Exception:
|
||||
row_args = None
|
||||
tokens: List[str] = []
|
||||
if isinstance(row_args, list) and row_args:
|
||||
tokens = [str(x) for x in row_args if x is not None]
|
||||
if tokens:
|
||||
stage_groups = _split_pipeline_tokens(tokens)
|
||||
if stage_groups:
|
||||
for stage in reversed(stage_groups):
|
||||
stages.insert(0, stage)
|
||||
selection_indices = []
|
||||
command_expanded = True
|
||||
example_selector_triggered = True
|
||||
|
||||
if src == "add-file" and selection_indices:
|
||||
row_args_list: List[List[str]] = []
|
||||
for idx in selection_indices:
|
||||
if not example_selector_triggered:
|
||||
if table_type in {"youtube",
|
||||
"soulseek"}:
|
||||
command_expanded = False
|
||||
elif source_cmd == "search-file" and source_args and "youtube" in source_args:
|
||||
command_expanded = False
|
||||
else:
|
||||
selected_row_args: List[str] = []
|
||||
skip_pipe_expansion = source_cmd in {".pipe", ".mpv"} and len(stages) > 0
|
||||
# Command expansion via @N:
|
||||
# - Default behavior: expand ONLY for single-row selections.
|
||||
# - Special case: allow multi-row expansion for add-file directory tables by
|
||||
# combining selected rows into a single `-path file1,file2,...` argument.
|
||||
if source_cmd and not skip_pipe_expansion:
|
||||
src = str(source_cmd).replace("_", "-").strip().lower()
|
||||
|
||||
if src == "add-file" and selection_indices:
|
||||
row_args_list: List[List[str]] = []
|
||||
for idx in selection_indices:
|
||||
try:
|
||||
row_args = ctx.get_current_stage_table_row_selection_args(
|
||||
idx
|
||||
)
|
||||
except Exception:
|
||||
row_args = None
|
||||
if isinstance(row_args, list) and row_args:
|
||||
row_args_list.append(
|
||||
[str(x) for x in row_args if x is not None]
|
||||
)
|
||||
|
||||
# Combine `['-path', <file>]` from each row into one `-path` arg.
|
||||
paths: List[str] = []
|
||||
can_merge = bool(row_args_list) and (
|
||||
len(row_args_list) == len(selection_indices)
|
||||
)
|
||||
if can_merge:
|
||||
for ra in row_args_list:
|
||||
if len(ra) == 2 and str(ra[0]).strip().lower() in {
|
||||
"-path",
|
||||
"--path",
|
||||
"-p",
|
||||
}:
|
||||
p = str(ra[1]).strip()
|
||||
if p:
|
||||
paths.append(p)
|
||||
else:
|
||||
can_merge = False
|
||||
break
|
||||
|
||||
if can_merge and paths:
|
||||
selected_row_args.extend(["-path", ",".join(paths)])
|
||||
elif len(selection_indices) == 1 and row_args_list:
|
||||
selected_row_args.extend(row_args_list[0])
|
||||
else:
|
||||
# Only perform @N command expansion for *single-item* selections.
|
||||
# For multi-item selections (e.g. @*, @1-5), expanding to one row
|
||||
# would silently drop items. In those cases we pipe items downstream.
|
||||
if 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):
|
||||
cmd_list: List[str] = [str(x) for x in source_cmd if x is not None]
|
||||
elif isinstance(source_cmd, str):
|
||||
cmd_list = [source_cmd]
|
||||
else:
|
||||
cmd_list = []
|
||||
|
||||
expanded_stage: List[str] = cmd_list + source_args + selected_row_args
|
||||
|
||||
if first_stage_had_extra_args and stages:
|
||||
expanded_stage += stages[0]
|
||||
stages[0] = expanded_stage
|
||||
else:
|
||||
stages.insert(0, expanded_stage)
|
||||
|
||||
if pipeline_session and worker_manager:
|
||||
try:
|
||||
row_args = ctx.get_current_stage_table_row_selection_args(
|
||||
idx
|
||||
worker_manager.log_step(
|
||||
pipeline_session.worker_id,
|
||||
f"@N expansion: {source_cmd} + {' '.join(str(x) for x in selected_row_args)}",
|
||||
)
|
||||
except Exception:
|
||||
row_args = None
|
||||
if isinstance(row_args, list) and row_args:
|
||||
row_args_list.append(
|
||||
[str(x) for x in row_args if x is not None]
|
||||
)
|
||||
pass
|
||||
|
||||
# Combine `['-path', <file>]` from each row into one `-path` arg.
|
||||
paths: List[str] = []
|
||||
can_merge = bool(row_args_list) and (
|
||||
len(row_args_list) == len(selection_indices)
|
||||
)
|
||||
if can_merge:
|
||||
for ra in row_args_list:
|
||||
if len(ra) == 2 and str(ra[0]).strip().lower() in {
|
||||
"-path",
|
||||
"--path",
|
||||
"-p",
|
||||
}:
|
||||
p = str(ra[1]).strip()
|
||||
if p:
|
||||
paths.append(p)
|
||||
else:
|
||||
can_merge = False
|
||||
break
|
||||
|
||||
if can_merge and paths:
|
||||
selected_row_args.extend(["-path", ",".join(paths)])
|
||||
elif len(selection_indices) == 1 and row_args_list:
|
||||
selected_row_args.extend(row_args_list[0])
|
||||
else:
|
||||
# Only perform @N command expansion for *single-item* selections.
|
||||
# For multi-item selections (e.g. @*, @1-5), expanding to one row
|
||||
# would silently drop items. In those cases we pipe items downstream.
|
||||
if 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):
|
||||
cmd_list: List[str] = [str(x) for x in source_cmd if x is not None]
|
||||
elif isinstance(source_cmd, str):
|
||||
cmd_list = [source_cmd]
|
||||
else:
|
||||
cmd_list = []
|
||||
|
||||
expanded_stage: List[str] = cmd_list + source_args + selected_row_args
|
||||
|
||||
if first_stage_had_extra_args and stages:
|
||||
expanded_stage += stages[0]
|
||||
stages[0] = expanded_stage
|
||||
else:
|
||||
stages.insert(0, expanded_stage)
|
||||
|
||||
if pipeline_session and worker_manager:
|
||||
try:
|
||||
worker_manager.log_step(
|
||||
pipeline_session.worker_id,
|
||||
f"@N expansion: {source_cmd} + {' '.join(str(x) for x in selected_row_args)}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
selection_indices = []
|
||||
command_expanded = True
|
||||
selection_indices = []
|
||||
command_expanded = True
|
||||
|
||||
if (not command_expanded) and selection_indices:
|
||||
stage_table = None
|
||||
@@ -2597,74 +2640,44 @@ class PipelineExecutor:
|
||||
"table") else None
|
||||
)
|
||||
|
||||
def _norm_cmd(name: Any) -> str:
|
||||
return str(name or "").replace("_", "-").strip().lower()
|
||||
|
||||
auto_stage = None
|
||||
if isinstance(table_type, str) and table_type:
|
||||
try:
|
||||
from ProviderCore.registry import selection_auto_stage_for_table
|
||||
|
||||
auto_stage = selection_auto_stage_for_table(table_type)
|
||||
except Exception:
|
||||
auto_stage = None
|
||||
|
||||
if not stages:
|
||||
if table_type == "youtube":
|
||||
print("Auto-running YouTube selection via download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type == "bandcamp":
|
||||
print("Auto-running Bandcamp selection via download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type == "internetarchive":
|
||||
print("Auto-loading Internet Archive item via download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type == "podcastindex.episodes":
|
||||
print("Auto-piping selection to download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type in {"soulseek",
|
||||
"openlibrary",
|
||||
"libgen"}:
|
||||
print("Auto-piping selection to download-file")
|
||||
stages.append(["download-file"])
|
||||
elif isinstance(table_type, str) and table_type.startswith("metadata."):
|
||||
if isinstance(table_type, str) and table_type.startswith("metadata."):
|
||||
print("Auto-applying metadata selection via get-tag")
|
||||
stages.append(["get-tag"])
|
||||
elif auto_stage:
|
||||
try:
|
||||
print(f"Auto-running selection via {auto_stage[0]}")
|
||||
except Exception:
|
||||
pass
|
||||
stages.append(list(auto_stage))
|
||||
else:
|
||||
first_cmd = stages[0][0] if stages and stages[0] else None
|
||||
if table_type == "soulseek" and first_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
debug("Auto-inserting download-file after Soulseek selection")
|
||||
stages.insert(0, ["download-file"])
|
||||
if table_type == "youtube" and first_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
debug("Auto-inserting download-file after YouTube selection")
|
||||
stages.insert(0, ["download-file"])
|
||||
if table_type == "bandcamp" and first_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
print("Auto-inserting download-file after Bandcamp selection")
|
||||
stages.insert(0, ["download-file"])
|
||||
if table_type == "internetarchive" and first_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
debug(
|
||||
"Auto-inserting download-file after Internet Archive selection"
|
||||
)
|
||||
stages.insert(0, ["download-file"])
|
||||
if table_type == "podcastindex.episodes" and first_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
print("Auto-inserting download-file after PodcastIndex episode selection")
|
||||
stages.insert(0, ["download-file"])
|
||||
if table_type == "libgen" and first_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
print("Auto-inserting download-file after Libgen selection")
|
||||
stages.insert(0, ["download-file"])
|
||||
if isinstance(table_type, str) and table_type.startswith("metadata.") and first_cmd not in (
|
||||
"get-tag",
|
||||
"get_tag",
|
||||
".pipe",
|
||||
".mpv",
|
||||
):
|
||||
print("Auto-inserting get-tag after metadata selection")
|
||||
stages.insert(0, ["get-tag"])
|
||||
elif auto_stage:
|
||||
first_cmd_norm = _norm_cmd(first_cmd)
|
||||
auto_cmd_norm = _norm_cmd(auto_stage[0])
|
||||
if first_cmd_norm not in (auto_cmd_norm, ".pipe", ".mpv"):
|
||||
debug(f"Auto-inserting {auto_cmd_norm} after selection")
|
||||
stages.insert(0, list(auto_stage))
|
||||
|
||||
return True, piped_result
|
||||
else:
|
||||
@@ -2744,7 +2757,7 @@ class PipelineExecutor:
|
||||
# `.pipe` (MPV) is an interactive launcher; disable pipeline Live progress
|
||||
# for it because it doesn't meaningfully "complete" (mpv may keep running)
|
||||
# and Live output interferes with MPV playlist UI.
|
||||
if name == ".pipe":
|
||||
if name in {".pipe", ".mpv"}:
|
||||
continue
|
||||
# `.matrix` uses a two-phase picker (@N then .matrix -send). Pipeline Live
|
||||
# progress can linger across those phases and interfere with interactive output.
|
||||
@@ -3161,62 +3174,37 @@ class PipelineExecutor:
|
||||
if stage_index + 1 < len(stages) and stages[stage_index + 1]:
|
||||
next_cmd = _norm_stage_cmd(stages[stage_index + 1][0])
|
||||
|
||||
auto_stage = None
|
||||
if isinstance(table_type, str) and table_type:
|
||||
try:
|
||||
from ProviderCore.registry import selection_auto_stage_for_table
|
||||
|
||||
# Preserve historical behavior: only forward selection-stage args
|
||||
# to the auto stage when we are appending a new last stage.
|
||||
at_end = bool(stage_index + 1 >= len(stages))
|
||||
auto_stage = selection_auto_stage_for_table(
|
||||
table_type,
|
||||
stage_args if at_end else None,
|
||||
)
|
||||
except Exception:
|
||||
auto_stage = None
|
||||
|
||||
# Auto-insert downloader stages for provider tables.
|
||||
# IMPORTANT: do not auto-download for filter selections; they may match many rows.
|
||||
if filter_spec is None:
|
||||
if stage_index + 1 >= len(stages):
|
||||
if table_type == "youtube":
|
||||
print("Auto-running YouTube selection via download-file")
|
||||
stages.append(["download-file", *stage_args])
|
||||
elif table_type == "bandcamp":
|
||||
print("Auto-running Bandcamp selection via download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type == "internetarchive":
|
||||
print("Auto-loading Internet Archive item via download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type == "podcastindex.episodes":
|
||||
print("Auto-piping selection to download-file")
|
||||
stages.append(["download-file"])
|
||||
elif table_type in {"soulseek", "openlibrary", "libgen"}:
|
||||
print("Auto-piping selection to download-file")
|
||||
stages.append(["download-file"])
|
||||
if auto_stage:
|
||||
try:
|
||||
print(f"Auto-running selection via {auto_stage[0]}")
|
||||
except Exception:
|
||||
pass
|
||||
stages.append(list(auto_stage))
|
||||
else:
|
||||
if table_type == "soulseek" and next_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
debug("Auto-inserting download-file after Soulseek selection")
|
||||
stages.insert(stage_index + 1, ["download-file"])
|
||||
if table_type == "youtube" and next_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
debug("Auto-inserting download-file after YouTube selection")
|
||||
stages.insert(stage_index + 1, ["download-file"])
|
||||
if table_type == "bandcamp" and next_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
print("Auto-inserting download-file after Bandcamp selection")
|
||||
stages.insert(stage_index + 1, ["download-file"])
|
||||
if table_type == "internetarchive" and next_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
debug("Auto-inserting download-file after Internet Archive selection")
|
||||
stages.insert(stage_index + 1, ["download-file"])
|
||||
if table_type == "podcastindex.episodes" and next_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
print("Auto-inserting download-file after PodcastIndex episode selection")
|
||||
stages.insert(stage_index + 1, ["download-file"])
|
||||
if table_type == "libgen" and next_cmd not in (
|
||||
"download-file",
|
||||
".pipe",
|
||||
):
|
||||
print("Auto-inserting download-file after Libgen selection")
|
||||
stages.insert(stage_index + 1, ["download-file"])
|
||||
if auto_stage:
|
||||
auto_cmd = _norm_stage_cmd(auto_stage[0])
|
||||
if next_cmd not in (auto_cmd, ".pipe", ".mpv"):
|
||||
debug(f"Auto-inserting {auto_cmd} after selection")
|
||||
stages.insert(stage_index + 1, list(auto_stage))
|
||||
continue
|
||||
|
||||
cmd_fn = REGISTRY.get(cmd_name)
|
||||
@@ -3386,9 +3374,9 @@ class PipelineExecutor:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# `.pipe` is typically the terminal interactive stage (MPV UI).
|
||||
# `.pipe`/`.mpv` is typically the terminal interactive stage (MPV UI).
|
||||
# Stop Live progress before running it so output doesn't get stuck behind Live.
|
||||
if (cmd_name == ".pipe" and progress_ui is not None
|
||||
if (cmd_name in {".pipe", ".mpv"} and progress_ui is not None
|
||||
and (stage_index + 1 >= len(stages))):
|
||||
try:
|
||||
progress_ui.stop()
|
||||
@@ -3495,7 +3483,7 @@ class PipelineExecutor:
|
||||
"bandcamp",
|
||||
"youtube",
|
||||
} or stage_table_source in {"download-file"}
|
||||
or stage_table_type in {"internetarchive.formats"}
|
||||
or stage_table_type in {"internetarchive.format", "internetarchive.formats"}
|
||||
or stage_table_source in {"download-file"})):
|
||||
try:
|
||||
is_selectable = not bool(
|
||||
|
||||
Reference in New Issue
Block a user