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 .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 "<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:
"""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
+3 -3
View File
@@ -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",
+107 -30
View File
@@ -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 {}
+31 -7
View File
@@ -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 []
+1 -1
View File
@@ -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 {}
+1 -1
View File
@@ -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
+32 -20
View File
@@ -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 = {}
+1 -4
View File
@@ -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"<rich:{type(renderable).__name__}>")
return
# Prepend DEBUG label
args = ("DEBUG:", *args)
# Use the same logic as log()
log(*args, file=target_file, **kwargs)
-5
View File
@@ -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"] = ""
+10 -2
View File
@@ -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:
+1 -1
View File
@@ -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
+20 -3
View File
@@ -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 "<none>"),
],
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:
@@ -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")
+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 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 "<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.
# 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)
+21 -3
View File
@@ -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 "<any>"),
("region", region_param or "<any>"),
("filters", normalized_filters or "<none>"),
],
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]]:
+9 -9
View File
@@ -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"