updated panel display

This commit is contained in:
2026-04-16 17:18:50 -07:00
parent 97e310be70
commit 343a7b37a0
14 changed files with 711 additions and 264 deletions
+2 -18
View File
@@ -21,7 +21,7 @@ from pathlib import Path
from urllib.parse import unquote, urlparse, parse_qs from urllib.parse import unquote, urlparse, parse_qs
import logging import logging
from SYS.logger import debug, is_debug_enabled, log from SYS.logger import debug, debug_panel, is_debug_enabled, log
from SYS.models import DebugLogger, DownloadError, DownloadMediaResult, ProgressBar from SYS.models import DebugLogger, DownloadError, DownloadMediaResult, ProgressBar
from SYS.utils import ensure_directory, sha256_file from SYS.utils import ensure_directory, sha256_file
@@ -80,23 +80,7 @@ class HTTPClient:
def _debug_panel(self, title: str, rows: List[tuple[str, Any]]) -> None: def _debug_panel(self, title: str, rows: List[tuple[str, Any]]) -> None:
if not is_debug_enabled(): if not is_debug_enabled():
return return
try: debug_panel(title, rows, border_style="bright_blue")
from rich.table import Table as RichTable
from rich.panel import Panel
grid = RichTable.grid(padding=(0, 1))
grid.add_column("Key", style="cyan", no_wrap=True)
grid.add_column("Value")
for key, val in rows:
try:
grid.add_row(str(key), str(val))
except Exception:
grid.add_row(str(key), "<unprintable>")
debug(Panel(grid, title=title, expand=False))
except Exception:
# Fallback to simple debug output
debug(title, rows)
def __enter__(self): def __enter__(self):
"""Context manager entry.""" """Context manager entry."""
+4 -4
View File
@@ -425,7 +425,7 @@
"(hexupload\\.net|hexload\\.com)/([a-zA-Z0-9]{12})" "(hexupload\\.net|hexload\\.com)/([a-zA-Z0-9]{12})"
], ],
"regexp": "(hexupload\\.net|hexload\\.com)/([a-zA-Z0-9]{12})", "regexp": "(hexupload\\.net|hexload\\.com)/([a-zA-Z0-9]{12})",
"status": true "status": false
}, },
"hot4share": { "hot4share": {
"name": "hot4share", "name": "hot4share",
@@ -482,7 +482,7 @@
"(katfile\\.com/[0-9a-zA-Z]{12})" "(katfile\\.com/[0-9a-zA-Z]{12})"
], ],
"regexp": "(katfile\\.(cloud|online|vip)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))", "regexp": "(katfile\\.(cloud|online|vip)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
"status": true "status": false
}, },
"mediafire": { "mediafire": {
"name": "mediafire", "name": "mediafire",
@@ -595,7 +595,7 @@
"(simfileshare\\.net/download/[0-9]+/)" "(simfileshare\\.net/download/[0-9]+/)"
], ],
"regexp": "(simfileshare\\.net/download/[0-9]+/)", "regexp": "(simfileshare\\.net/download/[0-9]+/)",
"status": true "status": false
}, },
"streamtape": { "streamtape": {
"name": "streamtape", "name": "streamtape",
@@ -690,7 +690,7 @@
"uploadrar\\.(net|com)/([0-9a-z]{12})" "uploadrar\\.(net|com)/([0-9a-z]{12})"
], ],
"regexp": "((get|cloud)\\.rahim-soft\\.com/([0-9a-z]{12}))|((fingau\\.com/([0-9a-z]{12})))|((tech|miui|cloud|flash)\\.getpczone\\.com/([0-9a-z]{12}))|(miui.rahim-soft\\.com/([0-9a-z]{12}))|(uploadrar\\.(net|com)/([0-9a-z]{12}))", "regexp": "((get|cloud)\\.rahim-soft\\.com/([0-9a-z]{12}))|((fingau\\.com/([0-9a-z]{12})))|((tech|miui|cloud|flash)\\.getpczone\\.com/([0-9a-z]{12}))|(miui.rahim-soft\\.com/([0-9a-z]{12}))|(uploadrar\\.(net|com)/([0-9a-z]{12}))",
"status": true, "status": false,
"hardRedirect": [ "hardRedirect": [
"uploadrar.com/([0-9a-zA-Z]{12})" "uploadrar.com/([0-9a-zA-Z]{12})"
] ]
+8 -4
View File
@@ -17,7 +17,7 @@ from ProviderCore.base import Provider, SearchResult
from SYS.provider_helpers import TableProviderMixin from SYS.provider_helpers import TableProviderMixin
from SYS.item_accessors import get_field as _extract_value from SYS.item_accessors import get_field as _extract_value
from SYS.utils import sanitize_filename from SYS.utils import sanitize_filename
from SYS.logger import log, debug from SYS.logger import log, debug, debug_panel
from SYS.models import DownloadError, PipeObject from SYS.models import DownloadError, PipeObject
_HOSTS_CACHE_TTL_SECONDS = 24 * 60 * 60 _HOSTS_CACHE_TTL_SECONDS = 24 * 60 * 60
@@ -757,9 +757,13 @@ class AllDebrid(TableProviderMixin, Provider):
dom = str(d or "").strip().lower() dom = str(d or "").strip().lower()
if dom and dom not in patterns: if dom and dom not in patterns:
patterns.append(dom) patterns.append(dom)
log( debug_panel(
f"[alldebrid] url_patterns loaded {len(cached)} cached host domains; total patterns={len(patterns)}", "AllDebrid host cache",
file=sys.stderr, [
("cached_domains", len(cached)),
("total_patterns", len(patterns)),
],
border_style="magenta",
) )
except Exception: except Exception:
pass pass
+6
View File
@@ -51,6 +51,12 @@ def global_config() -> List[Dict[str, Any]]:
"label": "Auto-Update", "label": "Auto-Update",
"default": "true", "default": "true",
"choices": ["true", "false"] "choices": ["true", "false"]
},
{
"key": "table_appearance",
"label": "Table Appearance",
"default": "rainbow",
"choices": ["plain", "bw-striped", "rainbow"]
} }
] ]
+116 -26
View File
@@ -5,7 +5,7 @@ import inspect
import logging import logging
import threading import threading
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Optional, Sequence
from SYS.rich_display import console_for from SYS.rich_display import console_for
@@ -43,6 +43,106 @@ def is_debug_enabled() -> bool:
return _DEBUG_ENABLED return _DEBUG_ENABLED
def _debug_output_suppressed() -> bool:
try:
stderr_name = getattr(sys.stderr, "name", "")
return "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name)
except Exception:
return False
def _debug_output_file(file=None):
stream = get_thread_stream()
if stream is not None:
return stream
if file is not None:
return file
return sys.stderr
def _is_rich_renderable(value: Any) -> bool:
if value is None:
return False
if isinstance(value, (str, bytes, bytearray)):
return False
return bool(
hasattr(value, "__rich_console__")
or hasattr(value, "__rich__")
or value.__class__.__module__.startswith("rich.")
)
def _caller_location(depth: int = 1) -> tuple[str, str]:
frame = inspect.currentframe()
current = frame
try:
for _ in range(max(0, int(depth))):
if current is None:
break
current = current.f_back
if current is None:
return "", ""
return Path(current.f_code.co_filename).stem, current.f_code.co_name
finally:
del frame
def _debug_db_log(*, caller_name: str, message: str, level: str = "DEBUG") -> None:
if not _DB_LOGGER:
return
try:
_DB_LOGGER(level, caller_name, message)
except Exception:
pass
def debug_panel(
title: str,
rows: Sequence[tuple[str, Any]],
*,
file=None,
border_style: str = "cyan",
) -> None:
"""Render a compact key/value debug panel when debug logging is enabled."""
if not _DEBUG_ENABLED or _debug_output_suppressed():
return
target_file = _debug_output_file(file)
try:
from rich.panel import Panel
from rich.table import Table as RichTable
grid = RichTable.grid(padding=(0, 1))
grid.add_column("Key", style="cyan", no_wrap=True)
grid.add_column("Value")
for key, val in rows:
try:
grid.add_row(str(key), str(val))
except Exception:
grid.add_row(str(key), "<unprintable>")
debug(
Panel(
grid,
title=str(title or "Debug"),
expand=False,
border_style=border_style,
),
file=target_file,
)
file_name, func_name = _caller_location(depth=2)
caller_name = f"{file_name}.{func_name}" if file_name and func_name else ""
_debug_db_log(
caller_name=caller_name,
message=f"[{title}] " + "; ".join(f"{k}={v}" for k, v in rows),
)
except Exception:
debug(title, list(rows), file=target_file)
def debug(*args, **kwargs) -> None: def debug(*args, **kwargs) -> None:
"""Print debug message if debug logging is enabled. """Print debug message if debug logging is enabled.
@@ -53,26 +153,24 @@ def debug(*args, **kwargs) -> None:
# Check if stderr has been redirected to /dev/null (quiet mode) # Check if stderr has been redirected to /dev/null (quiet mode)
# If so, skip output to avoid queuing in background worker's capture # If so, skip output to avoid queuing in background worker's capture
try: if _debug_output_suppressed():
stderr_name = getattr(sys.stderr, "name", "") return
if "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name):
return
except Exception:
pass
# Check for thread-local stream first target_file = _debug_output_file(kwargs.pop("file", None))
stream = get_thread_stream()
if stream: if len(args) == 1 and _is_rich_renderable(args[0]):
kwargs["file"] = stream renderable = args[0]
# Set default to stderr for debug messages console_for(target_file).print(renderable)
elif "file" not in kwargs: file_name, func_name = _caller_location(depth=1)
kwargs["file"] = sys.stderr caller_name = f"{file_name}.{func_name}" if file_name and func_name else ""
_debug_db_log(caller_name=caller_name, message=f"<rich:{type(renderable).__name__}>")
return
# Prepend DEBUG label # Prepend DEBUG label
args = ("DEBUG:", *args) args = ("DEBUG:", *args)
# Use the same logic as log() # Use the same logic as log()
log(*args, **kwargs) log(*args, file=target_file, **kwargs)
def debug_inspect( def debug_inspect(
@@ -97,19 +195,11 @@ def debug_inspect(
return return
# Mirror debug() quiet-mode guard. # Mirror debug() quiet-mode guard.
try: if _debug_output_suppressed():
stderr_name = getattr(sys.stderr, "name", "") return
if "nul" in str(stderr_name).lower() or "/dev/null" in str(stderr_name):
return
except Exception:
pass
# Resolve destination stream. # Resolve destination stream.
stream = get_thread_stream() file = _debug_output_file(file)
if stream is not None:
file = stream
elif file is None:
file = sys.stderr
# Compute caller prefix (same as log()). # Compute caller prefix (same as log()).
prefix = None prefix = None
+76 -17
View File
@@ -10,7 +10,7 @@ from dataclasses import dataclass, field
from contextvars import ContextVar from contextvars import ContextVar
from typing import Any, Dict, List, Optional, Sequence, Callable from typing import Any, Dict, List, Optional, Sequence, Callable
from SYS.models import PipelineStageContext from SYS.models import PipelineStageContext
from SYS.logger import log, debug, is_debug_enabled from SYS.logger import log, debug, debug_panel, is_debug_enabled
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from SYS.worker import WorkerManagerRegistry, WorkerStages from SYS.worker import WorkerManagerRegistry, WorkerStages
@@ -28,6 +28,47 @@ HELP_EXAMPLE_SOURCE_COMMANDS = {
} }
def _emit_selection_debug_panel(
*,
selection_token: Any,
selection_indices: Sequence[int],
item_count: int,
filtered_count: int,
stage_table_present: bool,
display_table_present: bool,
stage_is_last: bool,
row_action: Optional[Sequence[Any]] = None,
downstream_stages: Optional[Sequence[Sequence[Any]]] = None,
mode: Optional[str] = None,
) -> None:
if not is_debug_enabled():
return
try:
rows: List[tuple[str, Any]] = [
("selection", str(selection_token or "")),
("indices", [int(idx) + 1 for idx in (selection_indices or [])]),
("items", int(item_count)),
("filtered", int(filtered_count)),
("stage_table", bool(stage_table_present)),
("display_table", bool(display_table_present)),
("stage_is_last", bool(stage_is_last)),
("downstream_stages", len(list(downstream_stages or []))),
]
if mode:
rows.insert(1, ("mode", str(mode)))
if row_action:
rows.append(("row_action", " ".join(str(part) for part in row_action if part is not None)))
debug_panel(
f"Selection replay {selection_token}",
rows,
border_style="magenta",
)
except Exception:
pass
def set_live_progress(progress_ui: Any) -> None: def set_live_progress(progress_ui: Any) -> None:
"""Register the current Live progress UI so cmdlets can suspend it during prompts.""" """Register the current Live progress UI so cmdlets can suspend it during prompts."""
state = _get_pipeline_state() state = _get_pipeline_state()
@@ -1883,6 +1924,7 @@ class PipelineExecutor:
selected_row_args: List[str] = [] selected_row_args: List[str] = []
skip_pipe_expansion = source_cmd in {".pipe", ".mpv"} and len(stages) > 0 skip_pipe_expansion = source_cmd in {".pipe", ".mpv"} and len(stages) > 0
prefer_row_action = False prefer_row_action = False
preferred_row_action = None
if len(selection_indices) == 1 and not stages: if len(selection_indices) == 1 and not stages:
try: try:
row_action = _get_row_action(selection_indices[0]) row_action = _get_row_action(selection_indices[0])
@@ -1890,10 +1932,7 @@ class PipelineExecutor:
row_action = None row_action = None
if row_action: if row_action:
prefer_row_action = True prefer_row_action = True
debug( preferred_row_action = list(row_action)
"@N: skipping source command expansion because row has explicit selection_action "
f"{row_action}"
)
# Command expansion via @N: # Command expansion via @N:
# - Default behavior: expand ONLY for single-row selections. # - Default behavior: expand ONLY for single-row selections.
# - Special case: allow multi-row expansion for add-file directory tables by # - Special case: allow multi-row expansion for add-file directory tables by
@@ -1978,7 +2017,7 @@ class PipelineExecutor:
except Exception: except Exception:
logger.exception("Failed to record pipeline log step for @N expansion (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None)) logger.exception("Failed to record pipeline log step for @N expansion (pipeline_session=%r)", getattr(pipeline_session, 'worker_id', None))
elif selected_row_args and stages: elif selected_row_args and stages:
debug("@N: skipping source command expansion because downstream stages exist") pass
stage_table = None stage_table = None
try: try:
@@ -2003,8 +2042,6 @@ class PipelineExecutor:
except Exception: except Exception:
stage_table = None stage_table = None
debug(f"@N: stage_table={stage_table is not None}, display_table={display_table is not None}")
# ==================================================================== # ====================================================================
# PHASE 4: Retrieve and filter items from current result set # PHASE 4: Retrieve and filter items from current result set
# ==================================================================== # ====================================================================
@@ -2015,14 +2052,31 @@ class PipelineExecutor:
except Exception as exc: except Exception as exc:
debug(f"@N: Exception getting items_list: {exc}") debug(f"@N: Exception getting items_list: {exc}")
items_list = [] items_list = []
debug(f"@N: selection_indices={selection_indices}, items_list length={len(items_list)}")
resolved_items = items_list if items_list else [] resolved_items = items_list if items_list else []
if items_list: if items_list:
filtered = [ filtered = [
resolved_items[i] for i in selection_indices resolved_items[i] for i in selection_indices
if 0 <= i < len(resolved_items) if 0 <= i < len(resolved_items)
] ]
if selection_indices:
if len(selection_indices) == 1:
selection_label = f"@{selection_indices[0] + 1}"
else:
selection_label = "@{" + ",".join(str(idx + 1) for idx in selection_indices) + "}"
else:
selection_label = "@selection"
_emit_selection_debug_panel(
selection_token=selection_label,
selection_indices=selection_indices,
item_count=len(items_list),
filtered_count=len(filtered),
stage_table_present=(stage_table is not None),
display_table_present=(display_table is not None),
stage_is_last=(not stages),
row_action=preferred_row_action,
downstream_stages=stages,
mode=("row_action" if preferred_row_action else "selection"),
)
if not filtered: if not filtered:
print("No items matched selection in pipeline\n") print("No items matched selection in pipeline\n")
return False, None return False, None
@@ -2088,15 +2142,12 @@ class PipelineExecutor:
filtered = track_items filtered = track_items
table_type_hint = "tidal.track" table_type_hint = "tidal.track"
debug(f"@N: calling _maybe_run_class_selector with filtered={len(filtered)} items, stage_is_last={not stages}")
if PipelineExecutor._maybe_run_class_selector( if PipelineExecutor._maybe_run_class_selector(
ctx, ctx,
config, config,
filtered, filtered,
stage_is_last=(not stages)): stage_is_last=(not stages)):
debug(f"@N: _maybe_run_class_selector returned True, returning False")
return False, None return False, None
debug(f"@N: _maybe_run_class_selector returned False, continuing")
from SYS.pipe_object import coerce_to_pipe_object from SYS.pipe_object import coerce_to_pipe_object
@@ -2105,7 +2156,6 @@ class PipelineExecutor:
filtered_pipe_objs filtered_pipe_objs
if len(filtered_pipe_objs) > 1 else filtered_pipe_objs[0] if len(filtered_pipe_objs) > 1 else filtered_pipe_objs[0]
) )
debug(f"@N: coerced piped_result, stages={stages}")
if pipeline_session and worker_manager: if pipeline_session and worker_manager:
try: try:
@@ -2204,7 +2254,6 @@ class PipelineExecutor:
print("Auto-applying metadata selection via get-tag") print("Auto-applying metadata selection via get-tag")
stages.append(["get-tag"]) stages.append(["get-tag"])
elif auto_stage: elif auto_stage:
debug(f"@N: Found auto_stage={auto_stage}, appending")
try: try:
print(f"Auto-running selection via {auto_stage[0]}") print(f"Auto-running selection via {auto_stage[0]}")
except Exception: except Exception:
@@ -2238,7 +2287,6 @@ class PipelineExecutor:
if not stages and selection_indices and len(selection_indices) == 1: if not stages and selection_indices and len(selection_indices) == 1:
row_action = _get_row_action(selection_indices[0], items_list) row_action = _get_row_action(selection_indices[0], items_list)
if row_action: if row_action:
debug(f"@N: applying row_action {row_action}")
stages.append(row_action) stages.append(row_action)
if pipeline_session and worker_manager: if pipeline_session and worker_manager:
try: try:
@@ -2464,7 +2512,18 @@ class PipelineExecutor:
ctx = sys.modules[__name__] ctx = sys.modules[__name__]
try: try:
debug(f"execute_tokens: tokens={tokens}") try:
from SYS.logger import debug_panel
debug_panel(
"Pipeline execution",
[
("command", " ".join(str(tok) for tok in tokens)),
("token_count", len(tokens)),
],
)
except Exception:
debug(f"execute_tokens: tokens={tokens}")
self._try_clear_pipeline_stop(ctx) self._try_clear_pipeline_stop(ctx)
# REPL guard: stage-local tables should not persist across independent # REPL guard: stage-local tables should not persist across independent
+116 -19
View File
@@ -16,6 +16,7 @@ from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Callable, Set from typing import Any, Dict, List, Optional, Callable, Set
from pathlib import Path from pathlib import Path
import json import json
import re
from rich.box import SIMPLE from rich.box import SIMPLE
from rich.console import Group from rich.console import Group
@@ -113,14 +114,97 @@ _RESULT_TABLE_ROW_STYLE_LOOP: List[tuple[str, str]] = [
("#ffffff", "#000000"), ("#ffffff", "#000000"),
] ]
_RESULT_TABLE_BW_ROW_STYLE_LOOP: List[tuple[str, str]] = [
("#000000", "#ffffff"),
("#ffffff", "#000000"),
]
RESULT_TABLE_HEADER_STYLE = "bold #000000 on #ffffff" RESULT_TABLE_HEADER_STYLE = "bold #000000 on #ffffff"
RESULT_TABLE_BORDER_STYLE = "#000000 on #ffffff" RESULT_TABLE_BORDER_STYLE = "#000000 on #ffffff"
RESULT_TABLE_PLAIN_HEADER_STYLE = "bold"
_RESULT_TABLE_APPEARANCE_ALIASES: Dict[str, str] = {
"": "rainbow",
"rainbow": "rainbow",
"default": "rainbow",
"plain": "plain",
"none": "plain",
"bw": "bw-striped",
"b-w": "bw-striped",
"b-w-striped": "bw-striped",
"bw-striped": "bw-striped",
"b-w-stripes": "bw-striped",
"bw-stripes": "bw-striped",
"black-white": "bw-striped",
"black-white-striped": "bw-striped",
"black-white-stripes": "bw-striped",
"black-and-white": "bw-striped",
"black-and-white-striped": "bw-striped",
"black-and-white-stripes": "bw-striped",
}
def get_result_table_row_style(row_index: int) -> str: def normalize_result_table_appearance_mode(value: Any) -> str:
text_color, bg_color = _RESULT_TABLE_ROW_STYLE_LOOP[ text = str(value or "").strip().lower()
row_index % len(_RESULT_TABLE_ROW_STYLE_LOOP) if not text:
] return "rainbow"
collapsed = re.sub(r"[^a-z0-9]+", "-", text).strip("-")
return _RESULT_TABLE_APPEARANCE_ALIASES.get(collapsed, "rainbow")
def get_result_table_appearance_mode(config: Optional[Dict[str, Any]] = None) -> str:
cfg = config
if cfg is None:
try:
from SYS.config import load_config
cfg = load_config() or {}
except Exception:
cfg = {}
raw_value = None
if isinstance(cfg, dict):
raw_value = cfg.get("table_appearance")
return normalize_result_table_appearance_mode(raw_value)
def get_result_table_header_style(config: Optional[Dict[str, Any]] = None) -> str:
mode = get_result_table_appearance_mode(config)
if mode == "plain":
return RESULT_TABLE_PLAIN_HEADER_STYLE
return RESULT_TABLE_HEADER_STYLE
def get_result_table_border_style(config: Optional[Dict[str, Any]] = None) -> str:
mode = get_result_table_appearance_mode(config)
if mode == "plain":
return ""
return RESULT_TABLE_BORDER_STYLE
def get_result_table_panel_style(config: Optional[Dict[str, Any]] = None) -> str:
mode = get_result_table_appearance_mode(config)
if mode == "plain":
return ""
return "on #ffffff"
def get_result_table_row_style(
row_index: int,
appearance_mode: Optional[str] = None,
config: Optional[Dict[str, Any]] = None,
) -> str:
mode = appearance_mode or get_result_table_appearance_mode(config)
if mode == "plain":
return ""
style_loop = (
_RESULT_TABLE_BW_ROW_STYLE_LOOP
if mode == "bw-striped" else _RESULT_TABLE_ROW_STYLE_LOOP
)
text_color, bg_color = style_loop[row_index % len(style_loop)]
return f"{text_color} on {bg_color}" return f"{text_color} on {bg_color}"
@@ -1435,16 +1519,21 @@ class Table:
def to_rich(self): def to_rich(self):
"""Return a Rich renderable representing this table.""" """Return a Rich renderable representing this table."""
appearance_mode = get_result_table_appearance_mode()
header_style = get_result_table_header_style({"table_appearance": appearance_mode})
border_style = get_result_table_border_style({"table_appearance": appearance_mode})
panel_style = get_result_table_panel_style({"table_appearance": appearance_mode})
if not self.rows: if not self.rows:
empty = Text("No results") empty = Text("No results")
return ( return (
Panel( Panel(
empty, empty,
title=Text(str(self.title), style=RESULT_TABLE_HEADER_STYLE), title=Text(str(self.title), style=header_style),
border_style=RESULT_TABLE_BORDER_STYLE, border_style=border_style,
padding=(0, 0), padding=(0, 0),
expand=False, expand=False,
style="on #ffffff", style=panel_style,
) )
if self.title if self.title
else empty else empty
@@ -1460,8 +1549,8 @@ class Table:
table = RichTable( table = RichTable(
show_header=True, show_header=True,
header_style=RESULT_TABLE_HEADER_STYLE, header_style=header_style,
border_style=RESULT_TABLE_BORDER_STYLE, border_style=border_style,
box=None, box=None,
expand=False, expand=False,
show_lines=False, show_lines=False,
@@ -1497,7 +1586,13 @@ class Table:
for name in col_names: for name in col_names:
val = row.get_column(name) or "" val = row.get_column(name) or ""
cells.append(self._apply_value_case(_sanitize_cell_text(val))) cells.append(self._apply_value_case(_sanitize_cell_text(val)))
table.add_row(*cells, style=get_result_table_row_style(row_idx - 1)) table.add_row(
*cells,
style=get_result_table_row_style(
row_idx - 1,
appearance_mode=appearance_mode,
),
)
if self.title or self.header_lines: if self.title or self.header_lines:
header_bits = [Text(line) for line in (self.header_lines or [])] header_bits = [Text(line) for line in (self.header_lines or [])]
@@ -1505,11 +1600,11 @@ class Table:
return ( return (
Panel( Panel(
renderable, renderable,
title=Text(str(self.title), style=RESULT_TABLE_HEADER_STYLE), title=Text(str(self.title), style=header_style),
border_style=RESULT_TABLE_BORDER_STYLE, border_style=border_style,
padding=(0, 0), padding=(0, 0),
expand=False, expand=False,
style="on #ffffff", style=panel_style,
) )
if self.title if self.title
else renderable else renderable
@@ -2385,11 +2480,13 @@ class ItemDetailView(Table):
elements = [] elements = []
if has_details: if has_details:
header_style = get_result_table_header_style()
border_style = get_result_table_border_style()
detail_title = str(self.detail_title or "Item Details").strip() or "Item Details" detail_title = str(self.detail_title or "Item Details").strip() or "Item Details"
elements.append(Panel( elements.append(Panel(
details_table, details_table,
title=Text(detail_title, style=RESULT_TABLE_HEADER_STYLE), title=Text(detail_title, style=header_style),
border_style=RESULT_TABLE_BORDER_STYLE, border_style=border_style,
padding=(1, 2) padding=(1, 2)
)) ))
@@ -2397,11 +2494,11 @@ class ItemDetailView(Table):
# If it's a Panel already (from super().to_rich() with title), use it directly # If it's a Panel already (from super().to_rich() with title), use it directly
# but force the border style to the result-table standard for consistency # but force the border style to the result-table standard for consistency
if isinstance(results_renderable, Panel): if isinstance(results_renderable, Panel):
results_renderable.border_style = RESULT_TABLE_BORDER_STYLE results_renderable.border_style = get_result_table_border_style()
if results_renderable.title: if results_renderable.title:
results_renderable.title = Text( results_renderable.title = Text(
str(results_renderable.title), str(results_renderable.title),
style=RESULT_TABLE_HEADER_STYLE, style=get_result_table_header_style(),
) )
# Add a bit of padding inside if it contains a table # Add a bit of padding inside if it contains a table
elements.append(results_renderable) elements.append(results_renderable)
@@ -2416,8 +2513,8 @@ class ItemDetailView(Table):
elements.append( elements.append(
Panel( Panel(
results_group, results_group,
title=Text(str(display_title), style=RESULT_TABLE_HEADER_STYLE), title=Text(str(display_title), style=get_result_table_header_style()),
border_style=RESULT_TABLE_BORDER_STYLE, border_style=get_result_table_border_style(),
) )
) )
+13 -5
View File
@@ -13,9 +13,10 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from SYS.result_table import ( from SYS.result_table import (
RESULT_TABLE_BORDER_STYLE,
RESULT_TABLE_HEADER_STYLE,
apply_result_table_layout, apply_result_table_layout,
get_result_table_appearance_mode,
get_result_table_border_style,
get_result_table_header_style,
get_result_table_row_style, get_result_table_row_style,
) )
from SYS.result_table_api import ColumnSpec, ResultModel, ResultTable, Renderer from SYS.result_table_api import ColumnSpec, ResultModel, ResultTable, Renderer
@@ -38,8 +39,8 @@ class RichRenderer(Renderer):
table = RichTable( table = RichTable(
show_header=True, show_header=True,
header_style=RESULT_TABLE_HEADER_STYLE, header_style=get_result_table_header_style(),
border_style=RESULT_TABLE_BORDER_STYLE, border_style=get_result_table_border_style(),
box=None, box=None,
padding=(0, 1), padding=(0, 1),
pad_edge=False, pad_edge=False,
@@ -47,6 +48,7 @@ class RichRenderer(Renderer):
expand=False, expand=False,
) )
apply_result_table_layout(table) apply_result_table_layout(table)
appearance_mode = get_result_table_appearance_mode()
cols = list(columns) cols = list(columns)
for col in cols: for col in cols:
if str(col.header or "").strip().lower() == "tag": if str(col.header or "").strip().lower() == "tag":
@@ -71,7 +73,13 @@ class RichRenderer(Renderer):
logger.exception("Column extractor failed for '%s': %s", col.header, exc) logger.exception("Column extractor failed for '%s': %s", col.header, exc)
cell = "" cell = ""
cells.append(cell) cells.append(cell)
table.add_row(*cells, style=get_result_table_row_style(row_idx)) table.add_row(
*cells,
style=get_result_table_row_style(
row_idx,
appearance_mode=appearance_mode,
),
)
return table return table
+111 -39
View File
@@ -8,7 +8,7 @@ from collections import deque
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple
from urllib.parse import quote from urllib.parse import quote, parse_qsl, urlencode, urlsplit, urlunsplit
import httpx import httpx
from API.httpx_shared import get_shared_httpx_client from API.httpx_shared import get_shared_httpx_client
@@ -417,8 +417,6 @@ class HydrusNetwork(Store):
if not file_hash: if not file_hash:
file_hash = sha256_file(file_path) file_hash = sha256_file(file_path)
debug(f"{self._log_prefix()} file hash: {file_hash}")
# Use persistent client with session key # Use persistent client with session key
client = self._client client = self._client
if client is None: if client is None:
@@ -564,7 +562,6 @@ class HydrusNetwork(Store):
raise Exception(f"Hydrus response missing file hash: {response}") raise Exception(f"Hydrus response missing file hash: {response}")
file_hash = hydrus_hash file_hash = hydrus_hash
debug(f"{self._log_prefix()} hash: {file_hash}")
# Add tags if provided (both for new and existing files) # Add tags if provided (both for new and existing files)
if tag_list: if tag_list:
@@ -575,13 +572,7 @@ class HydrusNetwork(Store):
service_name = "my tags" service_name = "my tags"
try: try:
debug(
f"{self._log_prefix()} Adding {len(tag_list)} tag(s): {tag_list}"
)
client.add_tag(file_hash, tag_list, service_name) client.add_tag(file_hash, tag_list, service_name)
debug(
f"{self._log_prefix()} Tags added via '{service_name}'"
)
except Exception as exc: except Exception as exc:
log( log(
f"{self._log_prefix()} ⚠️ Failed to add tags: {exc}", f"{self._log_prefix()} ⚠️ Failed to add tags: {exc}",
@@ -590,14 +581,10 @@ class HydrusNetwork(Store):
# Associate url if provided (both for new and existing files) # Associate url if provided (both for new and existing files)
if url: if url:
debug(
f"{self._log_prefix()} Associating {len(url)} URL(s) with file"
)
for url in url: for url in url:
if url: if url:
try: try:
client.associate_url(file_hash, str(url)) client.associate_url(file_hash, str(url))
debug(f"{self._log_prefix()} Associated URL: {url}")
except Exception as exc: except Exception as exc:
log( log(
f"{self._log_prefix()} ⚠️ Failed to associate URL {url}: {exc}", f"{self._log_prefix()} ⚠️ Failed to associate URL {url}: {exc}",
@@ -634,7 +621,6 @@ class HydrusNetwork(Store):
raise Exception("Hydrus client unavailable") raise Exception("Hydrus client unavailable")
prefix = self._log_prefix() prefix = self._log_prefix()
debug(f"{prefix} Searching for: {query}")
def _extract_urls(meta_obj: Any) -> list[str]: def _extract_urls(meta_obj: Any) -> list[str]:
if not isinstance(meta_obj, dict): if not isinstance(meta_obj, dict):
@@ -720,6 +706,70 @@ class HydrusNetwork(Store):
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
"""Best-effort URL search by scanning Hydrus metadata with include_file_url=True.""" """Best-effort URL search by scanning Hydrus metadata with include_file_url=True."""
try:
from API.HydrusNetwork import _generate_hydrus_url_variants
except Exception:
_generate_hydrus_url_variants = None # type: ignore[assignment]
def _normalize_url_match_token(value: str | None) -> str:
token = str(value or "").strip()
if not token:
return ""
token = token.split("#", 1)[0]
try:
parsed_url = urlsplit(token)
except Exception:
return token.lower()
if parsed_url.scheme and parsed_url.scheme.lower() not in {"http", "https"}:
return token.lower()
netloc = str(parsed_url.netloc or "").strip().lower()
if netloc.startswith("www."):
netloc = netloc[4:]
try:
query_pairs = parse_qsl(parsed_url.query, keep_blank_values=True)
except Exception:
query_pairs = []
filtered_pairs = []
for key, val in query_pairs:
key_norm = str(key or "").lower()
if key_norm in {"t", "start", "time_continue", "timestamp", "time", "begin"}:
continue
if key_norm.startswith("utm_"):
continue
filtered_pairs.append((key, val))
normalized_query = urlencode(filtered_pairs, doseq=True) if filtered_pairs else ""
normalized = urlunsplit(("", netloc, parsed_url.path or "", normalized_query, ""))
return str(normalized or token).lstrip("/").lower()
def _append_url_needles(output: list[str], candidate: str | None) -> None:
raw_candidate = str(candidate or "").strip()
if not raw_candidate:
return
expanded_candidates = [raw_candidate]
if callable(_generate_hydrus_url_variants):
try:
expanded_candidates.extend(_generate_hydrus_url_variants(raw_candidate) or [])
except Exception:
pass
for expanded in expanded_candidates:
expanded_text = str(expanded or "").strip()
if not expanded_text:
continue
lowered = expanded_text.lower()
if lowered and lowered not in output:
output.append(lowered)
normalized = _normalize_url_match_token(expanded_text)
if normalized and normalized not in output:
output.append(normalized)
candidate_file_ids: list[int] = [] candidate_file_ids: list[int] = []
candidate_hashes: list[str] = [] candidate_hashes: list[str] = []
seen_file_ids: set[int] = set() seen_file_ids: set[int] = set()
@@ -775,13 +825,9 @@ class HydrusNetwork(Store):
needle_list: list[str] = [] needle_list: list[str] = []
if isinstance(needles, (list, tuple, set)): if isinstance(needles, (list, tuple, set)):
for item in needles: for item in needles:
text = str(item or "").strip().lower() _append_url_needles(needle_list, str(item or ""))
if text and text not in needle_list:
needle_list.append(text)
if not needle_list: if not needle_list:
needle = (url_value or "").strip().lower() _append_url_needles(needle_list, url_value)
if needle:
needle_list = [needle]
chunk_size = 200 chunk_size = 200
out: list[dict[str, Any]] = [] out: list[dict[str, Any]] = []
if scan_limit is None: if scan_limit is None:
@@ -855,7 +901,14 @@ class HydrusNetwork(Store):
continue continue
if not needle_list: if not needle_list:
continue continue
if any(any(n in u.lower() for n in needle_list) for u in urls): normalized_urls = [_normalize_url_match_token(u) for u in urls]
if any(
any(
n in str(u or "").lower() or (normalized_urls[idx] and n in normalized_urls[idx])
for idx, u in enumerate(urls)
)
for n in needle_list
):
out.append(meta) out.append(meta)
continue continue
@@ -1001,7 +1054,8 @@ class HydrusNetwork(Store):
return ids_out, hashes_out return ids_out, hashes_out
query_lower = query.lower().strip() raw_query = str(query or "").strip()
query_lower = raw_query.lower().strip()
# Support `ext:<value>` anywhere in the query. We filter results by the # Support `ext:<value>` anywhere in the query. We filter results by the
# Hydrus metadata extension field. # Hydrus metadata extension field.
@@ -1055,10 +1109,10 @@ class HydrusNetwork(Store):
hashes: list[str] = [] hashes: list[str] = []
file_ids: list[int] = [] file_ids: list[int] = []
if ":" in query_lower and not query_lower.startswith(":"): if ":" in raw_query and not raw_query.startswith(":"):
namespace, pattern = query_lower.split(":", 1) namespace_raw, pattern_raw = raw_query.split(":", 1)
namespace = namespace.strip().lower() namespace = namespace_raw.strip().lower()
pattern = pattern.strip() pattern = pattern_raw.strip()
if namespace == "url": if namespace == "url":
try: try:
fetch_limit_raw = int(limit) if limit else 100 fetch_limit_raw = int(limit) if limit else 100
@@ -1066,7 +1120,7 @@ class HydrusNetwork(Store):
fetch_limit_raw = 100 fetch_limit_raw = 100
if url_only: if url_only:
metadata_list = _search_url_query_metadata( metadata_list = _search_url_query_metadata(
query_lower, f"url:{pattern}",
fetch_limit_raw, fetch_limit_raw,
minimal=minimal, minimal=minimal,
) )
@@ -1092,12 +1146,13 @@ class HydrusNetwork(Store):
token = str(value or "").strip().lower() token = str(value or "").strip().lower()
if not token: if not token:
return "" return ""
return token.replace("*", "").replace("?", "") return token.replace("*", "")
# Fast-path: exact URL via /add_urls/get_url_files when a full URL is provided. # Fast-path: exact URL via /add_urls/get_url_files when a full URL is provided.
exact_url_attempted = False exact_url_attempted = False
try: try:
if pattern.startswith("http://") or pattern.startswith("https://"): has_wildcards = ("*" in pattern)
if (pattern.startswith("http://") or pattern.startswith("https://")) and not has_wildcards:
exact_url_attempted = True exact_url_attempted = True
metadata_list = self.lookup_url_metadata(pattern, minimal=minimal) metadata_list = self.lookup_url_metadata(pattern, minimal=minimal)
except Exception: except Exception:
@@ -1123,7 +1178,7 @@ class HydrusNetwork(Store):
minimal=minimal, minimal=minimal,
) )
elif namespace == "system": elif namespace == "system":
normalized_system_predicate = pattern.strip() normalized_system_predicate = pattern.strip().lower()
if normalized_system_predicate == "has url": if normalized_system_predicate == "has url":
try: try:
fetch_limit = int(limit) if limit else 100 fetch_limit = int(limit) if limit else 100
@@ -1173,7 +1228,6 @@ class HydrusNetwork(Store):
if freeform_union_search: if freeform_union_search:
if not title_predicates and not freeform_predicates: if not title_predicates and not freeform_predicates:
debug(f"{prefix} 0 result(s)")
return [] return []
payloads: list[Any] = [] payloads: list[Any] = []
@@ -1223,7 +1277,6 @@ class HydrusNetwork(Store):
hashes = [] hashes = []
else: else:
if not tags: if not tags:
debug(f"{prefix} 0 result(s)")
return [] return []
search_result = client.search_files( search_result = client.search_files(
@@ -1239,7 +1292,6 @@ class HydrusNetwork(Store):
if ext_only and ext_filter: if ext_only and ext_filter:
results = [] results = []
if not file_ids and not hashes: if not file_ids and not hashes:
debug(f"{prefix} 0 result(s)")
return [] return []
# Prefer file_ids if available. # Prefer file_ids if available.
@@ -1301,14 +1353,11 @@ class HydrusNetwork(Store):
"ext": _resolve_ext_from_meta(meta, mime_type), "ext": _resolve_ext_from_meta(meta, mime_type),
} }
) )
debug(f"{prefix} {len(results)} result(s)")
return results[:limit] return results[:limit]
# If we only got hashes, fall back to the normal flow below. # If we only got hashes, fall back to the normal flow below.
if not file_ids and not hashes: if not file_ids and not hashes:
debug(f"{prefix} 0 result(s)")
return [] return []
file_ids, hashes = _cap_metadata_candidates( file_ids, hashes = _cap_metadata_candidates(
@@ -1457,8 +1506,6 @@ class HydrusNetwork(Store):
"ext": ext, "ext": ext,
} }
) )
debug(f"{prefix} {len(results)} result(s)")
if ext_filter: if ext_filter:
wanted = ext_filter wanted = ext_filter
filtered: list[dict[str, Any]] = [] filtered: list[dict[str, Any]] = []
@@ -2110,6 +2157,31 @@ class HydrusNetwork(Store):
seen_ids.add(file_id) seen_ids.add(file_id)
file_ids.append(file_id) file_ids.append(file_id)
statuses = response.get("url_file_statuses")
if isinstance(statuses, list):
for entry in statuses:
if not isinstance(entry, dict):
continue
status_hash = entry.get("hash") or entry.get("file_hash")
try:
normalized_hash = str(status_hash or "").strip().lower()
except Exception:
normalized_hash = ""
if normalized_hash and normalized_hash not in seen_hashes:
seen_hashes.add(normalized_hash)
hashes.append(normalized_hash)
status_id = entry.get("file_id") or entry.get("fileid")
try:
file_id = int(status_id) if status_id is not None else None
except (TypeError, ValueError):
file_id = None
if file_id is None or file_id in seen_ids:
continue
seen_ids.add(file_id)
file_ids.append(file_id)
for key in ("normalized_url", "redirect_url", "url"): for key in ("normalized_url", "redirect_url", "url"):
value = response.get(key) value = response.get(key)
if isinstance(value, str): if isinstance(value, str):
+70 -20
View File
@@ -14,7 +14,7 @@ from collections.abc import Iterable as IterableABC
from functools import lru_cache from functools import lru_cache
from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
from SYS.logger import log, debug from SYS.logger import log, debug, debug_panel
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -3158,6 +3158,8 @@ def check_url_exists_in_storage(
storage: Any, storage: Any,
hydrus_available: bool, hydrus_available: bool,
final_output_dir: Optional[Path] = None, final_output_dir: Optional[Path] = None,
*,
auto_continue_duplicates: bool = True,
) -> bool: ) -> bool:
"""Pre-flight check to see if URLs already exist in storage. """Pre-flight check to see if URLs already exist in storage.
@@ -3187,7 +3189,6 @@ def check_url_exists_in_storage(
in_pipeline = bool(stage_ctx is not None or ("|" in str(current_cmd_text or ""))) in_pipeline = bool(stage_ctx is not None or ("|" in str(current_cmd_text or "")))
start_time = time.monotonic() start_time = time.monotonic()
time_budget = 45.0 time_budget = 45.0
debug(f"[preflight] check_url_exists_in_storage: checking {len(urls)} url(s)")
if in_pipeline: if in_pipeline:
try: try:
already_checked = bool( already_checked = bool(
@@ -3243,7 +3244,7 @@ def check_url_exists_in_storage(
return False return False
return False return False
if in_pipeline: if in_pipeline and auto_continue_duplicates:
try: try:
cached_cmd = pipeline_context.load_value("preflight.url_duplicates.command", default="") cached_cmd = pipeline_context.load_value("preflight.url_duplicates.command", default="")
cached_decision = pipeline_context.load_value("preflight.url_duplicates.continue", default=None) cached_decision = pipeline_context.load_value("preflight.url_duplicates.continue", default=None)
@@ -3706,6 +3707,20 @@ def check_url_exists_in_storage(
debug("Bulk URL preflight skipped: no searchable backends") debug("Bulk URL preflight skipped: no searchable backends")
return True return True
try:
debug_panel(
"URL preflight",
[
("url_count", len(unique_urls)),
("pipeline", in_pipeline),
("bulk_mode", bulk_mode),
("backends", ", ".join(str(name) for name in backend_names)),
],
border_style="yellow",
)
except Exception:
pass
seen_pairs: set[tuple[str, str]] = set() seen_pairs: set[tuple[str, str]] = set()
matched_urls: set[str] = set() matched_urls: set[str] = set()
match_rows: List[Dict[str, Any]] = [] match_rows: List[Dict[str, Any]] = []
@@ -3726,12 +3741,7 @@ def check_url_exists_in_storage(
except Exception: except Exception:
continue continue
debug(f"[preflight] Scanning backend: {backend_name}")
if HydrusNetwork is not None and isinstance(backend, HydrusNetwork): if HydrusNetwork is not None and isinstance(backend, HydrusNetwork):
client = getattr(backend, "_client", None)
if client is None:
continue
if not hydrus_available: if not hydrus_available:
debug("Bulk URL preflight: global Hydrus availability check failed; attempting per-backend best-effort lookup") debug("Bulk URL preflight: global Hydrus availability check failed; attempting per-backend best-effort lookup")
@@ -3748,7 +3758,33 @@ def check_url_exists_in_storage(
found_hash: Optional[str] = None found_hash: Optional[str] = None
found = False found = False
lookup_exact = getattr(backend, "find_hashes_by_url", None)
if callable(lookup_exact):
for needle in [original_url, *(needles or [])][:7]:
needle_text = str(needle or "").strip()
if not _httpish(needle_text):
continue
try:
exact_hashes = lookup_exact(needle_text) or []
except Exception:
continue
if not isinstance(exact_hashes, list) or not exact_hashes:
continue
try:
found_hash = str(exact_hashes[0] or "").strip().lower()
except Exception:
found_hash = None
found = True
break
client = getattr(backend, "_client", None)
if found:
pass
elif client is None:
continue
for needle in (needles or [])[:6]: for needle in (needles or [])[:6]:
if found:
break
if not _httpish(needle): if not _httpish(needle):
continue continue
try: try:
@@ -3868,7 +3904,6 @@ def check_url_exists_in_storage(
match_rows.append(display_row) match_rows.append(display_row)
if not match_rows: if not match_rows:
debug("Bulk URL preflight: no matches")
if in_pipeline: if in_pipeline:
preflight_cache = _load_preflight_cache() preflight_cache = _load_preflight_cache()
url_dup_cache = preflight_cache.get("url_duplicates") url_dup_cache = preflight_cache.get("url_duplicates")
@@ -3935,24 +3970,39 @@ def check_url_exists_in_storage(
auto_confirm_reason = "non-interactive stdin" auto_confirm_reason = "non-interactive stdin"
answered_yes = True answered_yes = True
auto_declined = False
with cm: with cm:
get_stderr_console().print(table) get_stderr_console().print(table)
setattr(table, "_rendered_by_cmdlet", True) setattr(table, "_rendered_by_cmdlet", True)
if auto_confirm_reason is None: if auto_confirm_reason is None:
answered_yes = bool(Confirm.ask("Continue anyway?", default=False, console=get_stderr_console())) answered_yes = bool(Confirm.ask("Continue anyway?", default=False, console=get_stderr_console()))
else: else:
debug( answered_yes = bool(auto_continue_duplicates)
f"Bulk URL preflight auto-confirmed duplicates ({auto_confirm_reason}); continuing without user input." auto_declined = not answered_yes
) if answered_yes:
try: debug(
log( f"Bulk URL preflight auto-confirmed duplicates ({auto_confirm_reason}); continuing without user input."
f"Auto-confirmed duplicate URL warning ({auto_confirm_reason}). Continuing...",
file=sys.stderr,
) )
except Exception: try:
pass log(
f"Auto-confirmed duplicate URL warning ({auto_confirm_reason}). Continuing...",
file=sys.stderr,
)
except Exception:
pass
else:
debug(
f"Bulk URL preflight auto-skipped duplicates ({auto_confirm_reason}); skipping without user input."
)
try:
log(
f"Duplicate URL detected ({auto_confirm_reason}). Skipping download.",
file=sys.stderr,
)
except Exception:
pass
if in_pipeline: if in_pipeline and auto_continue_duplicates:
try: try:
existing = pipeline_context.load_value("preflight", default=None) existing = pipeline_context.load_value("preflight", default=None)
except Exception: except Exception:
@@ -3977,7 +4027,7 @@ def check_url_exists_in_storage(
pass pass
if not answered_yes: if not answered_yes:
if in_pipeline: if in_pipeline and not auto_declined:
try: try:
pipeline_context.request_pipeline_stop(reason="duplicate-url declined", exit_code=0) pipeline_context.request_pipeline_stop(reason="duplicate-url declined", exit_code=0)
except Exception: except Exception:
+68 -32
View File
@@ -11,7 +11,7 @@ from urllib.parse import urlparse
from SYS import models from SYS import models
from SYS import pipeline as ctx from SYS import pipeline as ctx
from SYS.logger import log, debug, is_debug_enabled from SYS.logger import log, debug, debug_panel, is_debug_enabled
from SYS.payload_builders import build_table_result_payload from SYS.payload_builders import build_table_result_payload
from SYS.pipeline_progress import PipelineProgress from SYS.pipeline_progress import PipelineProgress
from SYS.result_publication import overlay_existing_result_table, publish_result_table from SYS.result_publication import overlay_existing_result_table, publish_result_table
@@ -247,11 +247,13 @@ class Add_File(Cmdlet):
is None) or bool(getattr(stage_ctx, is None) or bool(getattr(stage_ctx,
"is_last_stage", "is_last_stage",
False)) False))
has_downstream_stage = bool(stage_ctx is not None and not is_last_stage)
# Directory-mode selector: # Directory-mode selector:
# - First pass: `add-file -store X -path <DIR>` should ONLY show a selectable table. # - Terminal use: `add-file -store X -path <DIR>` shows a selectable table.
# - Second pass (triggered by @ selection expansion): re-run add-file with `-path file1,file2,...` # - Pipelined use: `add-file -store X -path <DIR> | ...` processes the full batch
# and actually ingest/copy. # immediately so downstream stages receive the uploaded items.
# - Selection replay: `@N` re-runs add-file with `-path file1,file2,...`.
dir_scan_mode = False dir_scan_mode = False
dir_scan_results: Optional[List[Dict[str, Any]]] = None dir_scan_results: Optional[List[Dict[str, Any]]] = None
explicit_path_list_results: Optional[List[Dict[str, Any]]] = None explicit_path_list_results: Optional[List[Dict[str, Any]]] = None
@@ -350,6 +352,19 @@ class Add_File(Cmdlet):
total_items = len(items_to_process) if isinstance(items_to_process, list) else 0 total_items = len(items_to_process) if isinstance(items_to_process, list) else 0
processed_items = 0 processed_items = 0
try:
ui, _ = progress.ui_and_pipe_index()
if ui is not None and total_items:
preview_items = (
list(items_to_process)
if isinstance(items_to_process, list) else [items_to_process]
)
progress.begin_pipe(
total_items=total_items,
items_preview=preview_items,
)
except Exception:
pass
try: try:
if total_items: if total_items:
progress.set_percent(0) progress.set_percent(0)
@@ -369,12 +384,20 @@ class Add_File(Cmdlet):
except Exception: except Exception:
use_steps = False use_steps = False
debug(f"[add-file] INPUT result type={type(result).__name__}") try:
if isinstance(result, list): debug_panel(
debug(f"[add-file] INPUT result is list with {len(result)} items") "add-file",
debug( [
f"[add-file] PARSED args: location={location}, provider={provider_name}, delete={delete_after}" ("result_type", type(result).__name__),
) ("items", total_items),
("location", location),
("provider", provider_name),
("delete", delete_after),
],
border_style="cyan",
)
except Exception:
pass
# add-file is ingestion-only: it does not download URLs here. # add-file is ingestion-only: it does not download URLs here.
@@ -393,22 +416,22 @@ class Add_File(Cmdlet):
except Exception: except Exception:
po = None po = None
if po is None: if po is None:
debug(f"[add-file] PIPE item[{idx}] preview (non-PipeObject)")
continue continue
debug(f"[add-file] PIPE item[{idx}] PipeObject preview")
try: try:
safe_po = _sanitize_pipe_object_for_debug(po) safe_po = _sanitize_pipe_object_for_debug(po)
safe_po.debug_table() safe_po.debug_table()
except Exception: except Exception:
pass pass
if len(preview_items) > max_preview:
debug(
f"[add-file] Skipping {len(preview_items) - max_preview} additional piped item(s) in debug preview"
)
# If this invocation was directory selector mode, show a selectable table and stop. should_present_directory_selector = bool(dir_scan_mode and not has_downstream_stage)
if dir_scan_mode and has_downstream_stage:
debug(
"[add-file] Continuing with directory batch ingest because downstream stages exist"
)
# If this invocation was terminal directory selector mode, show a selectable table and stop.
# The user then runs @N (optionally piped), which replays add-file with selected paths. # The user then runs @N (optionally piped), which replays add-file with selected paths.
if dir_scan_mode: if should_present_directory_selector:
try: try:
from SYS.result_table import Table from SYS.result_table import Table
from pathlib import Path as _Path from pathlib import Path as _Path
@@ -563,13 +586,19 @@ class Add_File(Cmdlet):
media_path, file_hash, temp_dir_to_cleanup = Add_File._download_provider_source( media_path, file_hash, temp_dir_to_cleanup = Add_File._download_provider_source(
pipe_obj, config, storage_registry pipe_obj, config, storage_registry
) )
if media_path: if media_path:
debug( try:
f"[add-file] Provider source downloaded: {media_path}" debug_panel(
f"add-file source {idx}/{max(1, total_items)}",
[
("path", media_path),
("hash", file_hash or "N/A"),
("provider", provider_name or "local"),
],
border_style="green",
) )
debug( except Exception:
f"[add-file] RESOLVED source: path={media_path}, hash={file_hash if file_hash else 'N/A'}..." pass
)
if not media_path: if not media_path:
failures += 1 failures += 1
continue continue
@@ -1616,6 +1645,7 @@ class Add_File(Cmdlet):
) -> None: ) -> None:
pipe_obj.hash = hash_value pipe_obj.hash = hash_value
pipe_obj.store = store pipe_obj.store = store
pipe_obj.is_temp = False
pipe_obj.path = path pipe_obj.path = path
pipe_obj.tag = tag pipe_obj.tag = tag
if title: if title:
@@ -2211,11 +2241,20 @@ class Add_File(Cmdlet):
upload_tags = tags upload_tags = tags
if prefer_defer_tags and upload_tags: if prefer_defer_tags and upload_tags:
upload_tags = [] upload_tags = []
debug(f"[add-file] Deferring tag application for {backend_name} (backend preference)") try:
debug_panel(
debug( "add-file store",
f"[add-file] Storing into backend '{backend_name}' path='{media_path}' title='{title}' hash='{f_hash[:12] if f_hash else 'N/A'}'" [
) ("backend", backend_name),
("path", media_path),
("title", title),
("hash_hint", f_hash[:12] if f_hash else "N/A"),
("defer_tags", bool(prefer_defer_tags and tags)),
],
border_style="yellow",
)
except Exception:
pass
# Call backend's add_file with full metadata # Call backend's add_file with full metadata
# Backend returns hash as identifier. If we already know the hash from _resolve_source # Backend returns hash as identifier. If we already know the hash from _resolve_source
@@ -2227,9 +2266,6 @@ class Add_File(Cmdlet):
url=[] if (defer_url_association and url) else url, url=[] if (defer_url_association and url) else url,
file_hash=f_hash, file_hash=f_hash,
) )
debug(
f"[add-file] backend.add_file returned identifier {file_identifier} (len={len(str(file_identifier)) if file_identifier is not None else 'None'})"
)
##log(f"✓ File added to '{backend_name}': {file_identifier}", file=sys.stderr) ##log(f"✓ File added to '{backend_name}': {file_identifier}", file=sys.stderr)
stored_path: Optional[str] = None stored_path: Optional[str] = None
+78 -38
View File
@@ -18,7 +18,7 @@ from contextlib import AbstractContextManager, nullcontext
from API.HTTP import _download_direct_file from API.HTTP import _download_direct_file
from SYS.models import DownloadError, DownloadOptions, DownloadMediaResult from SYS.models import DownloadError, DownloadOptions, DownloadMediaResult
from SYS.logger import log, debug, is_debug_enabled from SYS.logger import log, debug, debug_panel, is_debug_enabled
from SYS.payload_builders import build_file_result_payload, build_table_result_payload from SYS.payload_builders import build_file_result_payload, build_table_result_payload
from SYS.pipeline_progress import PipelineProgress from SYS.pipeline_progress import PipelineProgress
from SYS.result_table import Table from SYS.result_table import Table
@@ -113,7 +113,17 @@ class Download_File(Cmdlet):
def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int: def run(self, result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
"""Main execution method.""" """Main execution method."""
debug(f"[download-file] run invoked with args: {list(args)}") try:
debug_panel(
"download-file",
[
("args", list(args)),
("has_piped_input", bool(result)),
],
border_style="cyan",
)
except Exception:
debug(f"[download-file] run invoked with args: {list(args)}")
return self._run_impl(result, args, config) return self._run_impl(result, args, config)
@staticmethod @staticmethod
@@ -1008,7 +1018,6 @@ class Download_File(Cmdlet):
from Store import Store from Store import Store
from API.HydrusNetwork import is_hydrus_available from API.HydrusNetwork import is_hydrus_available
debug("[download-file] Initializing storage interface...")
storage = Store(config=config or {}, suppress_debug=True) storage = Store(config=config or {}, suppress_debug=True)
hydrus_available = bool(is_hydrus_available(config or {})) hydrus_available = bool(is_hydrus_available(config or {}))
@@ -1126,7 +1135,6 @@ class Download_File(Cmdlet):
@staticmethod @staticmethod
def _canonicalize_url_for_storage(*, requested_url: str, ytdlp_tool: YtDlpTool, playlist_items: Optional[str]) -> str: def _canonicalize_url_for_storage(*, requested_url: str, ytdlp_tool: YtDlpTool, playlist_items: Optional[str]) -> str:
if playlist_items: if playlist_items:
debug(f"[download-file] Skipping canonicalization for playlist item(s): {playlist_items}")
return str(requested_url) return str(requested_url)
try: try:
cf = None cf = None
@@ -1137,15 +1145,12 @@ class Download_File(Cmdlet):
except Exception: except Exception:
cf = None cf = None
debug(f"[download-file] Canonicalizing URL: {requested_url}")
pr = probe_url(requested_url, no_playlist=False, timeout_seconds=15, cookiefile=cf) pr = probe_url(requested_url, no_playlist=False, timeout_seconds=15, cookiefile=cf)
if isinstance(pr, dict): if isinstance(pr, dict):
for key in ("webpage_url", "original_url", "url", "requested_url"): for key in ("webpage_url", "original_url", "url", "requested_url"):
value = pr.get(key) value = pr.get(key)
if isinstance(value, str) and value.strip(): if isinstance(value, str) and value.strip():
canon = value.strip() canon = value.strip()
if canon != requested_url:
debug(f"[download-file] Resolved canonical URL: {requested_url} -> {canon}")
return canon return canon
except Exception as e: except Exception as e:
debug(f"[download-file] Canonicalization error for {requested_url}: {e}") debug(f"[download-file] Canonicalization error for {requested_url}: {e}")
@@ -1180,7 +1185,8 @@ class Download_File(Cmdlet):
urls=unique_to_check, urls=unique_to_check,
storage=storage, storage=storage,
hydrus_available=hydrus_available, hydrus_available=hydrus_available,
final_output_dir=final_output_dir final_output_dir=final_output_dir,
auto_continue_duplicates=False,
) )
def _preflight_url_duplicates_bulk( def _preflight_url_duplicates_bulk(
@@ -1204,7 +1210,8 @@ class Download_File(Cmdlet):
urls=unique_urls, urls=unique_urls,
storage=storage, storage=storage,
hydrus_available=hydrus_available, hydrus_available=hydrus_available,
final_output_dir=final_output_dir final_output_dir=final_output_dir,
auto_continue_duplicates=False,
) )
@@ -1512,6 +1519,7 @@ class Download_File(Cmdlet):
download_timeout_seconds: int, download_timeout_seconds: int,
) -> int: ) -> int:
downloaded_count = 0 downloaded_count = 0
duplicate_skipped_count = 0
downloaded_pipe_objects: List[Dict[str, Any]] = [] downloaded_pipe_objects: List[Dict[str, Any]] = []
pipe_seq = 0 pipe_seq = 0
clip_sections_spec = self._build_clip_sections_spec(clip_ranges) clip_sections_spec = self._build_clip_sections_spec(clip_ranges)
@@ -1527,9 +1535,6 @@ class Download_File(Cmdlet):
for url_index, url in enumerate(supported_url, 1): for url_index, url in enumerate(supported_url, 1):
try: try:
debug(f"[download-file] Processing URL in loop: {url}")
debug(f"[download-file] ytdl_format parameter passed in: {ytdl_format}")
display_total = batch_total if batch_total > 0 else total_urls display_total = batch_total if batch_total > 0 else total_urls
display_index = batch_index if batch_total > 0 else url_index display_index = batch_index if batch_total > 0 else url_index
display_label = batch_label or str(url) display_label = batch_label or str(url)
@@ -1543,7 +1548,6 @@ class Download_File(Cmdlet):
) )
if not skip_per_url_preflight: if not skip_per_url_preflight:
debug(f"[download-file] Running duplicate preflight for: {canonical_url}")
if not self._preflight_url_duplicate( if not self._preflight_url_duplicate(
storage=storage, storage=storage,
hydrus_available=hydrus_available, hydrus_available=hydrus_available,
@@ -1551,9 +1555,25 @@ class Download_File(Cmdlet):
candidate_url=canonical_url, candidate_url=canonical_url,
extra_urls=[url], extra_urls=[url],
): ):
duplicate_skipped_count += 1
log(f"Skipping download (duplicate found): {url}", file=sys.stderr) log(f"Skipping download (duplicate found): {url}", file=sys.stderr)
continue continue
try:
debug_panel(
f"Download item {display_index}/{display_total or total_urls}",
[
("url", url),
("canonical_url", canonical_url),
("mode", mode),
("format", ytdl_format or "auto"),
("duplicate_preflight", not skip_per_url_preflight),
],
border_style="green",
)
except Exception:
pass
if aggregate_status_mode: if aggregate_status_mode:
try: try:
if display_total > 0: if display_total > 0:
@@ -1683,10 +1703,6 @@ class Download_File(Cmdlet):
except Exception as e: except Exception as e:
pass pass
debug(
"[download-file] Resolved format for download: "
f"mode={mode}, format={actual_format or 'default'}, playlist_items={actual_playlist_items}"
)
attempted_single_format_fallback = False attempted_single_format_fallback = False
attempted_audio_fallback_specific = False attempted_audio_fallback_specific = False
@@ -1709,9 +1725,7 @@ class Download_File(Cmdlet):
if not aggregate_status_mode: if not aggregate_status_mode:
PipelineProgress(pipeline_context).step("downloading") PipelineProgress(pipeline_context).step("downloading")
debug(f"Starting download for {url} (format: {actual_format or 'default'}) with {download_timeout_seconds}s activity timeout...")
result_obj = _download_with_timeout(opts, timeout_seconds=download_timeout_seconds, config=config) result_obj = _download_with_timeout(opts, timeout_seconds=download_timeout_seconds, config=config)
debug(f"Download completed for {url}, building pipe object...")
break break
except DownloadError as e: except DownloadError as e:
cause = getattr(e, "__cause__", None) cause = getattr(e, "__cause__", None)
@@ -2028,8 +2042,6 @@ class Download_File(Cmdlet):
except Exception: except Exception:
pass pass
debug(f"Emitting {len(pipe_objects)} result(s) to pipeline...")
if not aggregate_status_mode: if not aggregate_status_mode:
PipelineProgress(pipeline_context).step("finalized") PipelineProgress(pipeline_context).step("finalized")
@@ -2049,7 +2061,17 @@ class Download_File(Cmdlet):
pass pass
downloaded_count += len(pipe_objects) downloaded_count += len(pipe_objects)
debug("✓ Downloaded and emitted") try:
debug_panel(
"download-file result",
[
("emitted", len(pipe_objects)),
("url", url),
],
border_style="green",
)
except Exception:
pass
except DownloadError as e: except DownloadError as e:
log(f"Download failed for {url}: {e}", file=sys.stderr) log(f"Download failed for {url}: {e}", file=sys.stderr)
@@ -2057,7 +2079,8 @@ class Download_File(Cmdlet):
log(f"Error processing {url}: {e}", file=sys.stderr) log(f"Error processing {url}: {e}", file=sys.stderr)
if downloaded_count > 0: if downloaded_count > 0:
debug(f"✓ Successfully processed {downloaded_count} URL(s)") return 0
if duplicate_skipped_count > 0:
return 0 return 0
log("No downloads completed", file=sys.stderr) log("No downloads completed", file=sys.stderr)
@@ -2072,7 +2095,6 @@ class Download_File(Cmdlet):
parsed: Dict[str, Any], parsed: Dict[str, Any],
) -> int: ) -> int:
try: try:
debug("Starting streaming download handler")
suppress_nested, _batch_total, _batch_index, _batch_label = self._batch_progress_state(config) suppress_nested, _batch_total, _batch_index, _batch_label = self._batch_progress_state(config)
ytdlp_tool = YtDlpTool(config) ytdlp_tool = YtDlpTool(config)
@@ -2091,21 +2113,18 @@ class Download_File(Cmdlet):
if not final_output_dir: if not final_output_dir:
return 1 return 1
debug(f"Output directory: {final_output_dir}")
progress = PipelineProgress(pipeline_context) progress = PipelineProgress(pipeline_context)
using_shared_ui = pipeline_context.get_stage_context() is not None
try: try:
# If we are already in a pipeline stage, the parent UI is already handling progress. # If we are already in a pipeline stage, the parent UI is already handling progress.
# Calling ensure_local_ui can cause re-initialization hangs on some platforms. # Calling ensure_local_ui can cause re-initialization hangs on some platforms.
if pipeline_context.get_stage_context() is None: if not using_shared_ui:
debug("[download-file] Initializing local UI...")
progress.ensure_local_ui( progress.ensure_local_ui(
label="download-file", label="download-file",
total_items=len(supported_url), total_items=len(supported_url),
items_preview=supported_url, items_preview=supported_url,
) )
else: else:
debug("[download-file] Skipping local UI: running inside pipeline stage")
try: try:
if not suppress_nested: if not suppress_nested:
progress.begin_pipe( progress.begin_pipe(
@@ -2116,8 +2135,6 @@ class Download_File(Cmdlet):
debug(f"[download-file] PipelineProgress begin_pipe error: {err}") debug(f"[download-file] PipelineProgress begin_pipe error: {err}")
except Exception as e: except Exception as e:
debug(f"[download-file] PipelineProgress update error: {e}") debug(f"[download-file] PipelineProgress update error: {e}")
debug("[download-file] Parsing clip and query specs...")
clip_spec = parsed.get("clip") clip_spec = parsed.get("clip")
query_spec = parsed.get("query") query_spec = parsed.get("query")
@@ -2223,7 +2240,6 @@ class Download_File(Cmdlet):
ytdl_format = query_format ytdl_format = query_format
if not ytdl_format: if not ytdl_format:
debug(f"[download-file] Checking for playlist at {candidate_url}...")
if self._maybe_show_playlist_table(url=candidate_url, ytdlp_tool=ytdlp_tool): if self._maybe_show_playlist_table(url=candidate_url, ytdlp_tool=ytdlp_tool):
playlist_selection_handled = True playlist_selection_handled = True
# ... (existing logging code) ... # ... (existing logging code) ...
@@ -2270,7 +2286,6 @@ class Download_File(Cmdlet):
forced_single_format_id = None forced_single_format_id = None
forced_single_format_for_batch = False forced_single_format_for_batch = False
debug("[download-file] Checking if format table should be shown...")
early_ret = self._maybe_show_format_table_for_single_url( early_ret = self._maybe_show_format_table_for_single_url(
mode=mode, mode=mode,
clip_spec=clip_spec, clip_spec=clip_spec,
@@ -2343,7 +2358,23 @@ class Download_File(Cmdlet):
except Exception: except Exception:
timeout_seconds = 300 timeout_seconds = 300
debug(f"[download-file] Proceeding to final download call for {len(supported_url)} URL(s)...") try:
debug_panel(
"Streaming download",
[
("urls", len(supported_url)),
("mode", mode),
("format", ytdl_format or "auto"),
("output_dir", final_output_dir),
("ui", "shared pipeline" if using_shared_ui else "local"),
("playlist_items", playlist_items),
("skip_preflight", skip_per_url_preflight),
("timeout_seconds", timeout_seconds),
],
border_style="blue",
)
except Exception:
pass
return self._download_supported_urls( return self._download_supported_urls(
supported_url=supported_url, supported_url=supported_url,
ytdlp_tool=ytdlp_tool, ytdlp_tool=ytdlp_tool,
@@ -2855,8 +2886,6 @@ class Download_File(Cmdlet):
prev_progress = None prev_progress = None
had_progress_key = False had_progress_key = False
try: try:
debug("Starting download-file")
# Allow providers to tap into the active PipelineProgress (optional). # Allow providers to tap into the active PipelineProgress (optional).
try: try:
if isinstance(config, dict): if isinstance(config, dict):
@@ -3117,7 +3146,6 @@ class Download_File(Cmdlet):
streaming_exit_code: Optional[int] = None streaming_exit_code: Optional[int] = None
streaming_downloaded = 0 streaming_downloaded = 0
if supported_streaming: if supported_streaming:
debug(f"[download-file] Using ytdlp provider for {len(supported_streaming)} URL(s)")
streaming_exit_code = self._run_streaming_urls( streaming_exit_code = self._run_streaming_urls(
streaming_urls=supported_streaming, streaming_urls=supported_streaming,
args=args, args=args,
@@ -3161,7 +3189,19 @@ class Download_File(Cmdlet):
if not final_output_dir: if not final_output_dir:
return 1 return 1
debug(f"Output directory: {final_output_dir}") try:
debug_panel(
"download-file plan",
[
("output_dir", final_output_dir),
("streaming_urls", len(supported_streaming)),
("remaining_urls", len(raw_url)),
("piped_items", len(piped_items) if isinstance(piped_items, list) else int(bool(piped_items))),
],
border_style="cyan",
)
except Exception:
debug(f"Output directory: {final_output_dir}")
# If the caller isn't running the shared pipeline Live progress UI (e.g. direct # If the caller isn't running the shared pipeline Live progress UI (e.g. direct
# cmdlet execution), start a minimal local pipeline progress panel so downloads # cmdlet execution), start a minimal local pipeline progress panel so downloads
-8
View File
@@ -2158,11 +2158,7 @@ class search_file(Cmdlet):
) )
db.update_worker_status(worker_id, "error") db.update_worker_status(worker_id, "error")
return 1 return 1
debug(f"[search-file] Searching '{backend_to_search}'")
results = target_backend.search(query, limit=limit) results = target_backend.search(query, limit=limit)
debug(
f"[search-file] '{backend_to_search}' -> {len(results or [])} result(s)"
)
else: else:
all_results = [] all_results = []
store_registry = None store_registry = None
@@ -2184,14 +2180,10 @@ class search_file(Cmdlet):
if type(backend).search is BaseStore.search: if type(backend).search is BaseStore.search:
continue continue
debug(f"[search-file] Searching '{backend_name}'")
backend_results = backend.search( backend_results = backend.search(
query, query,
limit=limit - len(all_results) limit=limit - len(all_results)
) )
debug(
f"[search-file] '{backend_name}' -> {len(backend_results or [])} result(s)"
)
if backend_results: if backend_results:
all_results.extend(backend_results) all_results.extend(backend_results)
if len(all_results) >= limit: if len(all_results) >= limit:
+41 -32
View File
@@ -19,7 +19,7 @@ from urllib.parse import urlparse
from SYS import pipeline as pipeline_context from SYS import pipeline as pipeline_context
from SYS.config import get_nested_config_value as _get_nested from SYS.config import get_nested_config_value as _get_nested
from SYS.logger import debug, log from SYS.logger import debug, debug_panel, log
from SYS.models import ( from SYS.models import (
DebugLogger, DebugLogger,
DownloadError, DownloadError,
@@ -505,7 +505,6 @@ def probe_url(
def _do_probe() -> None: def _do_probe() -> None:
try: try:
debug(f"[probe] Starting probe for {url}")
ensure_yt_dlp_ready() ensure_yt_dlp_ready()
assert yt_dlp is not None assert yt_dlp is not None
@@ -529,9 +528,7 @@ def probe_url(
ydl_opts["noplaylist"] = True ydl_opts["noplaylist"] = True
with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[arg-type] with yt_dlp.YoutubeDL(ydl_opts) as ydl: # type: ignore[arg-type]
debug(f"[probe] ytdlp extract_info (download=False) start: {url}")
info = ydl.extract_info(url, download=False) info = ydl.extract_info(url, download=False)
debug(f"[probe] ytdlp extract_info (download=False) done: {url}")
if not isinstance(info, dict): if not isinstance(info, dict):
result_container[0] = None result_container[0] = None
@@ -550,6 +547,16 @@ def probe_url(
"webpage_url": webpage_url, "webpage_url": webpage_url,
"url": webpage_url or url, "url": webpage_url or url,
} }
debug_panel(
"yt-dlp probe",
[
("url", url),
("extractor", info.get("extractor", "")),
("title", info.get("title", "")),
("playlist", bool(info.get("entries"))),
],
border_style="green",
)
except Exception as exc: except Exception as exc:
debug(f"Probe error for {url}: {exc}") debug(f"Probe error for {url}: {exc}")
result_container[1] = exc result_container[1] = exc
@@ -1112,12 +1119,6 @@ class YtDlpTool:
if clip_safe_fmt != fmt: if clip_safe_fmt != fmt:
debug(f"[ytdlp] clip format normalized: {fmt} -> {clip_safe_fmt}") debug(f"[ytdlp] clip format normalized: {fmt} -> {clip_safe_fmt}")
fmt = clip_safe_fmt fmt = clip_safe_fmt
debug(
"[ytdlp] build options: "
f"mode={opts.mode}, ytdl_format={ytdl_format}, default_format={default_fmt}, final_format={fmt}, "
f"cookiefile={base_options.get('cookiefile')}, cookiesfrombrowser={base_options.get('cookiesfrombrowser')}, "
f"player_client={((base_options.get('extractor_args') or {}).get('youtube') or {}).get('player_client')}"
)
base_options["format"] = fmt base_options["format"] = fmt
if opts.mode == "audio": if opts.mode == "audio":
@@ -1199,9 +1200,6 @@ class YtDlpTool:
if opts.playlist_items: if opts.playlist_items:
base_options["playlist_items"] = opts.playlist_items base_options["playlist_items"] = opts.playlist_items
if not opts.quiet:
debug(f"yt-dlp: mode={opts.mode}, format={base_options.get('format')}, cookiefile={base_options.get('cookiefile')}")
return base_options return base_options
def build_yt_dlp_cli_args( def build_yt_dlp_cli_args(
@@ -2100,7 +2098,6 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
format) are applied when constructing the YtDlpTool instance. format) are applied when constructing the YtDlpTool instance.
""" """
debug(f"[download_media] start: {opts.url}")
try: try:
netloc = urlparse(opts.url).netloc.lower() netloc = urlparse(opts.url).netloc.lower()
except Exception: except Exception:
@@ -2123,9 +2120,6 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
raise DownloadError(msg) raise DownloadError(msg)
if opts.playlist_items: if opts.playlist_items:
debug(
f"Skipping probe for playlist (item selection: {opts.playlist_items}), proceeding with download"
)
probe_result: Optional[Dict[str, Any]] = {"url": opts.url} probe_result: Optional[Dict[str, Any]] = {"url": opts.url}
else: else:
probe_cookiefile = None probe_cookiefile = None
@@ -2138,8 +2132,6 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
probe_result = probe_url(opts.url, no_playlist=opts.no_playlist, timeout_seconds=15, cookiefile=probe_cookiefile) probe_result = probe_url(opts.url, no_playlist=opts.no_playlist, timeout_seconds=15, cookiefile=probe_cookiefile)
if probe_result is None: if probe_result is None:
if not opts.quiet:
debug("yt-dlp probe returned no metadata; continuing with direct download attempt")
if debug_logger is not None: if debug_logger is not None:
debug_logger.write_record("ytdlp-probe-miss-continue", {"url": opts.url}) debug_logger.write_record("ytdlp-probe-miss-continue", {"url": opts.url})
@@ -2155,7 +2147,18 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
if _progress_callback not in hooks: if _progress_callback not in hooks:
hooks.append(_progress_callback) hooks.append(_progress_callback)
if not opts.quiet: if not opts.quiet:
debug(f"Starting yt-dlp download: {opts.url}") debug_panel(
"yt-dlp job",
[
("url", opts.url),
("mode", opts.mode),
("format", ytdl_options.get("format") or "default"),
("cookiefile", ytdl_options.get("cookiefile")),
("download_sections", ytdl_options.get("download_sections") or "None"),
("force_keyframes", ytdl_options.get("force_keyframes_at_cuts", False)),
],
border_style="blue",
)
if debug_logger is not None: if debug_logger is not None:
debug_logger.write_record("ytdlp-start", {"url": opts.url}) debug_logger.write_record("ytdlp-start", {"url": opts.url})
@@ -2164,11 +2167,6 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
session_id = None session_id = None
first_section_info: Dict[str, Any] = {} first_section_info: Dict[str, Any] = {}
try: try:
if not opts.quiet:
if ytdl_options.get("download_sections"):
debug(f"[yt-dlp] download_sections: {ytdl_options['download_sections']}")
debug(f"[yt-dlp] force_keyframes_at_cuts: {ytdl_options.get('force_keyframes_at_cuts', False)}")
if ytdl_options.get("download_sections"): if ytdl_options.get("download_sections"):
live_ui, _ = PipelineProgress(pipeline_context).ui_and_pipe_index() live_ui, _ = PipelineProgress(pipeline_context).ui_and_pipe_index()
quiet_sections = bool(opts.quiet) or (live_ui is not None) quiet_sections = bool(opts.quiet) or (live_ui is not None)
@@ -2354,18 +2352,21 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
media_paths = renamed_media_files if renamed_media_files else None media_paths = renamed_media_files if renamed_media_files else None
if not opts.quiet: if not opts.quiet:
count = len(media_paths) if isinstance(media_paths, list) else 1 count = len(media_paths) if isinstance(media_paths, list) else 1
debug(f"✓ Downloaded {count} section media file(s) (session: {session_id})") debug_panel(
"yt-dlp result",
[
("files", count),
("session", session_id),
("file", media_path.name),
],
border_style="green",
)
else: else:
media_path = files[0] media_path = files[0]
media_paths = None media_paths = None
if not opts.quiet:
debug(f"✓ Downloaded section file (pattern not found): {media_path.name}")
else: else:
media_path = files[0] media_path = files[0]
media_paths = None media_paths = None
if not opts.quiet:
debug(f"✓ Downloaded: {media_path.name}")
if debug_logger is not None: if debug_logger is not None:
debug_logger.write_record("ytdlp-file-found", {"path": str(media_path)}) debug_logger.write_record("ytdlp-file-found", {"path": str(media_path)})
except Exception as exc: except Exception as exc:
@@ -2462,7 +2463,15 @@ def download_media(opts: DownloadOptions, *, config: Optional[Dict[str, Any]] =
source_url = entry.get("webpage_url") or entry.get("original_url") or entry.get("url") source_url = entry.get("webpage_url") or entry.get("original_url") or entry.get("url")
if not opts.quiet: if not opts.quiet:
debug(f"✓ Downloaded: {media_path.name} ({len(tags_res)} tags)") debug_panel(
"yt-dlp result",
[
("file", media_path.name),
("tag_count", len(tags_res)),
("source_url", source_url or opts.url),
],
border_style="green",
)
if debug_logger is not None: if debug_logger is not None:
debug_logger.write_record( debug_logger.write_record(
"downloaded", "downloaded",