huge refactor of the entire codebase, with the goal of improving maintainability, readability, and extensibility. This commit includes changes to almost every file in the project, including:

This commit is contained in:
2026-04-19 00:41:09 -07:00
parent d9e736172a
commit bafd37fdfb
50 changed files with 3258 additions and 4177 deletions
+15 -15
View File
@@ -5,6 +5,7 @@ from importlib import import_module, reload as reload_module
from types import ModuleType
from typing import Any, Dict, List, Optional
import logging
from ProviderCore.registry import get_plugin
logger = logging.getLogger(__name__)
try:
@@ -370,22 +371,21 @@ def get_cmdlet_arg_choices(
token = matrix_conf.get("access_token")
if hs and token:
try:
from Provider.matrix import Matrix
try:
m = Matrix(config)
rooms = m.list_rooms(room_ids=ids)
choices = []
for r in rooms or []:
name = str(r.get("name") or "").strip()
rid = str(r.get("room_id") or "").strip()
choices.append(name or rid)
if choices:
return choices
except Exception as exc:
logger.exception("Matrix provider failed while listing rooms: %s", exc)
provider = get_plugin("matrix", config)
if provider is not None:
try:
rooms = provider.list_rooms(room_ids=ids)
choices = []
for r in rooms or []:
name = str(r.get("name") or "").strip()
rid = str(r.get("room_id") or "").strip()
choices.append(name or rid)
if choices:
return choices
except Exception as exc:
logger.exception("Matrix provider failed while listing rooms: %s", exc)
except Exception as exc:
logger.exception("Failed to import Matrix provider or initialize: %s", exc)
logger.exception("Failed to initialize Matrix plugin: %s", exc)
except Exception as exc:
logger.exception("Failed to resolve matrix rooms: %s", exc)
+16 -6
View File
@@ -90,10 +90,10 @@ class SharedArgs:
description="http parser",
)
PROVIDER = CmdletArg(
name="provider",
PLUGIN = CmdletArg(
name="plugin",
type="string",
description="selects provider",
description="selects plugin",
)
@staticmethod
@@ -284,7 +284,13 @@ class Cmdlet:
return {f"-{arg_name}", f"--{arg_name}"}
def build_flag_registry(self) -> Dict[str, set[str]]:
return {arg.name: self.get_flags(arg.name) for arg in self.arg}
registry: Dict[str, set[str]] = {}
for arg in self.arg:
try:
registry[arg.name] = {str(flag).lower() for flag in arg.to_flags()}
except Exception:
registry[arg.name] = {flag.lower() for flag in self.get_flags(arg.name)}
return registry
def parse_cmdlet_args(
@@ -335,8 +341,12 @@ def parse_cmdlet_args(
positional_args.append(spec)
arg_spec_map[canonical_key] = canonical_name
arg_spec_map[f"-{canonical_name}".lower()] = canonical_name
arg_spec_map[f"--{canonical_name}".lower()] = canonical_name
try:
for flag in spec.to_flags():
arg_spec_map[str(flag).lower()] = canonical_name
except Exception:
arg_spec_map[f"-{canonical_name}".lower()] = canonical_name
arg_spec_map[f"--{canonical_name}".lower()] = canonical_name
i = 0
positional_index = 0
+10 -29
View File
@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple
from ProviderCore.registry import get_plugin
from SYS.yt_metadata import extract_ytdlp_tags
try: # Optional; used when available for richer metadata fetches
@@ -2213,40 +2214,20 @@ def enrich_playlist_entries(entries: list, extractor: str) -> list:
Returns:
List of enriched entry dicts
"""
# Import here to avoid circular dependency
from tool.ytdlp import is_url_supported_by_ytdlp
if not entries:
return entries
enriched = []
for entry in entries:
# If entry has a direct URL, fetch its full metadata
entry_url = entry.get("url")
if entry_url and is_url_supported_by_ytdlp(entry_url):
try:
import yt_dlp
plugin = get_plugin("ytdlp", {})
if plugin is None:
return entries
ydl_opts: Any = {
"quiet": True,
"no_warnings": True,
"skip_download": True,
"noprogress": True,
"socket_timeout": 5,
"retries": 1,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
full_info = ydl.extract_info(entry_url, download=False)
if full_info:
enriched.append(full_info)
continue
except Exception:
logger.exception("Failed to fetch full metadata for entry URL: %s", entry_url)
try:
enriched = plugin.enrich_playlist_entries(entries, extractor=extractor)
except Exception:
logger.exception("Failed to enrich playlist entries for extractor: %s", extractor)
return entries
# Fallback to original entry if fetch failed
enriched.append(entry)
return enriched
return enriched if isinstance(enriched, list) else entries
def format_playlist_entry(entry: Dict[str,
+126 -79
View File
@@ -1505,9 +1505,9 @@ class PipelineExecutor:
"table") else None
)
# Prefer an explicit provider hint from table metadata when available.
# Prefer an explicit plugin hint from table metadata when available.
# This keeps @N selectors working even when row payloads don't carry a
# provider key (or when they carry a table-type like tidal.album).
# plugin key (or when they carry a table-type like tidal.album).
try:
meta = (
current_table.get_table_metadata()
@@ -1517,56 +1517,58 @@ class PipelineExecutor:
except Exception:
meta = None
if isinstance(meta, dict):
_add(meta.get("plugin"))
_add(meta.get("provider"))
except Exception:
logger.exception("Failed to inspect current_table/table metadata in _maybe_run_class_selector")
for item in selected_items or []:
if isinstance(item, dict):
_add(item.get("plugin"))
_add(item.get("provider"))
_add(item.get("store"))
_add(item.get("table"))
else:
_add(getattr(item, "plugin", None))
_add(getattr(item, "provider", None))
_add(getattr(item, "store", None))
_add(getattr(item, "table", None))
try:
from ProviderCore.registry import get_provider, is_known_provider_name
from ProviderCore.registry import get_plugin, is_known_plugin_name
except Exception:
get_provider = None # type: ignore
is_known_provider_name = None # type: ignore
get_plugin = None # type: ignore
is_known_plugin_name = None # type: ignore
# If we have a table-type like "tidal.album", also try its provider prefix ("tidal")
# when that prefix is a registered provider name.
if is_known_provider_name is not None:
# If we have a table-type like "tidal.album", also try its plugin prefix ("tidal")
# when that prefix is a registered plugin name.
if is_known_plugin_name is not None:
try:
for key in list(candidates):
if not isinstance(key, str):
continue
if "." not in key:
continue
if is_known_provider_name(key):
if is_known_plugin_name(key):
continue
prefix = str(key).split(".", 1)[0].strip().lower()
if prefix and is_known_provider_name(prefix):
if prefix and is_known_plugin_name(prefix):
_add(prefix)
except Exception:
logger.exception("Failed while computing provider prefix heuristics in _maybe_run_class_selector")
logger.exception("Failed while computing plugin prefix heuristics in _maybe_run_class_selector")
if get_provider is not None:
if get_plugin is not None:
for key in candidates:
try:
if is_known_provider_name is not None and (
not is_known_provider_name(key)):
if is_known_plugin_name is not None and (
not is_known_plugin_name(key)):
continue
except Exception:
# If the predicate fails for any reason, fall back to legacy behavior.
logger.exception("is_known_provider_name predicate failed for key %s; falling back", key)
logger.exception("is_known_plugin_name predicate failed for key %s; falling back", key)
try:
provider = get_provider(key, config)
provider = get_plugin(key, config)
except Exception as exc:
logger.exception("Failed to load provider '%s' during selector resolution: %s", key, exc)
logger.exception("Failed to load plugin '%s' during selector resolution: %s", key, exc)
continue
selector = getattr(provider, "selector", None)
if selector is None:
@@ -1583,6 +1585,92 @@ class PipelineExecutor:
if handled:
return True
@staticmethod
def _maybe_expand_plugin_selection(
selected_items: List[Any],
*,
ctx: Any,
config: Dict[str, Any],
stage_table: Any,
) -> Optional[List[Any]]:
candidates: list[str] = []
def _add(value: Any) -> None:
text = str(value or "").strip().lower()
if text and text not in candidates:
candidates.append(text)
table_type = None
try:
table_type = stage_table.table if stage_table is not None and hasattr(stage_table, "table") else None
except Exception:
table_type = None
_add(table_type)
try:
meta = (
stage_table.get_table_metadata()
if stage_table is not None and hasattr(stage_table, "get_table_metadata")
else getattr(stage_table, "table_metadata", None)
)
except Exception:
meta = None
if isinstance(meta, dict):
_add(meta.get("plugin"))
_add(meta.get("provider"))
for item in selected_items or []:
if isinstance(item, dict):
_add(item.get("plugin"))
_add(item.get("provider"))
_add(item.get("table"))
_add(item.get("source"))
else:
_add(getattr(item, "plugin", None))
_add(getattr(item, "provider", None))
_add(getattr(item, "table", None))
_add(getattr(item, "source", None))
try:
from ProviderCore.registry import get_plugin, is_known_plugin_name
except Exception:
return None
for key in list(candidates):
if "." in key:
prefix = str(key).split(".", 1)[0].strip().lower()
if prefix and prefix not in candidates:
candidates.append(prefix)
for key in candidates:
try:
if not is_known_plugin_name(key):
continue
except Exception:
continue
try:
plugin = get_plugin(key, config)
except Exception:
continue
if plugin is None:
continue
expand = getattr(plugin, "expand_selection", None)
if not callable(expand):
continue
try:
expanded = expand(
selected_items,
ctx=ctx,
stage_is_last=False,
table_type=str(table_type or ""),
)
except Exception:
logger.exception("%s expand_selection failed", key)
return None
if expanded:
return list(expanded)
return None
store_keys: list[str] = []
for item in selected_items or []:
if isinstance(item, dict):
@@ -1998,10 +2086,10 @@ class PipelineExecutor:
# IMPORTANT: Put selected row args *before* source_args.
# Rationale: The cmdlet argument parser treats the *first* unknown
# token as a positional value (e.g., URL). If `source_args`
# contain unknown flags (like -provider which download-file does
# contain unknown flags (like a removed legacy flag that download-file does
# not declare), they could be misinterpreted as the positional
# URL argument and cause attempts to download strings like
# "-provider" (which is invalid). By placing selection args
# not accept). By placing selection args
# first we ensure the intended URL/selection token is parsed
# as the positional URL and avoid this class of parsing errors.
expanded_stage: List[str] = cmd_list + selected_row_args + source_args
@@ -2081,66 +2169,15 @@ class PipelineExecutor:
print("No items matched selection in pipeline\n")
return False, None
# Provider selection expansion (non-terminal): allow certain provider tables
# (e.g. tidal.album) to expand to multiple downstream items when the user
# pipes into another stage (e.g. @N | .mpv or @N | add-file).
table_type_hint = None
try:
table_type_hint = (
stage_table.table
if stage_table is not None and hasattr(stage_table, "table")
else None
if stages:
expanded = PipelineExecutor._maybe_expand_plugin_selection(
filtered,
ctx=ctx,
config=config,
stage_table=stage_table,
)
except Exception:
table_type_hint = None
if stages and isinstance(table_type_hint, str) and table_type_hint.strip().lower() == "tidal.album":
try:
from ProviderCore.registry import get_provider
prov = get_provider("tidal", config)
except Exception:
prov = None
if prov is not None and hasattr(prov, "_extract_album_selection_context") and hasattr(prov, "_tracks_for_album"):
try:
album_contexts = prov._extract_album_selection_context(filtered) # type: ignore[attr-defined]
except Exception:
album_contexts = []
track_items: List[Any] = []
seen_track_ids: set[int] = set()
for album_id, album_title, artist_name in album_contexts or []:
try:
track_results = prov._tracks_for_album( # type: ignore[attr-defined]
album_id=album_id,
album_title=album_title,
artist_name=artist_name,
limit=500,
)
except Exception:
track_results = []
for tr in track_results or []:
try:
md = getattr(tr, "full_metadata", None)
tid = None
if isinstance(md, dict):
raw_id = md.get("trackId") or md.get("id")
try:
tid = int(raw_id) if raw_id is not None else None
except Exception:
tid = None
if tid is not None:
if tid in seen_track_ids:
continue
seen_track_ids.add(tid)
except Exception:
logger.exception("Failed to extract/parse track metadata in album processing")
track_items.append(tr)
if track_items:
filtered = track_items
table_type_hint = "tidal.track"
if expanded:
filtered = expanded
if PipelineExecutor._maybe_run_class_selector(
ctx,
@@ -2177,6 +2214,16 @@ class PipelineExecutor:
except Exception:
logger.exception("Failed to determine current_table for selection auto-insert; defaulting to None")
current_table = None
table_type_hint = None
try:
raw_table_type = (
stage_table.table
if stage_table is not None and hasattr(stage_table, "table") else None
)
if isinstance(raw_table_type, str) and raw_table_type.strip():
table_type_hint = raw_table_type
except Exception:
table_type_hint = None
table_type = None
try:
if isinstance(table_type_hint, str) and table_type_hint.strip():
+23 -29
View File
@@ -1,10 +1,4 @@
"""Provider registry for ResultTable API (breaking, strict API).
Providers register themselves here with an adapter and optional column factory
and selection function. Consumers (cmdlets) can look up providers by name and
obtain the columns and selection behavior for building tables and for selection
args used by subsequent cmdlets.
"""
"""Plugin registry for the strict ResultTable API."""
from __future__ import annotations
from dataclasses import dataclass
@@ -18,7 +12,7 @@ SelectionFn = Callable[[ResultModel], List[str]]
@dataclass
class Provider:
class Plugin:
name: str
adapter: ProviderAdapter
# columns can be a static list or a factory that derives columns from sample rows
@@ -28,7 +22,7 @@ class Provider:
def get_columns(self, rows: Optional[Iterable[ResultModel]] = None) -> List[ColumnSpec]:
if self.columns is None:
raise ValueError(f"provider '{self.name}' must define columns")
raise ValueError(f"plugin '{self.name}' must define columns")
if callable(self.columns):
rows_list = list(rows) if rows is not None else []
@@ -37,13 +31,13 @@ class Provider:
cols = list(self.columns)
if not cols:
raise ValueError(f"provider '{self.name}' produced no columns")
raise ValueError(f"plugin '{self.name}' produced no columns")
return cols
def selection_args(self, row: ResultModel) -> List[str]:
if not callable(self.selection_fn):
raise ValueError(f"provider '{self.name}' must define a selection function")
raise ValueError(f"plugin '{self.name}' must define a selection function")
sel = list(self.selection_fn(ensure_result_model(row)))
return sel
@@ -54,7 +48,7 @@ class Provider:
try:
rows = [ensure_result_model(r) for r in self.adapter(items)]
except Exception as exc:
raise RuntimeError(f"provider '{self.name}' adapter failed") from exc
raise RuntimeError(f"plugin '{self.name}' adapter failed") from exc
cols = self.get_columns(rows)
return ResultTable(provider=self.name, rows=rows, columns=cols, meta=self.metadata or {})
@@ -82,37 +76,37 @@ class Provider:
return [self.serialize_row(r) for r in rows]
_PROVIDERS: Dict[str, Provider] = {}
_PLUGINS: Dict[str, Plugin] = {}
def register_provider(
def register_plugin(
name: str,
adapter: ProviderAdapter,
*,
columns: Union[List[ColumnSpec], ColumnFactory],
selection_fn: SelectionFn,
metadata: Optional[Dict[str, Any]] = None,
) -> Provider:
) -> Plugin:
name = str(name or "").strip().lower()
if not name:
raise ValueError("provider name required")
if name in _PROVIDERS:
raise ValueError(f"provider already registered: {name}")
raise ValueError("plugin name required")
if name in _PLUGINS:
raise ValueError(f"plugin already registered: {name}")
if columns is None:
raise ValueError("provider registration requires columns")
raise ValueError("plugin registration requires columns")
if selection_fn is None:
raise ValueError("provider registration requires selection_fn")
p = Provider(name=name, adapter=adapter, columns=columns, selection_fn=selection_fn, metadata=metadata)
_PROVIDERS[name] = p
return p
raise ValueError("plugin registration requires selection_fn")
plugin = Plugin(name=name, adapter=adapter, columns=columns, selection_fn=selection_fn, metadata=metadata)
_PLUGINS[name] = plugin
return plugin
def get_provider(name: str) -> Provider:
def get_plugin(name: str) -> Plugin:
normalized = str(name or "").lower()
if normalized not in _PROVIDERS:
raise KeyError(f"provider not registered: {name}")
return _PROVIDERS[normalized]
if normalized not in _PLUGINS:
raise KeyError(f"plugin not registered: {name}")
return _PLUGINS[normalized]
def list_providers() -> List[str]:
return list(_PROVIDERS.keys())
def list_plugins() -> List[str]:
return list(_PLUGINS.keys())
+3 -3
View File
@@ -148,7 +148,7 @@ def show_store_config_panel(
def show_available_providers_panel(provider_names: List[str]) -> None:
"""Show a Rich panel listing available/configured providers."""
"""Show a Rich panel listing available/configured plugins."""
from rich.columns import Columns
from rich.console import Group
@@ -164,13 +164,13 @@ def show_available_providers_panel(provider_names: List[str]) -> None:
)
group = Group(
Text("The following providers are configured and ready to use:\n"),
Text("The following plugins are configured and ready to use:\n"),
cols
)
panel = Panel(
group,
title="[bold green]Configured Providers[/bold green]",
title="[bold green]Configured Plugins[/bold green]",
border_style="green",
padding=(1, 2)
)