From 97e310be70c0f8a66bfa444d29cbd5321108edb0 Mon Sep 17 00:00:00 2001 From: Nose Date: Mon, 13 Apr 2026 14:28:38 -0700 Subject: [PATCH] updated bandcamp and list parsing --- .../script-opts/medeia-selected-store.json | 1 + Provider/bandcamp.py | 22 ++- SYS/result_table.py | 9 +- cmdlet/_shared.py | 4 +- cmdlet/download_file.py | 160 +++++++++++++++++- 5 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 MPV/portable_config/script-opts/medeia-selected-store.json diff --git a/MPV/portable_config/script-opts/medeia-selected-store.json b/MPV/portable_config/script-opts/medeia-selected-store.json new file mode 100644 index 0000000..6bf5457 --- /dev/null +++ b/MPV/portable_config/script-opts/medeia-selected-store.json @@ -0,0 +1 @@ +{"store":"rpi"} \ No newline at end of file diff --git a/Provider/bandcamp.py b/Provider/bandcamp.py index 9d1be3c..0a6c2ae 100644 --- a/Provider/bandcamp.py +++ b/Provider/bandcamp.py @@ -16,6 +16,15 @@ class Bandcamp(Provider): TABLE_AUTO_STAGES = { "bandcamp": ["download-file"], } + AUTO_STAGE_USE_SELECTION_ARGS = True + + @staticmethod + def _download_selection_args(target_url: str, media_type: str) -> Optional[List[str]]: + target = str(target_url or "").strip() + kind = str(media_type or "").strip().lower() + if not target or kind == "artist": + return None + return ["-url", target] @staticmethod def _base_url(raw_url: str) -> str: @@ -89,6 +98,8 @@ class Bandcamp(Provider): "album" if "/album/" in target else ("track" if "/track/" in target else "item") ) + selection_args = self._download_selection_args(target, kind) + selection_action = (["download-file"] + selection_args) if selection_args else None results.append( SearchResult( @@ -111,6 +122,8 @@ class Bandcamp(Provider): "url": target, "artist_url": base, }, + selection_args=selection_args, + selection_action=selection_action, ) ) except Exception as exc: @@ -318,6 +331,8 @@ class Bandcamp(Provider): itemtype = item.query_selector(".itemtype") media_type = itemtype.inner_text().strip() if itemtype else "album" + selection_args = self._download_selection_args(str(target_url or ""), media_type) + selection_action = (["download-file"] + selection_args) if selection_args else None results.append( SearchResult( @@ -335,13 +350,16 @@ class Bandcamp(Provider): ("Type", media_type), ("Url", - base_url or str(target_url or "")), + str(target_url or "")), ], full_metadata={ "artist": artist, "type": media_type, - "url": base_url or str(target_url or ""), + "url": str(target_url or ""), + "artist_url": base_url, }, + selection_args=selection_args, + selection_action=selection_action, ) ) diff --git a/SYS/result_table.py b/SYS/result_table.py index 2f1b0e7..6d42579 100644 --- a/SYS/result_table.py +++ b/SYS/result_table.py @@ -630,6 +630,8 @@ class Table: """Base arguments for the source command""" self.header_lines: List[str] = [] """Optional metadata lines rendered under the title""" + self.preserve_order: bool = bool(preserve_order) + """If True, skip automatic sorting so display order matches input order.""" self.perseverance: bool = preserve_order """If True, skip automatic sorting so display order matches input order.""" self.interactive: bool = False @@ -687,7 +689,9 @@ class Table: def _perseverance(self, perseverance: bool = True) -> "Table": """Configure whether this table should skip automatic sorting.""" - self.perseverance = bool(perseverance) + keep_order = bool(perseverance) + self.perseverance = keep_order + self.preserve_order = keep_order return self def add_row(self) -> Row: @@ -741,7 +745,8 @@ class Table: self.title = title self.source_command = command self.source_args = args or [] - self.perseverance = preserve_order + self.perseverance = bool(preserve_order) + self.preserve_order = bool(preserve_order) return self def copy_with_title(self, new_title: str) -> "Table": diff --git a/cmdlet/_shared.py b/cmdlet/_shared.py index 83f6613..d6227c8 100644 --- a/cmdlet/_shared.py +++ b/cmdlet/_shared.py @@ -1920,11 +1920,13 @@ def create_pipe_object_result( Returns: Dict with all PipeObject fields for emission """ + store_override = extra.pop("store", None) + result = build_file_result_payload( title=title, path=file_path, hash_value=hash_value, - store=source, + store=store_override if store_override is not None else source, tag=tag, source=source, id=identifier, diff --git a/cmdlet/download_file.py b/cmdlet/download_file.py index f22bd5e..2ba266c 100644 --- a/cmdlet/download_file.py +++ b/cmdlet/download_file.py @@ -124,6 +124,48 @@ class Download_File(Cmdlet): raise DownloadError("Could not determine downloaded file path") return resolved + @staticmethod + def _selection_run_label(run_args: Sequence[str]) -> str: + try: + urls = extract_urls_from_selection_args( + run_args, + extra_url_prefixes=_ALLDEBRID_PREFIXES, + ) + if urls: + return str(urls[0]) + except Exception: + pass + + for arg in run_args: + text = str(arg or "").strip() + if text and not text.startswith("-"): + return text + return "item" + + @staticmethod + def _batch_progress_state(config: Optional[Dict[str, Any]]) -> tuple[bool, int, int, str]: + if not isinstance(config, dict): + return False, 0, 0, "" + + suppress_nested = bool(config.get("_download_file_suppress_nested_pipe_progress")) + if not suppress_nested: + return False, 0, 0, "" + + try: + total = max(0, int(config.get("_download_file_batch_total") or 0)) + except Exception: + total = 0 + try: + index = max(0, int(config.get("_download_file_batch_index") or 0)) + except Exception: + index = 0 + try: + label = str(config.get("_download_file_batch_label") or "").strip() + except Exception: + label = "" + + return True, total, index, label + def _process_explicit_urls( self, *, @@ -139,14 +181,32 @@ class Download_File(Cmdlet): ) -> tuple[int, Optional[int]]: downloaded_count = 0 + suppress_nested, batch_total, batch_index, batch_label = self._batch_progress_state(config) + total_urls = len(raw_urls or []) + + try: + if total_urls > 1 and not suppress_nested: + progress.begin_pipe(total_items=total_urls, items_preview=list(raw_urls[:5])) + except Exception: + pass SearchResult = registry.get("SearchResult") get_provider = registry.get("get_provider") match_provider_name_for_url = registry.get("match_provider_name_for_url") - for url in raw_urls: + for idx, url in enumerate(raw_urls, 1): try: debug(f"Processing URL: {url}") + try: + display_total = batch_total if batch_total > 0 else total_urls + display_index = batch_index if batch_total > 0 else idx + display_label = batch_label or str(url) + if display_total > 0: + progress.set_status( + f"downloading {display_index}/{display_total}: {display_label}" + ) + except Exception: + pass # Check providers first provider_name = None @@ -1455,6 +1515,9 @@ class Download_File(Cmdlet): downloaded_pipe_objects: List[Dict[str, Any]] = [] pipe_seq = 0 clip_sections_spec = self._build_clip_sections_spec(clip_ranges) + suppress_nested, batch_total, batch_index, batch_label = self._batch_progress_state(config) + total_urls = len(supported_url or []) + aggregate_status_mode = bool(suppress_nested or total_urls > 1) if clip_sections_spec: try: @@ -1462,11 +1525,15 @@ class Download_File(Cmdlet): except Exception: pass - for url in supported_url: + for url_index, url in enumerate(supported_url, 1): 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_index = batch_index if batch_total > 0 else url_index + display_label = batch_label or str(url) + canonical_url = url if not skip_per_url_preflight or clip_ranges: canonical_url = self._canonicalize_url_for_storage( @@ -1487,7 +1554,16 @@ class Download_File(Cmdlet): log(f"Skipping download (duplicate found): {url}", file=sys.stderr) continue - PipelineProgress(pipeline_context).begin_steps(2) + if aggregate_status_mode: + try: + if display_total > 0: + PipelineProgress(pipeline_context).set_status( + f"downloading {display_index}/{display_total}: {display_label}" + ) + except Exception: + pass + else: + PipelineProgress(pipeline_context).begin_steps(2) actual_format = ytdl_format actual_playlist_items = playlist_items @@ -1631,7 +1707,8 @@ class Download_File(Cmdlet): write_sub=write_sub, ) - PipelineProgress(pipeline_context).step("downloading") + if not aggregate_status_mode: + 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) debug(f"Download completed for {url}, building pipe object...") @@ -1953,7 +2030,8 @@ class Download_File(Cmdlet): debug(f"Emitting {len(pipe_objects)} result(s) to pipeline...") - PipelineProgress(pipeline_context).step("finalized") + if not aggregate_status_mode: + PipelineProgress(pipeline_context).step("finalized") stage_ctx = pipeline_context.get_stage_context() emit_enabled = bool(stage_ctx is not None) @@ -1995,6 +2073,7 @@ class Download_File(Cmdlet): ) -> int: try: debug("Starting streaming download handler") + suppress_nested, _batch_total, _batch_index, _batch_label = self._batch_progress_state(config) ytdlp_tool = YtDlpTool(config) @@ -2028,10 +2107,11 @@ class Download_File(Cmdlet): else: debug("[download-file] Skipping local UI: running inside pipeline stage") try: - progress.begin_pipe( - total_items=len(supported_url), - items_preview=supported_url, - ) + if not suppress_nested: + progress.begin_pipe( + total_items=len(supported_url), + items_preview=supported_url, + ) except Exception as err: debug(f"[download-file] PipelineProgress begin_pipe error: {err}") except Exception as e: @@ -2852,14 +2932,26 @@ class Download_File(Cmdlet): original_skip_preflight = None original_timeout = None original_skip_direct = None + original_batch_total = None + original_batch_index = None + original_batch_label = None + original_suppress_nested = None try: if isinstance(config, dict): original_skip_preflight = config.get("_skip_url_preflight") original_timeout = config.get("_pipeobject_timeout_seconds") original_skip_direct = config.get("_skip_direct_on_streaming_failure") + original_batch_total = config.get("_download_file_batch_total") + original_batch_index = config.get("_download_file_batch_index") + original_batch_label = config.get("_download_file_batch_label") + original_suppress_nested = config.get("_download_file_suppress_nested_pipe_progress") except Exception: original_skip_preflight = None original_timeout = None + original_batch_total = None + original_batch_index = None + original_batch_label = None + original_suppress_nested = None try: if selection_urls: @@ -2878,10 +2970,44 @@ class Download_File(Cmdlet): failures = 0 last_code = 0 total_selection = len(selection_runs) + preview_items = list(selection_urls[:5]) or [ + self._selection_run_label(run_args) + for run_args in selection_runs[:5] + ] + try: + progress.ensure_local_ui( + label="download-file", + total_items=total_selection, + items_preview=preview_items, + ) + except Exception: + pass + try: + progress.begin_pipe( + total_items=total_selection, + items_preview=preview_items, + ) + except Exception: + pass debug(f"[download-file] Processing {total_selection} selected item(s) from table...") for idx, run_args in enumerate(selection_runs, 1): + run_label = self._selection_run_label(run_args) debug(f"[download-file] Item {idx}/{total_selection}: {run_args}") debug("[download-file] Re-invoking download-file for selected item...") + try: + progress.set_status( + f"downloading {idx}/{total_selection}: {run_label}" + ) + except Exception: + pass + try: + if isinstance(config, dict): + config["_download_file_batch_total"] = total_selection + config["_download_file_batch_index"] = idx + config["_download_file_batch_label"] = run_label + config["_download_file_suppress_nested_pipe_progress"] = True + except Exception: + pass exit_code = self._run_impl(None, run_args, config) if exit_code == 0: successes += 1 @@ -2909,6 +3035,22 @@ class Download_File(Cmdlet): config.pop("_skip_direct_on_streaming_failure", None) else: config["_skip_direct_on_streaming_failure"] = original_skip_direct + if original_batch_total is None: + config.pop("_download_file_batch_total", None) + else: + config["_download_file_batch_total"] = original_batch_total + if original_batch_index is None: + config.pop("_download_file_batch_index", None) + else: + config["_download_file_batch_index"] = original_batch_index + if original_batch_label is None: + config.pop("_download_file_batch_label", None) + else: + config["_download_file_batch_label"] = original_batch_label + if original_suppress_nested is None: + config.pop("_download_file_suppress_nested_pipe_progress", None) + else: + config["_download_file_suppress_nested_pipe_progress"] = original_suppress_nested except Exception: pass