From 39ee857559af73087b3211a71f92898e429a043e Mon Sep 17 00:00:00 2001 From: Nose Date: Sun, 26 Apr 2026 15:08:35 -0700 Subject: [PATCH] pre-migration commit --- API/Tidal.py | 49 ++++++- API/data/alldebrid.json | 6 +- CLI.py | 137 ++++++++++++++---- ProviderCore/registry.py | 38 ++++- SYS/cmdlet_catalog.py | 2 +- SYS/cmdlet_spec.py | 2 +- SYS/config.py | 52 ++++--- SYS/logger.py | 5 +- SYS/result_table.py | 5 - Store/HydrusNetwork.py | 12 +- cmdlet/_shared.py | 2 +- cmdlet/search_file.py | 23 ++- .../alldebrid/__init__.py | 0 .../bandcamp/__init__.py | 20 ++- .../fileio.py => plugins/fileio/__init__.py | 0 .../hello/__init__.py | 0 Provider/HIFI.py => plugins/hifi/__init__.py | 46 +++++- .../internetarchive/__init__.py | 0 .../libgen.py => plugins/libgen/__init__.py | 0 Provider/loc.py => plugins/loc/__init__.py | 0 .../matrix.py => plugins/matrix/__init__.py | 0 .../openlibrary/__init__.py | 0 .../podcastindex/__init__.py | 0 .../soulseek/__init__.py | 0 .../telegram/__init__.py | 0 .../Tidal.py => plugins/tidal/__init__.py | 0 .../torrent.py => plugins/torrent/__init__.py | 0 Provider/vimm.py => plugins/vimm/__init__.py | 24 ++- .../youtube.py => plugins/youtube/__init__.py | 0 .../ytdlp.py => plugins/ytdlp/__init__.py | 0 .../zeroxzero/__init__.py | 0 scripts/bootstrap.py | 18 +-- 32 files changed, 335 insertions(+), 106 deletions(-) rename Provider/alldebrid.py => plugins/alldebrid/__init__.py (100%) rename Provider/bandcamp.py => plugins/bandcamp/__init__.py (96%) rename Provider/fileio.py => plugins/fileio/__init__.py (100%) rename Provider/hello_provider.py => plugins/hello/__init__.py (100%) rename Provider/HIFI.py => plugins/hifi/__init__.py (98%) rename Provider/internetarchive.py => plugins/internetarchive/__init__.py (100%) rename Provider/libgen.py => plugins/libgen/__init__.py (100%) rename Provider/loc.py => plugins/loc/__init__.py (100%) rename Provider/matrix.py => plugins/matrix/__init__.py (100%) rename Provider/openlibrary.py => plugins/openlibrary/__init__.py (100%) rename Provider/podcastindex.py => plugins/podcastindex/__init__.py (100%) rename Provider/soulseek.py => plugins/soulseek/__init__.py (100%) rename Provider/telegram.py => plugins/telegram/__init__.py (100%) rename Provider/Tidal.py => plugins/tidal/__init__.py (100%) rename Provider/torrent.py => plugins/torrent/__init__.py (100%) rename Provider/vimm.py => plugins/vimm/__init__.py (98%) rename Provider/youtube.py => plugins/youtube/__init__.py (100%) rename Provider/ytdlp.py => plugins/ytdlp/__init__.py (100%) rename Provider/zeroxzero.py => plugins/zeroxzero/__init__.py (100%) diff --git a/API/Tidal.py b/API/Tidal.py index bf03473..10f5aec 100644 --- a/API/Tidal.py +++ b/API/Tidal.py @@ -3,11 +3,37 @@ from __future__ import annotations from typing import Any, Dict, List, Optional, Set from .base import API, ApiError -from SYS.logger import debug +from SYS.logger import debug, debug_panel DEFAULT_BASE_URL = "https://tidal-api.binimum.org" +def _debug_payload_summary(title: str, payload: Any) -> None: + try: + keys = list(payload.keys()) if isinstance(payload, dict) else [] + except Exception: + keys = [] + + preview = ", ".join(str(key) for key in keys[:8]) if keys else "" + if keys and len(keys) > 8: + preview = f"{preview}, ..." + + try: + payload_size = len(str(payload)) + except Exception: + payload_size = "" + + debug_panel( + title, + [ + ("type", type(payload).__name__), + ("keys", preview), + ("size", payload_size), + ], + border_style="cyan", + ) + + def stringify(value: Any) -> str: """Helper to ensure we have a stripped string or empty.""" return str(value or "").strip() @@ -242,14 +268,14 @@ class Tidal(API): # 1. Fetch info (metadata) - fetch raw to ensure all fields are available for merging info_resp = self._get_json("info/", params={"id": track_int}) - debug(f"[API.Tidal] info_resp (len={len(str(info_resp))}): {info_resp}") + _debug_payload_summary("API.Tidal info", info_resp) info_data = info_resp.get("data") if isinstance(info_resp, dict) else info_resp if not isinstance(info_data, dict) or "id" not in info_data: info_data = info_resp if isinstance(info_resp, dict) and "id" in info_resp else {} # 2. Fetch track (manifest/bit depth) track_resp = self.track(track_id) - debug(f"[API.Tidal] track_resp (len={len(str(track_resp))}): {track_resp}") + _debug_payload_summary("API.Tidal track", track_resp) # Note: track() method in this class currently returns raw JSON, so we handle it similarly. track_data = track_resp.get("data") if isinstance(track_resp, dict) else track_resp if not isinstance(track_data, dict): @@ -259,7 +285,7 @@ class Tidal(API): lyrics_data = {} try: lyr_resp = self.lyrics(track_id) - debug(f"[API.Tidal] lyrics_resp (len={len(str(lyr_resp))}): {lyr_resp}") + _debug_payload_summary("API.Tidal lyrics", lyr_resp) lyrics_data = lyr_resp.get("lyrics") or lyr_resp if isinstance(lyr_resp, dict) else {} except Exception: pass @@ -271,11 +297,8 @@ class Tidal(API): if isinstance(track_data, dict): merged_md.update(track_data) - debug(f"[API.Tidal] merged_md keys: {list(merged_md.keys())}") - # Derived tags and normalized/parsed info tags = build_track_tags(merged_md) - debug(f"[API.Tidal] generated tags: {tags}") parsed_info = parse_track_item(merged_md) # Structure for return @@ -285,7 +308,17 @@ class Tidal(API): "tags": list(tags), "lyrics": lyrics_data, } - debug(f"[API.Tidal] returning full_track_metadata keys: {list(res.keys())}") + debug_panel( + "API.Tidal full track metadata", + [ + ("track_id", track_int), + ("metadata_keys", len(merged_md)), + ("tags", len(tags)), + ("has_lyrics", bool(lyrics_data)), + ("result_keys", ", ".join(res.keys())), + ], + border_style="cyan", + ) return res diff --git a/API/data/alldebrid.json b/API/data/alldebrid.json index 8cac978..ca709f8 100644 --- a/API/data/alldebrid.json +++ b/API/data/alldebrid.json @@ -37,7 +37,7 @@ "(rapidgator\\.net/file/[0-9]{7,8})" ], "regexp": "((rapidgator\\.net|rg\\.to|rapidgator\\.asia)/file/([0-9a-zA-Z]{32}))|((rapidgator\\.net/file/[0-9]{7,8}))", - "status": false + "status": true }, "turbobit": { "name": "turbobit", @@ -463,7 +463,7 @@ "isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12})" ], "regexp": "((isra\\.cloud/[0-9a-zA-Z]{12}))|(isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12}))", - "status": false, + "status": true, "hardRedirect": [ "isra\\.cloud/([0-9a-zA-Z]{12})" ] @@ -494,7 +494,7 @@ "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})" ], "regexp": "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})", - "status": true + "status": false }, "mixdrop": { "name": "mixdrop", diff --git a/CLI.py b/CLI.py index 8d098e1..19f1361 100644 --- a/CLI.py +++ b/CLI.py @@ -506,28 +506,21 @@ class CmdletIntrospection: if normalized_arg == "plugin": canonical_cmd = (cmd_name or "").replace("_", "-").lower() try: - from ProviderCore.registry import list_search_plugins, list_upload_plugins + from ProviderCore.registry import ( + list_search_plugin_names, + list_upload_plugin_names, + ) except Exception: - list_search_plugins = None # type: ignore - list_upload_plugins = None # type: ignore + list_search_plugin_names = None # type: ignore + list_upload_plugin_names = None # type: ignore provider_choices: List[str] = [] - if canonical_cmd in {"add-file"} and list_upload_plugins is not None: - providers = list_upload_plugins(config) or {} - available = [ - name for name, is_ready in providers.items() if is_ready - ] - return sorted(available) if available else sorted(providers.keys()) + if canonical_cmd in {"add-file"} and list_upload_plugin_names is not None: + return list_upload_plugin_names() or [] - if list_search_plugins is not None: - providers = list_search_plugins(config) or {} - available = [ - name for name, is_ready in providers.items() if is_ready - ] - provider_choices = sorted(available) if available else sorted( - providers.keys() - ) + if list_search_plugin_names is not None: + provider_choices = list_search_plugin_names() or [] if provider_choices: return provider_choices @@ -579,11 +572,90 @@ class CmdletIntrospection: class CmdletCompleter(Completer): """Prompt-toolkit completer for the Medeia cmdlet REPL.""" + _CMDLET_NAME_REFRESH_SECONDS = 2.0 + def __init__(self, *, config_loader: "ConfigLoader") -> None: self._config_loader = config_loader self.cmdlet_names = CmdletIntrospection.cmdlet_names() + self._cmdlet_names_refreshed_at = time.monotonic() + self._cmdlet_args_cache: Dict[Tuple[str, int], List[str]] = {} + self._query_args_cache: Dict[Tuple[str, int], List[Dict[str, Any]]] = {} + self._arg_choices_cache: Dict[Tuple[str, str, int], List[str]] = {} + self._inline_query_choices_cache: Dict[Tuple[str, str, int], List[str]] = {} + + def _refresh_cmdlet_names(self) -> None: + now = time.monotonic() + if self.cmdlet_names and (now - self._cmdlet_names_refreshed_at) < self._CMDLET_NAME_REFRESH_SECONDS: + return + self.cmdlet_names = CmdletIntrospection.cmdlet_names(force=False) + self._cmdlet_names_refreshed_at = now @staticmethod + def _config_cache_key(config: Dict[str, Any]) -> int: + return id(config) if isinstance(config, dict) else 0 + + def _cmdlet_args(self, cmd_name: str, config: Dict[str, Any]) -> List[str]: + key = (str(cmd_name or "").lower(), self._config_cache_key(config)) + cached = self._cmdlet_args_cache.get(key) + if cached is not None: + return cached + value = CmdletIntrospection.cmdlet_args(cmd_name, config) + self._cmdlet_args_cache[key] = value + return value + + def _query_args(self, cmd_name: str, config: Dict[str, Any]) -> List[Dict[str, Any]]: + key = (str(cmd_name or "").lower(), self._config_cache_key(config)) + cached = self._query_args_cache.get(key) + if cached is not None: + return cached + value = CmdletIntrospection.query_args(cmd_name, config) + self._query_args_cache[key] = value + return value + + def _arg_choices( + self, + *, + cmd_name: str, + arg_name: str, + config: Dict[str, Any], + force: bool = False, + ) -> List[str]: + key = ( + str(cmd_name or "").lower(), + str(arg_name or "").lower(), + self._config_cache_key(config), + ) + if not force: + cached = self._arg_choices_cache.get(key) + if cached is not None: + return cached + value = CmdletIntrospection.arg_choices( + cmd_name=cmd_name, + arg_name=arg_name, + config=config, + force=force, + ) + self._arg_choices_cache[key] = value + return value + + def _inline_query_choices( + self, + provider_name: str, + field_name: str, + config: Dict[str, Any], + ) -> List[str]: + key = ( + str(provider_name or "").lower(), + str(field_name or "").lower(), + self._config_cache_key(config), + ) + cached = self._inline_query_choices_cache.get(key) + if cached is not None: + return cached + value = plugin_inline_query_choices(provider_name, field_name, config) + self._inline_query_choices_cache[key] = value + return value + def _used_arg_logicals( cmd_name: str, stage_tokens: List[str], @@ -595,7 +667,7 @@ class CmdletCompleter(Completer): Example: if the user has typed `download-file -url ...`, then `url` is considered used and should not be suggested again (even as `--url`). """ - arg_flags = CmdletIntrospection.cmdlet_args(cmd_name, config) + arg_flags = self._cmdlet_args(cmd_name, config) allowed = {a.lstrip("-").strip().lower() for a in arg_flags if a} if not allowed: @@ -636,8 +708,7 @@ class CmdletCompleter(Completer): document: Document, complete_event ): # type: ignore[override] - # Refresh cmdlet names from introspection to pick up dynamic updates - self.cmdlet_names = CmdletIntrospection.cmdlet_names(force=True) + self._refresh_cmdlet_names() text = document.text_before_cursor tokens = text.split() @@ -660,7 +731,7 @@ class CmdletCompleter(Completer): if ends_with_space: cmd_name = current.replace("_", "-") - config = self._config_loader.load() + config = self._config_loader.load_shared() if cmd_name == "help": for cmd in self.cmdlet_names: @@ -670,7 +741,7 @@ class CmdletCompleter(Completer): if cmd_name not in self.cmdlet_names: return - arg_names = CmdletIntrospection.cmdlet_args(cmd_name, config) + arg_names = self._cmdlet_args(cmd_name, config) seen_logicals: Set[str] = set() for arg in arg_names: arg_low = arg.lower() @@ -701,13 +772,13 @@ class CmdletCompleter(Completer): current_token = stage_tokens[-1].lower() prev_token = stage_tokens[-2].lower() if len(stage_tokens) > 1 else "" - config = self._config_loader.load() + config = self._config_loader.load_shared() provider_name = None if cmd_name == "search-file": provider_name = self._flag_value(stage_tokens, "-plugin", "--plugin") - query_specs = CmdletIntrospection.query_args(cmd_name, config) + query_specs = self._query_args(cmd_name, config) query_flag_index = -1 for idx, tok in enumerate(stage_tokens): if str(tok or "").strip().lower() in {"-query", "--query"}: @@ -754,7 +825,7 @@ class CmdletCompleter(Completer): inline_choices = [] if cmd_name == "search-file" and provider_name: - inline_choices = plugin_inline_query_choices(provider_name, field, config) + inline_choices = self._inline_query_choices(provider_name, field, config) choice_pool = inline_choices or field_choices.get(field, []) if choice_pool: @@ -800,7 +871,7 @@ class CmdletCompleter(Completer): field, partial = inline_token.split(":", 1) field = field.strip().lower() partial_lower = partial.strip().lower() - inline_choices = plugin_inline_query_choices(provider_name, field, config) + inline_choices = self._inline_query_choices(provider_name, field, config) if inline_choices: filtered = ( [c for c in inline_choices if partial_lower in str(c).lower()] @@ -814,11 +885,11 @@ class CmdletCompleter(Completer): yield Completion(suggestion, start_position=start_pos) return - choices = CmdletIntrospection.arg_choices( + choices = self._arg_choices( cmd_name=cmd_name, arg_name=prev_token, config=config, - force=True + force=False, ) if choices: choice_list = choices @@ -835,7 +906,7 @@ class CmdletCompleter(Completer): # is considered used and should not be suggested again (even as `--url`). return - arg_names = CmdletIntrospection.cmdlet_args(cmd_name, config) + arg_names = self._cmdlet_args(cmd_name, config) used_logicals = self._used_arg_logicals(cmd_name, stage_tokens, config) logical_seen: Set[str] = set() for arg in arg_names: @@ -869,9 +940,15 @@ class ConfigLoader: def __init__(self, *, root: Path) -> None: self._root = root + def load_shared(self) -> Dict[str, Any]: + try: + return load_config(emit_summary=False) + except Exception: + return {} + def load(self) -> Dict[str, Any]: try: - return deepcopy(load_config()) + return deepcopy(self.load_shared()) except Exception: return {} diff --git a/ProviderCore/registry.py b/ProviderCore/registry.py index 41543fc..6b162ca 100644 --- a/ProviderCore/registry.py +++ b/ProviderCore/registry.py @@ -567,6 +567,15 @@ def list_search_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bo return availability +def list_search_plugin_names() -> List[str]: + """Return registered search-provider names without instantiating plugins.""" + return sorted( + info.canonical_name + for info in REGISTRY.iter_providers() + if info.supports_search + ) + + def get_upload_plugin(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[FileProvider]: plugin = get_plugin(name, config) @@ -591,6 +600,15 @@ def list_upload_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bo return availability +def list_upload_plugin_names() -> List[str]: + """Return registered upload-provider names without instantiating plugins.""" + return sorted( + info.canonical_name + for info in REGISTRY.iter_providers() + if info.supports_upload + ) + + def match_plugin_name_for_url(url: str) -> Optional[str]: raw_url = str(url or "").strip() raw_url_lower = raw_url.lower() @@ -666,14 +684,20 @@ def plugin_inline_query_choices( if not pname or not field: return [] - plugin = get_search_plugin(pname, config) - if plugin is None: - plugin = get_plugin(pname, config) - if plugin is None: - return [] - try: - mapping = _collect_inline_choice_mapping(plugin) + mapping: Dict[str, List[Dict[str, Any]]] = {} + info = REGISTRY.get(pname) + if info is not None: + mapping = _collect_inline_choice_mapping(info.provider_class) + + if not mapping: + plugin = get_search_plugin(pname, config) + if plugin is None: + plugin = get_plugin(pname, config) + if plugin is None: + return [] + mapping = _collect_inline_choice_mapping(plugin) + if not mapping: return [] diff --git a/SYS/cmdlet_catalog.py b/SYS/cmdlet_catalog.py index 70778d4..79ce7eb 100644 --- a/SYS/cmdlet_catalog.py +++ b/SYS/cmdlet_catalog.py @@ -338,7 +338,7 @@ def get_cmdlet_arg_choices( if config is None: from SYS.config import load_config - config = load_config() + config = load_config(emit_summary=False) except Exception as exc: logger.exception("Failed to load config for matrix default choices: %s", exc) config = config or {} diff --git a/SYS/cmdlet_spec.py b/SYS/cmdlet_spec.py index 5242d37..728eb3f 100644 --- a/SYS/cmdlet_spec.py +++ b/SYS/cmdlet_spec.py @@ -111,7 +111,7 @@ class SharedArgs: try: from SYS.config import load_config - config = load_config() + config = load_config(emit_summary=False) except Exception: SharedArgs._cached_available_stores = [] return diff --git a/SYS/config.py b/SYS/config.py index f244246..4756e7d 100644 --- a/SYS/config.py +++ b/SYS/config.py @@ -28,6 +28,7 @@ _SAVE_LOCK_STALE_SECONDS = 3600 # consider lock stale after 1 hour _CONFIG_CACHE: Dict[str, Any] = {} _LAST_SAVED_CONFIG: Dict[str, Any] = {} +_CONFIG_SUMMARY_PENDING = False _CONFIG_SAVE_MAX_RETRIES = 5 _CONFIG_SAVE_RETRY_DELAY = 0.15 _CONFIG_MISSING = object() @@ -84,9 +85,28 @@ def global_config() -> List[Dict[str, Any]]: def clear_config_cache() -> None: """Clear the configuration cache and baseline snapshot.""" - global _CONFIG_CACHE, _LAST_SAVED_CONFIG + global _CONFIG_CACHE, _LAST_SAVED_CONFIG, _CONFIG_SUMMARY_PENDING _CONFIG_CACHE = {} _LAST_SAVED_CONFIG = {} + _CONFIG_SUMMARY_PENDING = False + + +def _log_config_load_summary(config: Dict[str, Any]) -> None: + try: + provs = list(config.get("provider", {}).keys()) if isinstance(config.get("provider"), dict) else [] + stores = list(config.get("store", {}).keys()) if isinstance(config.get("store"), dict) else [] + mtime = None + try: + mtime = datetime.datetime.fromtimestamp(db.db_path.stat().st_mtime, datetime.timezone.utc).isoformat().replace('+00:00', 'Z') + except Exception: + mtime = None + summary = ( + f"Loaded config from {db.db_path.name}: providers={len(provs)} ({', '.join(provs[:10])}{'...' if len(provs)>10 else ''}), " + f"stores={len(stores)} ({', '.join(stores[:10])}{'...' if len(stores)>10 else ''}), mtime={mtime}" + ) + log(summary) + except Exception: + logger.exception("Failed to build config load summary from %s", db.db_path) def get_nested_config_value(config: Dict[str, Any], *path: str) -> Any: @@ -624,9 +644,12 @@ def _count_changed_entries(old_config: Dict[str, Any], new_config: Dict[str, Any return len(changed) + len(removed) -def load_config() -> Dict[str, Any]: - global _CONFIG_CACHE, _LAST_SAVED_CONFIG +def load_config(*, emit_summary: bool = True) -> Dict[str, Any]: + global _CONFIG_CACHE, _LAST_SAVED_CONFIG, _CONFIG_SUMMARY_PENDING if _CONFIG_CACHE: + if emit_summary and _CONFIG_SUMMARY_PENDING: + _log_config_load_summary(_CONFIG_CACHE) + _CONFIG_SUMMARY_PENDING = False return _CONFIG_CACHE # Load strictly from database @@ -635,24 +658,13 @@ def load_config() -> Dict[str, Any]: _sync_alldebrid_api_key(db_config) _CONFIG_CACHE = db_config _LAST_SAVED_CONFIG = deepcopy(db_config) - try: - # Log a compact summary to help detect startup overwrites/mismatches - provs = list(db_config.get("provider", {}).keys()) if isinstance(db_config.get("provider"), dict) else [] - stores = list(db_config.get("store", {}).keys()) if isinstance(db_config.get("store"), dict) else [] - mtime = None - try: - mtime = datetime.datetime.fromtimestamp(db.db_path.stat().st_mtime, datetime.timezone.utc).isoformat().replace('+00:00', 'Z') - except Exception: - mtime = None - summary = ( - f"Loaded config from {db.db_path.name}: providers={len(provs)} ({', '.join(provs[:10])}{'...' if len(provs)>10 else ''}), " - f"stores={len(stores)} ({', '.join(stores[:10])}{'...' if len(stores)>10 else ''}), mtime={mtime}" - ) - log(summary) + if emit_summary: + _log_config_load_summary(db_config) + _CONFIG_SUMMARY_PENDING = False + else: + _CONFIG_SUMMARY_PENDING = True - # Forensics disabled: audit/mismatch/backup detection removed to simplify code. - except Exception: - logger.exception("Failed to build config load summary from %s", db.db_path) + # Forensics disabled: audit/mismatch/backup detection removed to simplify code. return db_config _LAST_SAVED_CONFIG = {} diff --git a/SYS/logger.py b/SYS/logger.py index 623d96c..2653476 100644 --- a/SYS/logger.py +++ b/SYS/logger.py @@ -146,7 +146,7 @@ def debug_panel( def debug(*args, **kwargs) -> None: """Print debug message if debug logging is enabled. - Automatically prepends [filename.function_name] to all output. + Automatically routes through log() so debug output keeps the caller prefix. """ if not _DEBUG_ENABLED: return @@ -166,9 +166,6 @@ def debug(*args, **kwargs) -> None: _debug_db_log(caller_name=caller_name, message=f"") return - # Prepend DEBUG label - args = ("DEBUG:", *args) - # Use the same logic as log() log(*args, file=target_file, **kwargs) diff --git a/SYS/result_table.py b/SYS/result_table.py index 977ad9f..29631a8 100644 --- a/SYS/result_table.py +++ b/SYS/result_table.py @@ -1354,9 +1354,6 @@ class Table: "") ).lower() - # Debug logging - # print(f"DEBUG: Processing dict result. Store: {store_val}, Keys: {list(visible_data.keys())}") - if store_val == "local": # Find title field title_field = next( @@ -1373,8 +1370,6 @@ class Table: # Only use title suffix as fallback when ext is missing. if not str(visible_data.get("ext") or "").strip(): visible_data["ext"] = extension - # print(f"DEBUG: Split extension. Title: {visible_data[title_field]}, Ext: {extension}") - # Ensure 'ext' is present so it gets picked up by priority_groups in correct order if "ext" not in visible_data: visible_data["ext"] = "" diff --git a/Store/HydrusNetwork.py b/Store/HydrusNetwork.py index 2646fcd..52f7526 100644 --- a/Store/HydrusNetwork.py +++ b/Store/HydrusNetwork.py @@ -1045,8 +1045,16 @@ class HydrusNetwork(Store): if total_candidates <= hydrate_limit: return ids_out, hashes_out - debug( - f"{prefix} limiting metadata hydration to {hydrate_limit} of {total_candidates} candidate(s)" + debug_panel( + "Hydrus metadata hydration cap", + [ + ("store", self.NAME), + ("candidates", total_candidates), + ("hydrate_limit", hydrate_limit), + ("freeform_mode", freeform_mode), + ("fallback_scan", fallback_scan), + ], + border_style="cyan", ) if ids_out: diff --git a/cmdlet/_shared.py b/cmdlet/_shared.py index 244e043..6c979ae 100644 --- a/cmdlet/_shared.py +++ b/cmdlet/_shared.py @@ -249,7 +249,7 @@ class SharedArgs: if config is None: try: from SYS.config import load_config - config = load_config() + config = load_config(emit_summary=False) except Exception: SharedArgs._cached_available_stores = [] return diff --git a/cmdlet/search_file.py b/cmdlet/search_file.py index 5165cd1..ec52e53 100644 --- a/cmdlet/search_file.py +++ b/cmdlet/search_file.py @@ -13,7 +13,7 @@ import html import time from urllib.parse import urlparse, parse_qs, unquote, urljoin -from SYS.logger import log, debug +from SYS.logger import log, debug, debug_panel from SYS.payload_builders import build_file_result_payload, normalize_file_extension from ProviderCore.registry import get_search_plugin, list_search_plugins from SYS.rich_display import ( @@ -1560,9 +1560,26 @@ class search_file(Cmdlet): source_cmd, source_args = provider.get_source_command(args_list) table.set_source_command(source_cmd, source_args) - debug(f"[search-file] Calling {plugin_name}.search(filters={search_filters})") + debug_panel( + "search-file provider request", + [ + ("provider", plugin_name), + ("query", query), + ("limit", limit), + ("filters", search_filters or ""), + ], + border_style="cyan", + ) results = provider.search(query, limit=limit, filters=search_filters or None) - debug(f"[search-file] {plugin_name} -> {len(results or [])} result(s)") + debug_panel( + "search-file provider response", + [ + ("provider", plugin_name), + ("results", len(results or [])), + ("table", table_type), + ], + border_style="cyan", + ) # Allow providers to apply provider-specific UX transforms (e.g. auto-expansion) try: diff --git a/Provider/alldebrid.py b/plugins/alldebrid/__init__.py similarity index 100% rename from Provider/alldebrid.py rename to plugins/alldebrid/__init__.py diff --git a/Provider/bandcamp.py b/plugins/bandcamp/__init__.py similarity index 96% rename from Provider/bandcamp.py rename to plugins/bandcamp/__init__.py index 0a6c2ae..be67208 100644 --- a/Provider/bandcamp.py +++ b/plugins/bandcamp/__init__.py @@ -5,7 +5,7 @@ from urllib.parse import urlparse from typing import Any, Dict, List, Optional from ProviderCore.base import Provider, SearchResult -from SYS.logger import log, debug +from SYS.logger import log, debug, debug_panel from tool.playwright import PlaywrightTool @@ -58,7 +58,14 @@ class Bandcamp(Provider): if not base or not discography_url: return [] - debug(f"[bandcamp] Scraping artist page: {discography_url}") + debug_panel( + "bandcamp artist scrape", + [ + ("url", discography_url), + ("limit", limit), + ], + border_style="cyan", + ) page.goto(discography_url) page.wait_for_load_state("domcontentloaded") @@ -301,7 +308,14 @@ class Bandcamp(Provider): return [] def _scrape_url(self, page: Any, url: str, limit: int) -> List[SearchResult]: - debug(f"[bandcamp] Scraping: {url}") + debug_panel( + "bandcamp search", + [ + ("url", url), + ("limit", limit), + ], + border_style="cyan", + ) page.goto(url) page.wait_for_load_state("domcontentloaded") diff --git a/Provider/fileio.py b/plugins/fileio/__init__.py similarity index 100% rename from Provider/fileio.py rename to plugins/fileio/__init__.py diff --git a/Provider/hello_provider.py b/plugins/hello/__init__.py similarity index 100% rename from Provider/hello_provider.py rename to plugins/hello/__init__.py diff --git a/Provider/HIFI.py b/plugins/hifi/__init__.py similarity index 98% rename from Provider/HIFI.py rename to plugins/hifi/__init__.py index 103f899..c996aeb 100644 --- a/Provider/HIFI.py +++ b/plugins/hifi/__init__.py @@ -20,7 +20,7 @@ from ProviderCore.base import Provider, SearchResult, parse_inline_query_argumen from SYS.field_access import get_field from Provider.tidal_manifest import resolve_tidal_manifest_path from SYS import pipeline as pipeline_context -from SYS.logger import debug, log +from SYS.logger import debug, debug_panel, log URL_API = ( "https://triton.squid.wtf", @@ -1136,7 +1136,15 @@ class HIFI(Provider): md = dict(getattr(result, "full_metadata") or {}) track_id = self._extract_track_id_from_result(result) - debug(f"[hifi] download: track_id={track_id}, manifest_present={bool(md.get('manifest'))}, tag_count={len(result.tag) if result.tag else 0}") + debug_panel( + "hifi download", + [ + ("track_id", track_id or ""), + ("manifest_present", bool(md.get("manifest"))), + ("tag_count", len(result.tag) if result.tag else 0), + ], + border_style="cyan", + ) # Enrichment: fetch full metadata if manifest or detailed info (like tags/lyrics) is missing. # We check for 'manifest' because it's required for DASH playback. @@ -1144,20 +1152,46 @@ class HIFI(Provider): has_lyrics = bool(md.get("_tidal_lyrics_subtitles")) or bool(md.get("lyrics")) if track_id and (not md.get("manifest") or not md.get("artist") or len(result.tag or []) <= 1 or not has_lyrics): - debug(f"[hifi] Enriching track data (reason: manifest={not md.get('manifest')}, lyrics={not has_lyrics}, tags={len(result.tag or [])})") + debug_panel( + "hifi enrichment request", + [ + ("track_id", track_id), + ("needs_manifest", not md.get("manifest")), + ("needs_lyrics", not has_lyrics), + ("current_tags", len(result.tag or [])), + ], + border_style="cyan", + ) # Multi-part enrichment from API: metadata, tags, and lyrics. full_data = self._fetch_all_track_data(track_id) - debug(f"[hifi] download: enrichment full_data present={bool(full_data)}") + debug_panel( + "hifi enrichment response", + [ + ("track_id", track_id), + ("full_data", bool(full_data)), + ( + "metadata_keys", + len(full_data.get("metadata") or {}) if isinstance(full_data, dict) else 0, + ), + ( + "tag_count", + len(full_data.get("tags") or []) if isinstance(full_data, dict) else 0, + ), + ( + "has_lyrics", + bool(full_data.get("lyrics")) if isinstance(full_data, dict) else False, + ), + ], + border_style="cyan", + ) if isinstance(full_data, dict): # 1. Update metadata api_md = full_data.get("metadata") if isinstance(api_md, dict): - debug(f"[hifi] download: updating metadata with {len(api_md)} keys") md.update(api_md) # 2. Update tags (re-sync result.tag so cmdlet sees them) api_tags = full_data.get("tags") - debug(f"[hifi] download: enrichment tags={api_tags}") if isinstance(api_tags, list) and api_tags: result.tag = set(api_tags) diff --git a/Provider/internetarchive.py b/plugins/internetarchive/__init__.py similarity index 100% rename from Provider/internetarchive.py rename to plugins/internetarchive/__init__.py diff --git a/Provider/libgen.py b/plugins/libgen/__init__.py similarity index 100% rename from Provider/libgen.py rename to plugins/libgen/__init__.py diff --git a/Provider/loc.py b/plugins/loc/__init__.py similarity index 100% rename from Provider/loc.py rename to plugins/loc/__init__.py diff --git a/Provider/matrix.py b/plugins/matrix/__init__.py similarity index 100% rename from Provider/matrix.py rename to plugins/matrix/__init__.py diff --git a/Provider/openlibrary.py b/plugins/openlibrary/__init__.py similarity index 100% rename from Provider/openlibrary.py rename to plugins/openlibrary/__init__.py diff --git a/Provider/podcastindex.py b/plugins/podcastindex/__init__.py similarity index 100% rename from Provider/podcastindex.py rename to plugins/podcastindex/__init__.py diff --git a/Provider/soulseek.py b/plugins/soulseek/__init__.py similarity index 100% rename from Provider/soulseek.py rename to plugins/soulseek/__init__.py diff --git a/Provider/telegram.py b/plugins/telegram/__init__.py similarity index 100% rename from Provider/telegram.py rename to plugins/telegram/__init__.py diff --git a/Provider/Tidal.py b/plugins/tidal/__init__.py similarity index 100% rename from Provider/Tidal.py rename to plugins/tidal/__init__.py diff --git a/Provider/torrent.py b/plugins/torrent/__init__.py similarity index 100% rename from Provider/torrent.py rename to plugins/torrent/__init__.py diff --git a/Provider/vimm.py b/plugins/vimm/__init__.py similarity index 98% rename from Provider/vimm.py rename to plugins/vimm/__init__.py index 5f188c0..04c3f25 100644 --- a/Provider/vimm.py +++ b/plugins/vimm/__init__.py @@ -18,7 +18,7 @@ from pathlib import Path from API.HTTP import HTTPClient from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments from ProviderCore.inline_utils import resolve_filter -from SYS.logger import debug +from SYS.logger import debug, debug_panel from SYS.provider_helpers import TableProviderMixin from tool.playwright import PlaywrightTool @@ -178,7 +178,17 @@ class Vimm(TableProviderMixin, Provider): if region_param: params.append(("region", region_param)) url = f"{base}?{urlencode(params)}" - debug(f"[vimm] search: query={q} url={url} filters={normalized_filters}") + debug_panel( + "vimm search", + [ + ("query", q), + ("url", url), + ("system", system_param or ""), + ("region", region_param or ""), + ("filters", normalized_filters or ""), + ], + border_style="cyan", + ) try: with HTTPClient(timeout=9.0) as client: @@ -210,7 +220,15 @@ class Vimm(TableProviderMixin, Provider): results = [self._apply_selection_defaults(r, referer=url, detail_url=getattr(r, "path", "")) for r in (results or [])] - debug(f"[vimm] results={len(results)}") + debug_panel( + "vimm search results", + [ + ("query", q), + ("results", len(results)), + ("url", url), + ], + border_style="cyan", + ) return results[: int(limit)] def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]: diff --git a/Provider/youtube.py b/plugins/youtube/__init__.py similarity index 100% rename from Provider/youtube.py rename to plugins/youtube/__init__.py diff --git a/Provider/ytdlp.py b/plugins/ytdlp/__init__.py similarity index 100% rename from Provider/ytdlp.py rename to plugins/ytdlp/__init__.py diff --git a/Provider/zeroxzero.py b/plugins/zeroxzero/__init__.py similarity index 100% rename from Provider/zeroxzero.py rename to plugins/zeroxzero/__init__.py diff --git a/scripts/bootstrap.py b/scripts/bootstrap.py index a7dabcd..db3cd6a 100644 --- a/scripts/bootstrap.py +++ b/scripts/bootstrap.py @@ -77,7 +77,7 @@ def _ensure_interactive_stdin() -> None: sys.stdin.flush() except Exception as e: if "--debug" in sys.argv: - print(f"DEBUG: Failed to re-open stdin: {e}") + print(f"[bootstrap] Failed to re-open stdin: {e}") def run(cmd: list[str], quiet: bool = False, debug: bool = False, cwd: Optional[Path] = None, env: Optional[dict[str, str]] = None, check: bool = True) -> subprocess.CompletedProcess: @@ -1799,10 +1799,10 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { raise RuntimeError("Failed to create mm.bat shim") if args.debug: - print(f"DEBUG: Created mm.bat ({len(bat_text)} bytes)") - print(f"DEBUG: Repo path embedded in shim: {repo}") - print(f"DEBUG: Venv location: {repo}/.venv") - print(f"DEBUG: Shim directory: {user_bin}") + print(f"[bootstrap] Created mm.bat ({len(bat_text)} bytes)") + print(f"[bootstrap] Repo path embedded in shim: {repo}") + print(f"[bootstrap] Venv location: {repo}/.venv") + print(f"[bootstrap] Shim directory: {user_bin}") # Add user_bin to PATH for current and future sessions str_bin = str(user_bin) @@ -1832,7 +1832,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { text=True ) if args.debug and result.stderr: - print(f"DEBUG: PowerShell output: {result.stderr}") + print(f"[bootstrap] PowerShell output: {result.stderr}") # Also reload PATH in current session for immediate availability reload_cmd = ( @@ -1849,7 +1849,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { ) except Exception as e: if args.debug: - print(f"DEBUG: Could not persist PATH to registry: {e}", file=sys.stderr) + print(f"[bootstrap] Could not persist PATH to registry: {e}", file=sys.stderr) if not args.quiet: print(f"Installed global launcher to: {user_bin}") @@ -1927,7 +1927,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { 'VENV="$REPO/.venv"\n' "# Debug mode: set MM_DEBUG=1 to print repository, venv, and import diagnostics\n" 'if [ -n "${MM_DEBUG:-}" ]; then\n' - ' echo "MM_DEBUG: diagnostics" >&2\n' + ' echo "[mm-debug] diagnostics" >&2\n' ' echo "Resolved REPO: $REPO" >&2\n' ' echo "Resolved VENV: $VENV" >&2\n' ' echo "VENV exists: $( [ -d "$VENV" ] && echo yes || echo no )" >&2\n' @@ -1943,7 +1943,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { " $pycmd - <<'PY'\nimport sys, importlib, traceback, importlib.util\nprint('sys.executable:', sys.executable)\nprint('sys.path (first 8):', sys.path[:8])\nfor mod in ('CLI','medeia_macina','scripts.cli_entry'):\n try:\n spec = importlib.util.find_spec(mod)\n print(mod, 'spec:', spec)\n if spec:\n m = importlib.import_module(mod)\n print(mod, 'loaded at', getattr(m, '__file__', None))\n except Exception:\n print(mod, 'import failed')\n traceback.print_exc()\nPY\n" " fi\n" " done\n" - ' echo "MM_DEBUG: end diagnostics" >&2\n' + ' echo "[mm-debug] end diagnostics" >&2\n' "fi\n" "\n" "# Automatically check for updates if this is a git repository\n"