This commit is contained in:
2026-02-02 11:40:51 -08:00
parent 4f7bac464f
commit ccd47db869
2 changed files with 94 additions and 127 deletions

View File

@@ -1655,6 +1655,9 @@ class PipelineExecutor:
if not selection_indices: if not selection_indices:
return True, None return True, None
# ============================================================================
# PHASE 1: Synchronize current stage table with display table
# ============================================================================
# Selection should operate on the *currently displayed* selectable table. # Selection should operate on the *currently displayed* selectable table.
# Some navigation flows (e.g. @.. back) can show a display table without # Some navigation flows (e.g. @.. back) can show a display table without
# updating current_stage_table. Provider selectors rely on current_stage_table # updating current_stage_table. Provider selectors rely on current_stage_table
@@ -1689,6 +1692,60 @@ class PipelineExecutor:
except Exception: except Exception:
logger.exception("Failed to sync current_stage_table from display/last table in _maybe_apply_initial_selection") logger.exception("Failed to sync current_stage_table from display/last table in _maybe_apply_initial_selection")
# ============================================================================
# Helper functions for row action/args discovery (performance: inline caching)
# ============================================================================
def _get_row_action(idx: int, items_cache: List[Any] | None = None) -> List[str] | None:
"""Retrieve row selection_action from table or payload fallback."""
try:
action = ctx.get_current_stage_table_row_selection_action(idx)
if action:
return [str(x) for x in action if x is not None]
except Exception:
pass
# Fallback to serialized _selection_action in payload
if items_cache is None:
try:
items_cache = ctx.get_last_result_items() or []
except Exception:
items_cache = []
if 0 <= idx < len(items_cache):
item = items_cache[idx]
if isinstance(item, dict):
candidate = item.get("_selection_action")
if isinstance(candidate, (list, tuple)):
return [str(x) for x in candidate if x is not None]
return None
def _get_row_args(idx: int, items_cache: List[Any] | None = None) -> List[str] | None:
"""Retrieve row selection_args from table or payload fallback."""
try:
args = ctx.get_current_stage_table_row_selection_args(idx)
if args:
return [str(x) for x in args if x is not None]
except Exception:
pass
# Fallback to serialized _selection_args in payload
if items_cache is None:
try:
items_cache = ctx.get_last_result_items() or []
except Exception:
items_cache = []
if 0 <= idx < len(items_cache):
item = items_cache[idx]
if isinstance(item, dict):
candidate = item.get("_selection_args")
if isinstance(candidate, (list, tuple)):
return [str(x) for x in candidate if x is not None]
return None
# ============================================================================
# PHASE 2: Parse source command and table metadata
# ============================================================================
source_cmd = None source_cmd = None
source_args_raw = None source_args_raw = None
try: try:
@@ -1715,6 +1772,9 @@ class PipelineExecutor:
"table") else None "table") else None
) )
# ============================================================================
# PHASE 3: Handle command expansion for @N syntax
# ============================================================================
command_expanded = False command_expanded = False
example_selector_triggered = False example_selector_triggered = False
normalized_source_cmd = str(source_cmd or "").replace("_", "-").strip().lower() normalized_source_cmd = str(source_cmd or "").replace("_", "-").strip().lower()
@@ -1859,9 +1919,10 @@ class PipelineExecutor:
debug(f"@N: stage_table={stage_table is not None}, display_table={display_table is not None}") debug(f"@N: stage_table={stage_table is not None}, display_table={display_table is not None}")
# Prefer selecting from the last selectable *table* (search/playlist) # ====================================================================
# rather than from display-only emitted items, unless we're explicitly # PHASE 4: Retrieve and filter items from current result set
# selecting from an overlay table. # ====================================================================
# Cache items_list to avoid redundant lookups in helper functions below.
try: try:
if display_table is not None and stage_table is display_table: if display_table is not None and stage_table is display_table:
items_list = ctx.get_last_result_items() or [] items_list = ctx.get_last_result_items() or []
@@ -2034,72 +2095,30 @@ class PipelineExecutor:
if src_norm in {".worker", "worker", "workers"}: if src_norm in {".worker", "worker", "workers"}:
if len(selection_indices) == 1: if len(selection_indices) == 1:
idx = selection_indices[0] idx = selection_indices[0]
row_args = None row_args = _get_row_args(idx, items_list)
try:
row_args = ctx.get_current_stage_table_row_selection_args(idx)
except Exception:
row_args = None
if not row_args:
try:
row_args = ctx.get_last_result_table_row_selection_args(idx)
except Exception:
row_args = None
if not row_args:
try:
items = ctx.get_last_result_items() or []
if 0 <= idx < len(items):
maybe = items[idx]
if isinstance(maybe, dict):
candidate = maybe.get("_selection_args")
if isinstance(candidate, (list, tuple)):
row_args = [str(x) for x in candidate if x is not None]
except Exception:
row_args = row_args or None
if row_args: if row_args:
stages.append( stages.append(
[str(source_cmd_for_selection)] [str(source_cmd_for_selection)]
+ [str(x) for x in row_args if x is not None] + row_args
+ [str(x) for x in source_args_for_selection if x is not None] + [str(x) for x in source_args_for_selection if x is not None]
) )
def _apply_row_action_to_stage(stage_idx: int) -> bool: def _apply_row_action_to_stage(stage_idx: int) -> bool:
"""Apply row selection_action to a specific stage, replacing it."""
if not selection_indices or len(selection_indices) != 1: if not selection_indices or len(selection_indices) != 1:
return False return False
try: row_action = _get_row_action(selection_indices[0], items_list)
row_action = ctx.get_current_stage_table_row_selection_action(
selection_indices[0]
)
except Exception:
row_action = None
if not row_action: if not row_action:
# Fallback to serialized payload when the table row is unavailable
try:
items = ctx.get_last_result_items() or []
if 0 <= selection_indices[0] < len(items):
maybe = items[selection_indices[0]]
if isinstance(maybe, dict):
candidate = maybe.get("_selection_action")
if isinstance(candidate, (list, tuple)):
row_action = [str(x) for x in candidate if x is not None]
debug(f"@N row {selection_indices[0]} restored action from payload: {row_action}")
except Exception:
row_action = row_action or None
if not row_action:
debug(f"@N row {selection_indices[0]} has no selection_action")
return False return False
normalized = [str(x) for x in row_action if x is not None]
if not normalized:
return False
debug(f"Applying row action for row {selection_indices[0]} -> {normalized}")
if 0 <= stage_idx < len(stages): if 0 <= stage_idx < len(stages):
debug(f"Replacing stage {stage_idx} {stages[stage_idx]} with row action {normalized}") stages[stage_idx] = row_action
stages[stage_idx] = normalized
return True return True
return False return False
# ====================================================================
# PHASE 5: Auto-insert stages based on table type and user selection
# ====================================================================
if not stages: if not stages:
debug(f"@N: stages is empty, checking auto_stage and metadata")
if 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") print("Auto-applying metadata selection via get-tag")
stages.append(["get-tag"]) stages.append(["get-tag"])
@@ -2123,78 +2142,31 @@ class PipelineExecutor:
# Only support single-row selection for auto-attach here # Only support single-row selection for auto-attach here
if len(selection_indices) == 1: if len(selection_indices) == 1:
idx = selection_indices[0] idx = selection_indices[0]
row_args = ctx.get_current_stage_table_row_selection_args(idx) row_args = _get_row_args(idx, items_list)
if not row_args:
try:
items = ctx.get_last_result_items() or []
if 0 <= idx < len(items):
maybe = items[idx]
if isinstance(maybe, dict):
candidate = maybe.get("_selection_args")
if isinstance(candidate, (list, tuple)):
row_args = [str(x) for x in candidate if x is not None]
except Exception:
row_args = row_args or None
if row_args: if row_args:
# Place selection args before any existing source args # Place selection args before any existing source args
inserted = stages[-1] inserted = stages[-1]
if inserted: if inserted:
cmd = inserted[0] cmd = inserted[0]
tail = [str(x) for x in inserted[1:]] tail = [str(x) for x in inserted[1:]]
stages[-1] = [cmd] + [str(x) for x in row_args] + tail stages[-1] = [cmd] + row_args + tail
except Exception: except Exception:
logger.exception("Failed to attach selection args to auto-inserted stage") logger.exception("Failed to attach selection args to auto-inserted stage")
# Look for row_action in payload if still no stages # Look for row_action in payload if still no stages
if not stages and selection_indices and len(selection_indices) == 1: if not stages and selection_indices and len(selection_indices) == 1:
debug(f"@N: No stages and no auto_stage, looking for row_action in payload") row_action = _get_row_action(selection_indices[0], items_list)
try: if row_action:
idx = selection_indices[0] debug(f"@N: applying row_action {row_action}")
debug(f"@N: idx={idx}, looking for row_action") stages.append(row_action)
row_action = None if pipeline_session and worker_manager:
try:
row_action = ctx.get_current_stage_table_row_selection_action(idx)
debug(f"@N: row_action from table={row_action}")
except Exception as exc:
debug(f"@N: Exception getting row_selection_action: {exc}")
row_action = None
if not row_action:
debug(f"@N: row_action not found from table, checking payload")
try: try:
items = ctx.get_last_result_items() or [] worker_manager.log_step(
debug(f"@N: got items, length={len(items)}") pipeline_session.worker_id,
if 0 <= idx < len(items): f"@N applied row action -> {' '.join(row_action)}",
maybe = items[idx] )
try: except Exception:
if isinstance(maybe, dict): logger.exception("Failed to record pipeline log step for applied row action (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None))
debug(f"@N: payload is dict with _selection_action={maybe.get('_selection_action')}")
else:
debug(f"@N: payload type={type(maybe).__name__}")
except Exception:
pass
if isinstance(maybe, dict):
candidate = maybe.get("_selection_action")
if isinstance(candidate, (list, tuple)):
row_action = [str(x) for x in candidate if x is not None]
debug(f"@N: extracted row_action from payload={row_action}")
except Exception as exc:
debug(f"@N: Exception checking payload: {exc}")
row_action = None
if row_action:
debug(f"@N: FOUND row_action, appending {row_action}")
stages.append(row_action)
if pipeline_session and worker_manager:
try:
worker_manager.log_step(
pipeline_session.worker_id,
f"@N applied row action -> {' '.join(row_action)}",
)
except Exception:
logger.exception("Failed to record pipeline log step for applied row action (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None))
except Exception:
logger.exception("Failed to apply single-row selection action")
else: else:
first_cmd = stages[0][0] if stages and stages[0] else None first_cmd = stages[0][0] if stages and stages[0] else None
if isinstance(table_type, str) and table_type.startswith("metadata.") and first_cmd not in ( if isinstance(table_type, str) and table_type.startswith("metadata.") and first_cmd not in (
@@ -2224,24 +2196,13 @@ class PipelineExecutor:
if not _apply_row_action_to_stage(0): if not _apply_row_action_to_stage(0):
if len(selection_indices) == 1: if len(selection_indices) == 1:
idx = selection_indices[0] idx = selection_indices[0]
row_args = ctx.get_current_stage_table_row_selection_args(idx) row_args = _get_row_args(idx, items_list)
if not row_args:
try:
items = ctx.get_last_result_items() or []
if 0 <= idx < len(items):
maybe = items[idx]
if isinstance(maybe, dict):
candidate = maybe.get("_selection_args")
if isinstance(candidate, (list, tuple)):
row_args = [str(x) for x in candidate if x is not None]
except Exception:
row_args = row_args or None
if row_args: if row_args:
inserted = stages[0] inserted = stages[0]
if inserted: if inserted:
cmd = inserted[0] cmd = inserted[0]
tail = [str(x) for x in inserted[1:]] tail = [str(x) for x in inserted[1:]]
stages[0] = [cmd] + [str(x) for x in row_args] + tail stages[0] = [cmd] + row_args + tail
except Exception: except Exception:
logger.exception("Failed to attach selection args to inserted auto stage (alternate branch)") logger.exception("Failed to attach selection args to inserted auto stage (alternate branch)")

View File

@@ -117,6 +117,11 @@ def _required_keys_for(store_cls: Type[BaseStore]) -> list[str]:
) )
# Store type names that have been converted to providers-only.
# These should be silently skipped without warning.
_PROVIDER_ONLY_STORE_NAMES = frozenset(("debrid", "alldebrid"))
def _build_kwargs(store_cls: Type[BaseStore], instance_name: str, instance_config: Any) -> Dict[str, Any]: def _build_kwargs(store_cls: Type[BaseStore], instance_name: str, instance_config: Any) -> Dict[str, Any]:
if isinstance(instance_config, dict): if isinstance(instance_config, dict):
cfg_dict = dict(instance_config) cfg_dict = dict(instance_config)
@@ -180,7 +185,8 @@ class Store:
continue continue
store_cls = classes_by_type.get(store_type) store_cls = classes_by_type.get(store_type)
if store_cls is None: if store_cls is None:
if not self._suppress_debug: # Skip provider-only names without debug warning
if store_type not in _PROVIDER_ONLY_STORE_NAMES and not self._suppress_debug:
debug(f"[Store] Unknown store type '{raw_store_type}'") debug(f"[Store] Unknown store type '{raw_store_type}'")
continue continue