pre-migration commit
This commit is contained in:
+41
-8
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|
||||||
|
|||||||
@@ -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 []
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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]]:
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user