pre-migration commit

This commit is contained in:
2026-04-26 15:08:35 -07:00
parent c724cb36b1
commit 39ee857559
32 changed files with 335 additions and 106 deletions
+41 -8
View File
@@ -3,11 +3,37 @@ from __future__ import annotations
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set
from .base import API, ApiError 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" 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 "<none>"
if keys and len(keys) > 8:
preview = f"{preview}, ..."
try:
payload_size = len(str(payload))
except Exception:
payload_size = "<unknown>"
debug_panel(
title,
[
("type", type(payload).__name__),
("keys", preview),
("size", payload_size),
],
border_style="cyan",
)
def stringify(value: Any) -> str: def stringify(value: Any) -> str:
"""Helper to ensure we have a stripped string or empty.""" """Helper to ensure we have a stripped string or empty."""
return str(value or "").strip() 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 # 1. Fetch info (metadata) - fetch raw to ensure all fields are available for merging
info_resp = self._get_json("info/", params={"id": track_int}) 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 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: 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 {} info_data = info_resp if isinstance(info_resp, dict) and "id" in info_resp else {}
# 2. Fetch track (manifest/bit depth) # 2. Fetch track (manifest/bit depth)
track_resp = self.track(track_id) 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. # 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 track_data = track_resp.get("data") if isinstance(track_resp, dict) else track_resp
if not isinstance(track_data, dict): if not isinstance(track_data, dict):
@@ -259,7 +285,7 @@ class Tidal(API):
lyrics_data = {} lyrics_data = {}
try: try:
lyr_resp = self.lyrics(track_id) 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 {} lyrics_data = lyr_resp.get("lyrics") or lyr_resp if isinstance(lyr_resp, dict) else {}
except Exception: except Exception:
pass pass
@@ -271,11 +297,8 @@ class Tidal(API):
if isinstance(track_data, dict): if isinstance(track_data, dict):
merged_md.update(track_data) merged_md.update(track_data)
debug(f"[API.Tidal] merged_md keys: {list(merged_md.keys())}")
# Derived tags and normalized/parsed info # Derived tags and normalized/parsed info
tags = build_track_tags(merged_md) tags = build_track_tags(merged_md)
debug(f"[API.Tidal] generated tags: {tags}")
parsed_info = parse_track_item(merged_md) parsed_info = parse_track_item(merged_md)
# Structure for return # Structure for return
@@ -285,7 +308,17 @@ class Tidal(API):
"tags": list(tags), "tags": list(tags),
"lyrics": lyrics_data, "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 return res
+3 -3
View File
@@ -37,7 +37,7 @@
"(rapidgator\\.net/file/[0-9]{7,8})" "(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}))", "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": { "turbobit": {
"name": "turbobit", "name": "turbobit",
@@ -463,7 +463,7 @@
"isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12})" "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}))", "regexp": "((isra\\.cloud/[0-9a-zA-Z]{12}))|(isra\\.cloud/\\?op=report_file&id=([0-9a-zA-Z]{12}))",
"status": false, "status": true,
"hardRedirect": [ "hardRedirect": [
"isra\\.cloud/([0-9a-zA-Z]{12})" "isra\\.cloud/([0-9a-zA-Z]{12})"
] ]
@@ -494,7 +494,7 @@
"mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})" "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})"
], ],
"regexp": "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": { "mixdrop": {
"name": "mixdrop", "name": "mixdrop",
+107 -30
View File
@@ -506,28 +506,21 @@ class CmdletIntrospection:
if normalized_arg == "plugin": if normalized_arg == "plugin":
canonical_cmd = (cmd_name or "").replace("_", "-").lower() canonical_cmd = (cmd_name or "").replace("_", "-").lower()
try: 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: except Exception:
list_search_plugins = None # type: ignore list_search_plugin_names = None # type: ignore
list_upload_plugins = None # type: ignore list_upload_plugin_names = None # type: ignore
provider_choices: List[str] = [] provider_choices: List[str] = []
if canonical_cmd in {"add-file"} and list_upload_plugins is not None: if canonical_cmd in {"add-file"} and list_upload_plugin_names is not None:
providers = list_upload_plugins(config) or {} return list_upload_plugin_names() or []
available = [
name for name, is_ready in providers.items() if is_ready
]
return sorted(available) if available else sorted(providers.keys())
if list_search_plugins is not None: if list_search_plugin_names is not None:
providers = list_search_plugins(config) or {} provider_choices = list_search_plugin_names() or []
available = [
name for name, is_ready in providers.items() if is_ready
]
provider_choices = sorted(available) if available else sorted(
providers.keys()
)
if provider_choices: if provider_choices:
return provider_choices return provider_choices
@@ -579,11 +572,90 @@ class CmdletIntrospection:
class CmdletCompleter(Completer): class CmdletCompleter(Completer):
"""Prompt-toolkit completer for the Medeia cmdlet REPL.""" """Prompt-toolkit completer for the Medeia cmdlet REPL."""
_CMDLET_NAME_REFRESH_SECONDS = 2.0
def __init__(self, *, config_loader: "ConfigLoader") -> None: def __init__(self, *, config_loader: "ConfigLoader") -> None:
self._config_loader = config_loader self._config_loader = config_loader
self.cmdlet_names = CmdletIntrospection.cmdlet_names() 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 @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( def _used_arg_logicals(
cmd_name: str, cmd_name: str,
stage_tokens: List[str], stage_tokens: List[str],
@@ -595,7 +667,7 @@ class CmdletCompleter(Completer):
Example: if the user has typed `download-file -url ...`, then `url` Example: if the user has typed `download-file -url ...`, then `url`
is considered used and should not be suggested again (even as `--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() allowed = {a.lstrip("-").strip().lower()
for a in arg_flags if a} for a in arg_flags if a}
if not allowed: if not allowed:
@@ -636,8 +708,7 @@ class CmdletCompleter(Completer):
document: Document, document: Document,
complete_event complete_event
): # type: ignore[override] ): # type: ignore[override]
# Refresh cmdlet names from introspection to pick up dynamic updates self._refresh_cmdlet_names()
self.cmdlet_names = CmdletIntrospection.cmdlet_names(force=True)
text = document.text_before_cursor text = document.text_before_cursor
tokens = text.split() tokens = text.split()
@@ -660,7 +731,7 @@ class CmdletCompleter(Completer):
if ends_with_space: if ends_with_space:
cmd_name = current.replace("_", "-") cmd_name = current.replace("_", "-")
config = self._config_loader.load() config = self._config_loader.load_shared()
if cmd_name == "help": if cmd_name == "help":
for cmd in self.cmdlet_names: for cmd in self.cmdlet_names:
@@ -670,7 +741,7 @@ class CmdletCompleter(Completer):
if cmd_name not in self.cmdlet_names: if cmd_name not in self.cmdlet_names:
return return
arg_names = CmdletIntrospection.cmdlet_args(cmd_name, config) arg_names = self._cmdlet_args(cmd_name, config)
seen_logicals: Set[str] = set() seen_logicals: Set[str] = set()
for arg in arg_names: for arg in arg_names:
arg_low = arg.lower() arg_low = arg.lower()
@@ -701,13 +772,13 @@ class CmdletCompleter(Completer):
current_token = stage_tokens[-1].lower() current_token = stage_tokens[-1].lower()
prev_token = stage_tokens[-2].lower() if len(stage_tokens) > 1 else "" 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 provider_name = None
if cmd_name == "search-file": if cmd_name == "search-file":
provider_name = self._flag_value(stage_tokens, "-plugin", "--plugin") 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 query_flag_index = -1
for idx, tok in enumerate(stage_tokens): for idx, tok in enumerate(stage_tokens):
if str(tok or "").strip().lower() in {"-query", "--query"}: if str(tok or "").strip().lower() in {"-query", "--query"}:
@@ -754,7 +825,7 @@ class CmdletCompleter(Completer):
inline_choices = [] inline_choices = []
if cmd_name == "search-file" and provider_name: 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, []) choice_pool = inline_choices or field_choices.get(field, [])
if choice_pool: if choice_pool:
@@ -800,7 +871,7 @@ class CmdletCompleter(Completer):
field, partial = inline_token.split(":", 1) field, partial = inline_token.split(":", 1)
field = field.strip().lower() field = field.strip().lower()
partial_lower = partial.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: if inline_choices:
filtered = ( filtered = (
[c for c in inline_choices if partial_lower in str(c).lower()] [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) yield Completion(suggestion, start_position=start_pos)
return return
choices = CmdletIntrospection.arg_choices( choices = self._arg_choices(
cmd_name=cmd_name, cmd_name=cmd_name,
arg_name=prev_token, arg_name=prev_token,
config=config, config=config,
force=True force=False,
) )
if choices: if choices:
choice_list = choices choice_list = choices
@@ -835,7 +906,7 @@ class CmdletCompleter(Completer):
# is considered used and should not be suggested again (even as `--url`). # is considered used and should not be suggested again (even as `--url`).
return 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) used_logicals = self._used_arg_logicals(cmd_name, stage_tokens, config)
logical_seen: Set[str] = set() logical_seen: Set[str] = set()
for arg in arg_names: for arg in arg_names:
@@ -869,9 +940,15 @@ class ConfigLoader:
def __init__(self, *, root: Path) -> None: def __init__(self, *, root: Path) -> None:
self._root = root 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]: def load(self) -> Dict[str, Any]:
try: try:
return deepcopy(load_config()) return deepcopy(self.load_shared())
except Exception: except Exception:
return {} return {}
+31 -7
View File
@@ -567,6 +567,15 @@ def list_search_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bo
return availability 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, def get_upload_plugin(name: str,
config: Optional[Dict[str, Any]] = None) -> Optional[FileProvider]: config: Optional[Dict[str, Any]] = None) -> Optional[FileProvider]:
plugin = get_plugin(name, config) plugin = get_plugin(name, config)
@@ -591,6 +600,15 @@ def list_upload_plugins(config: Optional[Dict[str, Any]] = None) -> Dict[str, bo
return availability 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]: def match_plugin_name_for_url(url: str) -> Optional[str]:
raw_url = str(url or "").strip() raw_url = str(url or "").strip()
raw_url_lower = raw_url.lower() raw_url_lower = raw_url.lower()
@@ -666,14 +684,20 @@ def plugin_inline_query_choices(
if not pname or not field: if not pname or not field:
return [] return []
plugin = get_search_plugin(pname, config)
if plugin is None:
plugin = get_plugin(pname, config)
if plugin is None:
return []
try: 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: if not mapping:
return [] return []
+1 -1
View File
@@ -338,7 +338,7 @@ def get_cmdlet_arg_choices(
if config is None: if config is None:
from SYS.config import load_config from SYS.config import load_config
config = load_config() config = load_config(emit_summary=False)
except Exception as exc: except Exception as exc:
logger.exception("Failed to load config for matrix default choices: %s", exc) logger.exception("Failed to load config for matrix default choices: %s", exc)
config = config or {} config = config or {}
+1 -1
View File
@@ -111,7 +111,7 @@ class SharedArgs:
try: try:
from SYS.config import load_config from SYS.config import load_config
config = load_config() config = load_config(emit_summary=False)
except Exception: except Exception:
SharedArgs._cached_available_stores = [] SharedArgs._cached_available_stores = []
return return
+32 -20
View File
@@ -28,6 +28,7 @@ _SAVE_LOCK_STALE_SECONDS = 3600 # consider lock stale after 1 hour
_CONFIG_CACHE: Dict[str, Any] = {} _CONFIG_CACHE: Dict[str, Any] = {}
_LAST_SAVED_CONFIG: Dict[str, Any] = {} _LAST_SAVED_CONFIG: Dict[str, Any] = {}
_CONFIG_SUMMARY_PENDING = False
_CONFIG_SAVE_MAX_RETRIES = 5 _CONFIG_SAVE_MAX_RETRIES = 5
_CONFIG_SAVE_RETRY_DELAY = 0.15 _CONFIG_SAVE_RETRY_DELAY = 0.15
_CONFIG_MISSING = object() _CONFIG_MISSING = object()
@@ -84,9 +85,28 @@ def global_config() -> List[Dict[str, Any]]:
def clear_config_cache() -> None: def clear_config_cache() -> None:
"""Clear the configuration cache and baseline snapshot.""" """Clear the configuration cache and baseline snapshot."""
global _CONFIG_CACHE, _LAST_SAVED_CONFIG global _CONFIG_CACHE, _LAST_SAVED_CONFIG, _CONFIG_SUMMARY_PENDING
_CONFIG_CACHE = {} _CONFIG_CACHE = {}
_LAST_SAVED_CONFIG = {} _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: 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) return len(changed) + len(removed)
def load_config() -> Dict[str, Any]: def load_config(*, emit_summary: bool = True) -> Dict[str, Any]:
global _CONFIG_CACHE, _LAST_SAVED_CONFIG global _CONFIG_CACHE, _LAST_SAVED_CONFIG, _CONFIG_SUMMARY_PENDING
if _CONFIG_CACHE: if _CONFIG_CACHE:
if emit_summary and _CONFIG_SUMMARY_PENDING:
_log_config_load_summary(_CONFIG_CACHE)
_CONFIG_SUMMARY_PENDING = False
return _CONFIG_CACHE return _CONFIG_CACHE
# Load strictly from database # Load strictly from database
@@ -635,24 +658,13 @@ def load_config() -> Dict[str, Any]:
_sync_alldebrid_api_key(db_config) _sync_alldebrid_api_key(db_config)
_CONFIG_CACHE = db_config _CONFIG_CACHE = db_config
_LAST_SAVED_CONFIG = deepcopy(db_config) _LAST_SAVED_CONFIG = deepcopy(db_config)
try: if emit_summary:
# Log a compact summary to help detect startup overwrites/mismatches _log_config_load_summary(db_config)
provs = list(db_config.get("provider", {}).keys()) if isinstance(db_config.get("provider"), dict) else [] _CONFIG_SUMMARY_PENDING = False
stores = list(db_config.get("store", {}).keys()) if isinstance(db_config.get("store"), dict) else [] else:
mtime = None _CONFIG_SUMMARY_PENDING = True
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)
# Forensics disabled: audit/mismatch/backup detection removed to simplify code. # 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)
return db_config return db_config
_LAST_SAVED_CONFIG = {} _LAST_SAVED_CONFIG = {}
+1 -4
View File
@@ -146,7 +146,7 @@ def debug_panel(
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.
Automatically prepends [filename.function_name] to all output. Automatically routes through log() so debug output keeps the caller prefix.
""" """
if not _DEBUG_ENABLED: if not _DEBUG_ENABLED:
return return
@@ -166,9 +166,6 @@ def debug(*args, **kwargs) -> None:
_debug_db_log(caller_name=caller_name, message=f"<rich:{type(renderable).__name__}>") _debug_db_log(caller_name=caller_name, message=f"<rich:{type(renderable).__name__}>")
return return
# Prepend DEBUG label
args = ("DEBUG:", *args)
# Use the same logic as log() # Use the same logic as log()
log(*args, file=target_file, **kwargs) log(*args, file=target_file, **kwargs)
-5
View File
@@ -1354,9 +1354,6 @@ class Table:
"") "")
).lower() ).lower()
# Debug logging
# print(f"DEBUG: Processing dict result. Store: {store_val}, Keys: {list(visible_data.keys())}")
if store_val == "local": if store_val == "local":
# Find title field # Find title field
title_field = next( title_field = next(
@@ -1373,8 +1370,6 @@ class Table:
# Only use title suffix as fallback when ext is missing. # Only use title suffix as fallback when ext is missing.
if not str(visible_data.get("ext") or "").strip(): if not str(visible_data.get("ext") or "").strip():
visible_data["ext"] = extension 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 # Ensure 'ext' is present so it gets picked up by priority_groups in correct order
if "ext" not in visible_data: if "ext" not in visible_data:
visible_data["ext"] = "" visible_data["ext"] = ""
+10 -2
View File
@@ -1045,8 +1045,16 @@ class HydrusNetwork(Store):
if total_candidates <= hydrate_limit: if total_candidates <= hydrate_limit:
return ids_out, hashes_out return ids_out, hashes_out
debug( debug_panel(
f"{prefix} limiting metadata hydration to {hydrate_limit} of {total_candidates} candidate(s)" "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: if ids_out:
+1 -1
View File
@@ -249,7 +249,7 @@ class SharedArgs:
if config is None: if config is None:
try: try:
from SYS.config import load_config from SYS.config import load_config
config = load_config() config = load_config(emit_summary=False)
except Exception: except Exception:
SharedArgs._cached_available_stores = [] SharedArgs._cached_available_stores = []
return return
+20 -3
View File
@@ -13,7 +13,7 @@ import html
import time import time
from urllib.parse import urlparse, parse_qs, unquote, urljoin 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 SYS.payload_builders import build_file_result_payload, normalize_file_extension
from ProviderCore.registry import get_search_plugin, list_search_plugins from ProviderCore.registry import get_search_plugin, list_search_plugins
from SYS.rich_display import ( from SYS.rich_display import (
@@ -1560,9 +1560,26 @@ class search_file(Cmdlet):
source_cmd, source_args = provider.get_source_command(args_list) source_cmd, source_args = provider.get_source_command(args_list)
table.set_source_command(source_cmd, source_args) 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 "<none>"),
],
border_style="cyan",
)
results = provider.search(query, limit=limit, filters=search_filters or None) 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) # Allow providers to apply provider-specific UX transforms (e.g. auto-expansion)
try: try:
@@ -5,7 +5,7 @@ from urllib.parse import urlparse
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from ProviderCore.base import Provider, SearchResult 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 from tool.playwright import PlaywrightTool
@@ -58,7 +58,14 @@ class Bandcamp(Provider):
if not base or not discography_url: if not base or not discography_url:
return [] 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.goto(discography_url)
page.wait_for_load_state("domcontentloaded") page.wait_for_load_state("domcontentloaded")
@@ -301,7 +308,14 @@ class Bandcamp(Provider):
return [] return []
def _scrape_url(self, page: Any, url: str, limit: int) -> List[SearchResult]: 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.goto(url)
page.wait_for_load_state("domcontentloaded") page.wait_for_load_state("domcontentloaded")
+40 -6
View File
@@ -20,7 +20,7 @@ from ProviderCore.base import Provider, SearchResult, parse_inline_query_argumen
from SYS.field_access import get_field from SYS.field_access import get_field
from Provider.tidal_manifest import resolve_tidal_manifest_path from Provider.tidal_manifest import resolve_tidal_manifest_path
from SYS import pipeline as pipeline_context from SYS import pipeline as pipeline_context
from SYS.logger import debug, log from SYS.logger import debug, debug_panel, log
URL_API = ( URL_API = (
"https://triton.squid.wtf", "https://triton.squid.wtf",
@@ -1136,7 +1136,15 @@ class HIFI(Provider):
md = dict(getattr(result, "full_metadata") or {}) md = dict(getattr(result, "full_metadata") or {})
track_id = self._extract_track_id_from_result(result) 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 "<missing>"),
("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. # 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. # 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")) 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): 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. # Multi-part enrichment from API: metadata, tags, and lyrics.
full_data = self._fetch_all_track_data(track_id) 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): if isinstance(full_data, dict):
# 1. Update metadata # 1. Update metadata
api_md = full_data.get("metadata") api_md = full_data.get("metadata")
if isinstance(api_md, dict): if isinstance(api_md, dict):
debug(f"[hifi] download: updating metadata with {len(api_md)} keys")
md.update(api_md) md.update(api_md)
# 2. Update tags (re-sync result.tag so cmdlet sees them) # 2. Update tags (re-sync result.tag so cmdlet sees them)
api_tags = full_data.get("tags") api_tags = full_data.get("tags")
debug(f"[hifi] download: enrichment tags={api_tags}")
if isinstance(api_tags, list) and api_tags: if isinstance(api_tags, list) and api_tags:
result.tag = set(api_tags) result.tag = set(api_tags)
+21 -3
View File
@@ -18,7 +18,7 @@ from pathlib import Path
from API.HTTP import HTTPClient from API.HTTP import HTTPClient
from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments from ProviderCore.base import Provider, SearchResult, parse_inline_query_arguments
from ProviderCore.inline_utils import resolve_filter 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 SYS.provider_helpers import TableProviderMixin
from tool.playwright import PlaywrightTool from tool.playwright import PlaywrightTool
@@ -178,7 +178,17 @@ class Vimm(TableProviderMixin, Provider):
if region_param: if region_param:
params.append(("region", region_param)) params.append(("region", region_param))
url = f"{base}?{urlencode(params)}" 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 "<any>"),
("region", region_param or "<any>"),
("filters", normalized_filters or "<none>"),
],
border_style="cyan",
)
try: try:
with HTTPClient(timeout=9.0) as client: 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 [])] 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)] return results[: int(limit)]
def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]: def extract_query_arguments(self, query: str) -> Tuple[str, Dict[str, Any]]:
+9 -9
View File
@@ -77,7 +77,7 @@ def _ensure_interactive_stdin() -> None:
sys.stdin.flush() sys.stdin.flush()
except Exception as e: except Exception as e:
if "--debug" in sys.argv: 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: 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") raise RuntimeError("Failed to create mm.bat shim")
if args.debug: if args.debug:
print(f"DEBUG: Created mm.bat ({len(bat_text)} bytes)") print(f"[bootstrap] Created mm.bat ({len(bat_text)} bytes)")
print(f"DEBUG: Repo path embedded in shim: {repo}") print(f"[bootstrap] Repo path embedded in shim: {repo}")
print(f"DEBUG: Venv location: {repo}/.venv") print(f"[bootstrap] Venv location: {repo}/.venv")
print(f"DEBUG: Shim directory: {user_bin}") print(f"[bootstrap] Shim directory: {user_bin}")
# Add user_bin to PATH for current and future sessions # Add user_bin to PATH for current and future sessions
str_bin = str(user_bin) str_bin = str(user_bin)
@@ -1832,7 +1832,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) {
text=True text=True
) )
if args.debug and result.stderr: 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 # Also reload PATH in current session for immediate availability
reload_cmd = ( reload_cmd = (
@@ -1849,7 +1849,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) {
) )
except Exception as e: except Exception as e:
if args.debug: 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: if not args.quiet:
print(f"Installed global launcher to: {user_bin}") print(f"Installed global launcher to: {user_bin}")
@@ -1927,7 +1927,7 @@ if (Test-Path (Join-Path $repo 'CLI.py')) {
'VENV="$REPO/.venv"\n' 'VENV="$REPO/.venv"\n'
"# Debug mode: set MM_DEBUG=1 to print repository, venv, and import diagnostics\n" "# Debug mode: set MM_DEBUG=1 to print repository, venv, and import diagnostics\n"
'if [ -n "${MM_DEBUG:-}" ]; then\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 REPO: $REPO" >&2\n'
' echo "Resolved VENV: $VENV" >&2\n' ' echo "Resolved VENV: $VENV" >&2\n'
' echo "VENV exists: $( [ -d "$VENV" ] && echo yes || echo no )" >&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" " $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" " fi\n"
" done\n" " done\n"
' echo "MM_DEBUG: end diagnostics" >&2\n' ' echo "[mm-debug] end diagnostics" >&2\n'
"fi\n" "fi\n"
"\n" "\n"
"# Automatically check for updates if this is a git repository\n" "# Automatically check for updates if this is a git repository\n"