jj
This commit is contained in:
2
CLI.py
2
CLI.py
@@ -1935,7 +1935,7 @@ class CmdletExecutor:
|
|||||||
if ret_code != 0:
|
if ret_code != 0:
|
||||||
stage_status = "failed"
|
stage_status = "failed"
|
||||||
stage_error = f"exit code {ret_code}"
|
stage_error = f"exit code {ret_code}"
|
||||||
print(f"[exit code: {ret_code}]\n")
|
# No print here - we want to keep output clean and avoid redundant "exit code" notices.
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
stage_status = "failed"
|
stage_status = "failed"
|
||||||
stage_error = f"{type(exc).__name__}: {exc}"
|
stage_error = f"{type(exc).__name__}: {exc}"
|
||||||
|
|||||||
@@ -47,13 +47,23 @@ async def _suppress_aioslsk_asyncio_task_noise() -> Any:
|
|||||||
try:
|
try:
|
||||||
exc = context.get("exception")
|
exc = context.get("exception")
|
||||||
msg = str(context.get("message") or "")
|
msg = str(context.get("message") or "")
|
||||||
# Only suppress un-retrieved task exceptions from aioslsk connection failures.
|
|
||||||
if msg == "Task exception was never retrieved" and exc is not None:
|
if exc is not None:
|
||||||
cls = getattr(exc, "__class__", None)
|
# Suppress internal asyncio AssertionError on Windows teardown (Proactor loop)
|
||||||
name = getattr(cls, "__name__", "")
|
if isinstance(exc, AssertionError):
|
||||||
mod = getattr(cls, "__module__", "")
|
m_lower = msg.lower()
|
||||||
if name == "ConnectionFailedError" and str(mod).startswith("aioslsk"):
|
if "proactor" in m_lower or "_start_serving" in m_lower or "self._sockets is not None" in str(exc):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Only suppress un-retrieved task exceptions from aioslsk connection failures.
|
||||||
|
if msg == "Task exception was never retrieved":
|
||||||
|
cls = getattr(exc, "__class__", None)
|
||||||
|
name = getattr(cls, "__name__", "")
|
||||||
|
mod = getattr(cls, "__module__", "")
|
||||||
|
|
||||||
|
# Suppress ConnectionFailedError from aioslsk
|
||||||
|
if name == "ConnectionFailedError" and str(mod).startswith("aioslsk"):
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
# If our filter logic fails, fall through to default handling.
|
# If our filter logic fails, fall through to default handling.
|
||||||
pass
|
pass
|
||||||
@@ -206,10 +216,34 @@ def _suppress_aioslsk_noise() -> Any:
|
|||||||
class Soulseek(Provider):
|
class Soulseek(Provider):
|
||||||
|
|
||||||
TABLE_AUTO_STAGES = {
|
TABLE_AUTO_STAGES = {
|
||||||
"soulseek": ["download-file"],
|
"soulseek": ["download-file", "-provider", "soulseek"],
|
||||||
}
|
}
|
||||||
"""Search provider for Soulseek P2P network."""
|
"""Search provider for Soulseek P2P network."""
|
||||||
|
|
||||||
|
def selector(
|
||||||
|
self,
|
||||||
|
selected_items: List[Any],
|
||||||
|
*,
|
||||||
|
ctx: Any,
|
||||||
|
stage_is_last: bool = True,
|
||||||
|
**_kwargs: Any,
|
||||||
|
) -> bool:
|
||||||
|
"""Handle Soulseek selection.
|
||||||
|
|
||||||
|
Currently defaults to download-file via TABLE_AUTO_STAGES, but this
|
||||||
|
hook allows for future 'Browse User' or 'Browse Folder' drill-down.
|
||||||
|
"""
|
||||||
|
if not stage_is_last:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If we wanted to handle drill-down (like HIFI.py) we would:
|
||||||
|
# 1. Fetch more data (e.g. user shares)
|
||||||
|
# 2. Create a new ResultTable
|
||||||
|
# 3. ctx.set_current_stage_table(new_table)
|
||||||
|
# 4. return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def config(cls) -> List[Dict[str, Any]]:
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
@@ -277,6 +311,13 @@ class Soulseek(Provider):
|
|||||||
username = full_metadata.get("username")
|
username = full_metadata.get("username")
|
||||||
filename = full_metadata.get("filename") or result.path
|
filename = full_metadata.get("filename") or result.path
|
||||||
|
|
||||||
|
if not username or not filename:
|
||||||
|
# If we were invoked via generic download-file on a SearchResult
|
||||||
|
# that has minimal data (e.g. from table selection), try to rescue it.
|
||||||
|
if isinstance(result, SearchResult) and result.full_metadata:
|
||||||
|
username = result.full_metadata.get("username")
|
||||||
|
filename = result.full_metadata.get("filename")
|
||||||
|
|
||||||
if not username or not filename:
|
if not username or not filename:
|
||||||
log(
|
log(
|
||||||
f"[soulseek] Missing metadata for download: {result.title}",
|
f"[soulseek] Missing metadata for download: {result.title}",
|
||||||
@@ -284,12 +325,20 @@ class Soulseek(Provider):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Use tempfile directory as default if '.' or generic placeholder was passed
|
||||||
|
# by a caller that didn't know better.
|
||||||
|
target_dir = Path(output_dir)
|
||||||
|
if str(target_dir) == "." or str(target_dir) == "downloads":
|
||||||
|
import tempfile
|
||||||
|
target_dir = Path(tempfile.gettempdir()) / "Medios" / "Soulseek"
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# This cmdlet stack is synchronous; use asyncio.run for clarity.
|
# This cmdlet stack is synchronous; use asyncio.run for clarity.
|
||||||
return asyncio.run(
|
return asyncio.run(
|
||||||
download_soulseek_file(
|
download_soulseek_file(
|
||||||
username=username,
|
username=username,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
output_dir=output_dir,
|
output_dir=target_dir,
|
||||||
timeout=self.MAX_WAIT_TRANSFER,
|
timeout=self.MAX_WAIT_TRANSFER,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -328,7 +377,7 @@ class Soulseek(Provider):
|
|||||||
from aioslsk.client import SoulSeekClient
|
from aioslsk.client import SoulSeekClient
|
||||||
from aioslsk.settings import CredentialsSettings, Settings
|
from aioslsk.settings import CredentialsSettings, Settings
|
||||||
|
|
||||||
os.makedirs(self.DOWNLOAD_DIR, exist_ok=True)
|
# Removed legacy os.makedirs(self.DOWNLOAD_DIR) - specific commands handle output dirs.
|
||||||
|
|
||||||
settings = Settings(
|
settings = Settings(
|
||||||
credentials=CredentialsSettings(
|
credentials=CredentialsSettings(
|
||||||
@@ -376,8 +425,9 @@ class Soulseek(Provider):
|
|||||||
await client.stop()
|
await client.stop()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Give Proactor/Windows loop a moment to drain internal buffers after stop.
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0.2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -386,33 +436,19 @@ class Soulseek(Provider):
|
|||||||
for result in getattr(search_request, "results", []):
|
for result in getattr(search_request, "results", []):
|
||||||
username = getattr(result, "username", "?")
|
username = getattr(result, "username", "?")
|
||||||
|
|
||||||
|
def _add(file_data: Any) -> None:
|
||||||
|
flat.append({
|
||||||
|
"file": file_data,
|
||||||
|
"username": username,
|
||||||
|
"filename": getattr(file_data, "filename", "?"),
|
||||||
|
"size": getattr(file_data, "filesize", 0)
|
||||||
|
})
|
||||||
|
|
||||||
for file_data in getattr(result, "shared_items", []):
|
for file_data in getattr(result, "shared_items", []):
|
||||||
flat.append(
|
_add(file_data)
|
||||||
{
|
|
||||||
"file": file_data,
|
|
||||||
"username": username,
|
|
||||||
"filename": getattr(file_data,
|
|
||||||
"filename",
|
|
||||||
"?"),
|
|
||||||
"size": getattr(file_data,
|
|
||||||
"filesize",
|
|
||||||
0),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for file_data in getattr(result, "locked_results", []):
|
for file_data in getattr(result, "locked_results", []):
|
||||||
flat.append(
|
_add(file_data)
|
||||||
{
|
|
||||||
"file": file_data,
|
|
||||||
"username": username,
|
|
||||||
"filename": getattr(file_data,
|
|
||||||
"filename",
|
|
||||||
"?"),
|
|
||||||
"size": getattr(file_data,
|
|
||||||
"filesize",
|
|
||||||
0),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return flat
|
return flat
|
||||||
|
|
||||||
@@ -440,6 +476,11 @@ class Soulseek(Provider):
|
|||||||
) -> List[SearchResult]:
|
) -> List[SearchResult]:
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
|
|
||||||
|
# Ensure temp download dir structure exists, but don't create legacy ./downloads here.
|
||||||
|
import tempfile
|
||||||
|
base_tmp = Path(tempfile.gettempdir()) / "Medios" / "Soulseek"
|
||||||
|
base_tmp.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
flat_results = asyncio.run(
|
flat_results = asyncio.run(
|
||||||
self.perform_search(query,
|
self.perform_search(query,
|
||||||
@@ -561,6 +602,7 @@ class Soulseek(Provider):
|
|||||||
media_kind="audio",
|
media_kind="audio",
|
||||||
size_bytes=item["size"],
|
size_bytes=item["size"],
|
||||||
columns=columns,
|
columns=columns,
|
||||||
|
selection_action=["download-file", "-provider", "soulseek"],
|
||||||
full_metadata={
|
full_metadata={
|
||||||
"username": item["username"],
|
"username": item["username"],
|
||||||
"filename": item["filename"],
|
"filename": item["filename"],
|
||||||
@@ -568,6 +610,7 @@ class Soulseek(Provider):
|
|||||||
"album": item["album"],
|
"album": item["album"],
|
||||||
"track_num": item["track_num"],
|
"track_num": item["track_num"],
|
||||||
"ext": item["ext"],
|
"ext": item["ext"],
|
||||||
|
"provider": "soulseek"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -785,8 +828,9 @@ async def download_soulseek_file(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# Let cancellation/cleanup callbacks run while our exception handler is still installed.
|
# Let cancellation/cleanup callbacks run while our exception handler is still installed.
|
||||||
|
# Increased to 0.2s for Windows Proactor loop stability.
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0.2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class SearchResult:
|
|||||||
size_bytes: Optional[int] = None
|
size_bytes: Optional[int] = None
|
||||||
tag: set[str] = field(default_factory=set) # Searchable tag values
|
tag: set[str] = field(default_factory=set) # Searchable tag values
|
||||||
columns: List[Tuple[str, str]] = field(default_factory=list) # Display columns
|
columns: List[Tuple[str, str]] = field(default_factory=list) # Display columns
|
||||||
|
selection_action: Optional[Dict[str, Any]] = None
|
||||||
|
selection_args: Optional[List[str]] = None
|
||||||
full_metadata: Dict[str, Any] = field(default_factory=dict) # Extra metadata
|
full_metadata: Dict[str, Any] = field(default_factory=dict) # Extra metadata
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from types import ModuleType
|
|||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type
|
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Type
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from SYS.logger import log
|
from SYS.logger import log, debug
|
||||||
|
|
||||||
from ProviderCore.base import FileProvider, Provider, SearchProvider, SearchResult
|
from ProviderCore.base import FileProvider, Provider, SearchProvider, SearchResult
|
||||||
from Provider.soulseek import download_soulseek_file
|
from Provider.soulseek import download_soulseek_file
|
||||||
@@ -265,17 +265,17 @@ def _provider_url_patterns(provider_class: Type[Provider]) -> Sequence[str]:
|
|||||||
def get_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
|
def get_provider(name: str, config: Optional[Dict[str, Any]] = None) -> Optional[Provider]:
|
||||||
info = REGISTRY.get(name)
|
info = REGISTRY.get(name)
|
||||||
if info is None:
|
if info is None:
|
||||||
log(f"[provider] Unknown provider: {name}", file=sys.stderr)
|
debug(f"[provider] Unknown provider: {name}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider = info.provider_class(config)
|
provider = info.provider_class(config)
|
||||||
if not provider.validate():
|
if not provider.validate():
|
||||||
log(f"[provider] Provider '{name}' is not available", file=sys.stderr)
|
debug(f"[provider] Provider '{name}' is not available")
|
||||||
return None
|
return None
|
||||||
return provider
|
return provider
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"[provider] Error initializing '{name}': {exc}", file=sys.stderr)
|
debug(f"[provider] Error initializing '{name}': {exc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ def get_search_provider(name: str,
|
|||||||
if provider is None:
|
if provider is None:
|
||||||
return None
|
return None
|
||||||
if not _supports_search(provider):
|
if not _supports_search(provider):
|
||||||
log(f"[provider] Provider '{name}' does not support search", file=sys.stderr)
|
debug(f"[provider] Provider '{name}' does not support search")
|
||||||
return None
|
return None
|
||||||
return provider # type: ignore[return-value]
|
return provider # type: ignore[return-value]
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ def get_file_provider(name: str,
|
|||||||
if provider is None:
|
if provider is None:
|
||||||
return None
|
return None
|
||||||
if not _supports_upload(provider):
|
if not _supports_upload(provider):
|
||||||
log(f"[provider] Provider '{name}' does not support upload", file=sys.stderr)
|
debug(f"[provider] Provider '{name}' does not support upload")
|
||||||
return None
|
return None
|
||||||
return provider # type: ignore[return-value]
|
return provider # type: ignore[return-value]
|
||||||
|
|
||||||
|
|||||||
@@ -77,23 +77,108 @@ def capture_rich_output(*, stdout: TextIO, stderr: TextIO) -> Iterator[None]:
|
|||||||
|
|
||||||
|
|
||||||
def show_provider_config_panel(
|
def show_provider_config_panel(
|
||||||
provider_name: str,
|
provider_names: str | List[str],
|
||||||
keys: Sequence[str] | None = None,
|
|
||||||
*,
|
|
||||||
config_hint: str = "config.conf"
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Show a Rich panel explaining how to configure a provider."""
|
"""Show a Rich panel explaining how to configure providers."""
|
||||||
pass
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.console import Group
|
||||||
|
|
||||||
|
if isinstance(provider_names, str):
|
||||||
|
providers = [p.strip() for p in provider_names.split(",")]
|
||||||
|
else:
|
||||||
|
providers = provider_names
|
||||||
|
|
||||||
|
table = Table.grid(padding=(0, 1))
|
||||||
|
table.add_column(style="bold red")
|
||||||
|
|
||||||
|
for provider in providers:
|
||||||
|
table.add_row(f" • {provider}")
|
||||||
|
|
||||||
|
group = Group(
|
||||||
|
Text("The following providers are not configured and cannot be used:\n"),
|
||||||
|
table,
|
||||||
|
Text.from_markup("\nTo configure them, run the command with [bold cyan].config[/bold cyan] or use the [bold green]TUI[/bold green] config menu.")
|
||||||
|
)
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
group,
|
||||||
|
title="[bold red]Configuration Required[/bold red]",
|
||||||
|
border_style="red",
|
||||||
|
padding=(1, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout_console().print()
|
||||||
|
stdout_console().print(panel)
|
||||||
|
|
||||||
|
|
||||||
def show_store_config_panel(
|
def show_store_config_panel(
|
||||||
store_type: str,
|
store_names: str | List[str],
|
||||||
keys: Sequence[str] | None = None,
|
|
||||||
*,
|
|
||||||
config_hint: str = "config.conf"
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Show a Rich panel explaining how to configure a storage backend."""
|
"""Show a Rich panel explaining how to configure storage backends."""
|
||||||
pass
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
from rich.console import Group
|
||||||
|
|
||||||
|
if isinstance(store_names, str):
|
||||||
|
stores = [s.strip() for s in store_names.split(",")]
|
||||||
|
else:
|
||||||
|
stores = store_names
|
||||||
|
|
||||||
|
table = Table.grid(padding=(0, 1))
|
||||||
|
table.add_column(style="bold yellow")
|
||||||
|
|
||||||
|
for store in stores:
|
||||||
|
table.add_row(f" • {store}")
|
||||||
|
|
||||||
|
group = Group(
|
||||||
|
Text("The following stores are not configured or available:\n"),
|
||||||
|
table,
|
||||||
|
Text.from_markup("\nInitialize them using [bold cyan].config[/bold cyan] or ensure they are properly set up.")
|
||||||
|
)
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
group,
|
||||||
|
title="[bold yellow]Store Not Configured[/bold yellow]",
|
||||||
|
border_style="yellow",
|
||||||
|
padding=(1, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout_console().print()
|
||||||
|
stdout_console().print(panel)
|
||||||
|
|
||||||
|
|
||||||
|
def show_available_providers_panel(provider_names: List[str]) -> None:
|
||||||
|
"""Show a Rich panel listing available/configured providers."""
|
||||||
|
from rich.columns import Columns
|
||||||
|
from rich.console import Group
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
if not provider_names:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use Columns to display them efficiently in the panel
|
||||||
|
cols = Columns(
|
||||||
|
[f"[bold green] \u2713 [/bold green]{p}" for p in sorted(provider_names)],
|
||||||
|
equal=True,
|
||||||
|
column_first=True,
|
||||||
|
expand=True
|
||||||
|
)
|
||||||
|
|
||||||
|
group = Group(
|
||||||
|
Text("The following providers are configured and ready to use:\n"),
|
||||||
|
cols
|
||||||
|
)
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
group,
|
||||||
|
title="[bold green]Configured Providers[/bold green]",
|
||||||
|
border_style="green",
|
||||||
|
padding=(1, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout_console().print()
|
||||||
|
stdout_console().print(panel)
|
||||||
|
|
||||||
|
|
||||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
|
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tiff"}
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ import sys
|
|||||||
from SYS.logger import log, debug
|
from SYS.logger import log, debug
|
||||||
from ProviderCore.registry import get_search_provider, list_search_providers
|
from ProviderCore.registry import get_search_provider, list_search_providers
|
||||||
from SYS.config import get_local_storage_path
|
from SYS.config import get_local_storage_path
|
||||||
|
from SYS.rich_display import (
|
||||||
|
show_provider_config_panel,
|
||||||
|
show_store_config_panel,
|
||||||
|
show_available_providers_panel,
|
||||||
|
)
|
||||||
|
|
||||||
from . import _shared as sh
|
from . import _shared as sh
|
||||||
|
|
||||||
@@ -165,29 +170,54 @@ class search_file(Cmdlet):
|
|||||||
"""Execute external provider search."""
|
"""Execute external provider search."""
|
||||||
|
|
||||||
if not provider_name or not query:
|
if not provider_name or not query:
|
||||||
|
from SYS import pipeline as ctx_mod
|
||||||
|
progress = None
|
||||||
|
if hasattr(ctx_mod, "get_pipeline_state"):
|
||||||
|
progress = ctx_mod.get_pipeline_state().live_progress
|
||||||
|
if progress:
|
||||||
|
try:
|
||||||
|
progress.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
log("Error: search-file -provider requires both provider and query", file=sys.stderr)
|
log("Error: search-file -provider requires both provider and query", file=sys.stderr)
|
||||||
log(f"Usage: {self.usage}", file=sys.stderr)
|
log(f"Usage: {self.usage}", file=sys.stderr)
|
||||||
log("Available providers:", file=sys.stderr)
|
|
||||||
providers = list_search_providers(config)
|
providers_map = list_search_providers(config)
|
||||||
for name, available in sorted(providers.items()):
|
available = [n for n, a in providers_map.items() if a]
|
||||||
status = "\u2713" if available else "\u2717"
|
unconfigured = [n for n, a in providers_map.items() if not a]
|
||||||
log(f" {status} {name}", file=sys.stderr)
|
|
||||||
|
if unconfigured:
|
||||||
|
show_provider_config_panel(unconfigured)
|
||||||
|
|
||||||
|
if available:
|
||||||
|
show_available_providers_panel(available)
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Align with provider default when user did not set -limit.
|
# Align with provider default when user did not set -limit.
|
||||||
if not limit_set:
|
if not limit_set:
|
||||||
limit = 50
|
limit = 50
|
||||||
|
|
||||||
debug(f"[search-file] provider={provider_name}, query={query}, limit={limit}, open_id={open_id}")
|
from SYS import pipeline as ctx_mod
|
||||||
|
progress = None
|
||||||
|
if hasattr(ctx_mod, "get_pipeline_state"):
|
||||||
|
progress = ctx_mod.get_pipeline_state().live_progress
|
||||||
|
|
||||||
provider = get_search_provider(provider_name, config)
|
provider = get_search_provider(provider_name, config)
|
||||||
if not provider:
|
if not provider:
|
||||||
log(f"Error: Provider '{provider_name}' is not available", file=sys.stderr)
|
if progress:
|
||||||
log("Available providers:", file=sys.stderr)
|
try:
|
||||||
providers = list_search_providers(config)
|
progress.stop()
|
||||||
for name, available in sorted(providers.items()):
|
except Exception:
|
||||||
if available:
|
pass
|
||||||
log(f" - {name}", file=sys.stderr)
|
|
||||||
|
show_provider_config_panel([provider_name])
|
||||||
|
|
||||||
|
providers_map = list_search_providers(config)
|
||||||
|
available = [n for n, a in providers_map.items() if a]
|
||||||
|
if available:
|
||||||
|
show_available_providers_panel(available)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
worker_id = str(uuid.uuid4())
|
worker_id = str(uuid.uuid4())
|
||||||
@@ -595,7 +625,16 @@ class search_file(Cmdlet):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not library_root:
|
if not library_root:
|
||||||
log("No library root configured. Use the .config command to set up storage.", file=sys.stderr)
|
from SYS import pipeline as ctx_mod
|
||||||
|
progress = None
|
||||||
|
if hasattr(ctx_mod, "get_pipeline_state"):
|
||||||
|
progress = ctx_mod.get_pipeline_state().live_progress
|
||||||
|
if progress:
|
||||||
|
try:
|
||||||
|
progress.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
show_store_config_panel(["Folder Store"])
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# Use context manager to ensure database is always closed
|
# Use context manager to ensure database is always closed
|
||||||
|
|||||||
Reference in New Issue
Block a user