kk
This commit is contained in:
@@ -568,6 +568,18 @@ class AllDebrid(TableProviderMixin, Provider):
|
|||||||
URL = ("magnet:",)
|
URL = ("magnet:",)
|
||||||
URL_DOMAINS = ()
|
URL_DOMAINS = ()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "api_key",
|
||||||
|
"label": "API Key",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"secret": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _resolve_magnet_spec_from_result(result: Any) -> Optional[str]:
|
def _resolve_magnet_spec_from_result(result: Any) -> Optional[str]:
|
||||||
table = getattr(result, "table", None)
|
table = getattr(result, "table", None)
|
||||||
|
|||||||
@@ -50,6 +50,33 @@ def _extract_key(payload: Any) -> Optional[str]:
|
|||||||
|
|
||||||
class FileIO(Provider):
|
class FileIO(Provider):
|
||||||
"""File provider for file.io."""
|
"""File provider for file.io."""
|
||||||
|
PROVIDER_NAME = "file.io"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "api_key",
|
||||||
|
"label": "API Key",
|
||||||
|
"default": "",
|
||||||
|
"secret": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "expires",
|
||||||
|
"label": "Default Expiration (e.g. 1w)",
|
||||||
|
"default": "1w"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "maxDownloads",
|
||||||
|
"label": "Max Downloads",
|
||||||
|
"default": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoDelete",
|
||||||
|
"label": "Auto Delete",
|
||||||
|
"default": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|||||||
@@ -464,16 +464,36 @@ class InternetArchive(Provider):
|
|||||||
- search-file -provider internetarchive <query>
|
- search-file -provider internetarchive <query>
|
||||||
- download-file / provider.download() from search results
|
- download-file / provider.download() from search results
|
||||||
- add-file -provider internetarchive (uploads)
|
- add-file -provider internetarchive (uploads)
|
||||||
|
|
||||||
Config (optional):
|
|
||||||
[provider=internetarchive]
|
|
||||||
access_key="..." # optional (upload)
|
|
||||||
secret_key="..." # optional (upload)
|
|
||||||
collection="..." # optional (upload)
|
|
||||||
mediatype="..." # optional (upload)
|
|
||||||
"""
|
"""
|
||||||
URL = ("archive.org",)
|
URL = ("archive.org",)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "access_key",
|
||||||
|
"label": "Access Key (for uploads)",
|
||||||
|
"default": "",
|
||||||
|
"secret": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "secret_key",
|
||||||
|
"label": "Secret Key (for uploads)",
|
||||||
|
"default": "",
|
||||||
|
"secret": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "collection",
|
||||||
|
"label": "Default Collection",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "mediatype",
|
||||||
|
"label": "Default Mediatype",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
TABLE_AUTO_STAGES = {
|
TABLE_AUTO_STAGES = {
|
||||||
"internetarchive": ["download-file"],
|
"internetarchive": ["download-file"],
|
||||||
"internetarchive.folder": ["download-file"],
|
"internetarchive.folder": ["download-file"],
|
||||||
|
|||||||
@@ -234,6 +234,29 @@ class Matrix(TableProviderMixin, Provider):
|
|||||||
4. Selection triggers upload of pending files to selected rooms
|
4. Selection triggers upload of pending files to selected rooms
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "homeserver",
|
||||||
|
"label": "Homeserver URL",
|
||||||
|
"default": "https://matrix.org",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "access_token",
|
||||||
|
"label": "Access Token",
|
||||||
|
"default": "",
|
||||||
|
"secret": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"label": "Password (fallback)",
|
||||||
|
"default": "",
|
||||||
|
"secret": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
self._init_ok: Optional[bool] = None
|
self._init_ok: Optional[bool] = None
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ from Provider.metadata_provider import (
|
|||||||
from SYS.utils import unique_path
|
from SYS.utils import unique_path
|
||||||
|
|
||||||
_ARCHIVE_VERIFY_VALUE = get_requests_verify_value()
|
_ARCHIVE_VERIFY_VALUE = get_requests_verify_value()
|
||||||
|
_DEFAULT_ARCHIVE_SCALE = 4
|
||||||
|
_QUALITY_TO_ARCHIVE_SCALE = {
|
||||||
|
"high": 2,
|
||||||
|
"medium": 5,
|
||||||
|
"low": 8,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _create_archive_session() -> requests.Session:
|
def _create_archive_session() -> requests.Session:
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
@@ -279,17 +286,30 @@ class OpenLibrary(Provider):
|
|||||||
"openlibrary": ["download-file"],
|
"openlibrary": ["download-file"],
|
||||||
}
|
}
|
||||||
|
|
||||||
REQUIRED_CONFIG_KEYS = (
|
@classmethod
|
||||||
"email",
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
"password",
|
return [
|
||||||
)
|
{
|
||||||
|
"key": "email",
|
||||||
DEFAULT_ARCHIVE_SCALE = 4
|
"label": "Archive.org Email",
|
||||||
QUALITY_TO_ARCHIVE_SCALE = {
|
"default": "",
|
||||||
"high": 2,
|
"required": True
|
||||||
"medium": 5,
|
},
|
||||||
"low": 8,
|
{
|
||||||
|
"key": "password",
|
||||||
|
"label": "Archive.org Password",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"secret": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "quality",
|
||||||
|
"label": "Image Quality",
|
||||||
|
"default": "medium",
|
||||||
|
"placeholder": "high, medium, low"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
# Domains that should be routed to this provider when the user supplies a URL.
|
# Domains that should be routed to this provider when the user supplies a URL.
|
||||||
# (Used by ProviderCore.registry.match_provider_name_for_url)
|
# (Used by ProviderCore.registry.match_provider_name_for_url)
|
||||||
URL_DOMAINS = (
|
URL_DOMAINS = (
|
||||||
@@ -342,88 +362,53 @@ class OpenLibrary(Provider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _credential_archive(config: Dict[str,
|
def _credential_archive(config: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
|
||||||
Any]) -> Tuple[Optional[str],
|
"""Get Archive.org email/password from config."""
|
||||||
Optional[str]]:
|
|
||||||
"""Get Archive.org email/password from config.
|
|
||||||
|
|
||||||
Supports:
|
|
||||||
- New: {"provider": {"openlibrary": {"email": "...", "password": "..."}}}
|
|
||||||
- Old: {"Archive": {"email": "...", "password": "..."}}
|
|
||||||
{"archive_org_email": "...", "archive_org_password": "..."}
|
|
||||||
"""
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
provider_config = config.get("provider",
|
entry = config.get("provider", {}).get("openlibrary", {})
|
||||||
{})
|
if isinstance(entry, dict):
|
||||||
if isinstance(provider_config, dict):
|
email = entry.get("email")
|
||||||
openlibrary_config = provider_config.get("openlibrary",
|
password = entry.get("password")
|
||||||
{})
|
|
||||||
if isinstance(openlibrary_config, dict):
|
|
||||||
email = openlibrary_config.get("email")
|
|
||||||
password = openlibrary_config.get("password")
|
|
||||||
if email or password:
|
if email or password:
|
||||||
return str(email) if email is not None else None, (
|
return str(email) if email is not None else None, (
|
||||||
str(password) if password is not None else None
|
str(password) if password is not None else None
|
||||||
)
|
)
|
||||||
|
|
||||||
archive_config = config.get("Archive")
|
return None, None
|
||||||
if isinstance(archive_config, dict):
|
|
||||||
email = archive_config.get("email")
|
|
||||||
password = archive_config.get("password")
|
|
||||||
if email or password:
|
|
||||||
return str(email) if email is not None else None, (
|
|
||||||
str(password) if password is not None else None
|
|
||||||
)
|
|
||||||
|
|
||||||
email = config.get("archive_org_email")
|
|
||||||
password = config.get("archive_org_password")
|
|
||||||
return str(email) if email is not None else None, (
|
|
||||||
str(password) if password is not None else None
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _archive_scale_from_config(cls, config: Dict[str, Any]) -> int:
|
def _archive_scale_from_config(cls, config: Dict[str, Any]) -> int:
|
||||||
"""Resolve Archive.org book-reader scale from provider config.
|
"""Resolve Archive.org book-reader scale from provider config."""
|
||||||
|
|
||||||
Config:
|
|
||||||
[provider=OpenLibrary]
|
|
||||||
quality="medium" # High=2, Medium=5, Low=8
|
|
||||||
|
|
||||||
Default when missing/invalid: 4.
|
|
||||||
"""
|
|
||||||
|
|
||||||
default_scale = int(getattr(cls, "DEFAULT_ARCHIVE_SCALE", 4) or 4)
|
|
||||||
if not isinstance(config, dict):
|
if not isinstance(config, dict):
|
||||||
return default_scale
|
return _DEFAULT_ARCHIVE_SCALE
|
||||||
|
|
||||||
provider_config = config.get("provider", {})
|
entry = config.get("provider", {}).get("openlibrary", {})
|
||||||
openlibrary_config = None
|
if not isinstance(entry, dict):
|
||||||
if isinstance(provider_config, dict):
|
return _DEFAULT_ARCHIVE_SCALE
|
||||||
openlibrary_config = provider_config.get("openlibrary")
|
|
||||||
if not isinstance(openlibrary_config, dict):
|
|
||||||
openlibrary_config = {}
|
|
||||||
|
|
||||||
raw_quality = openlibrary_config.get("quality")
|
raw_quality = entry.get("quality")
|
||||||
if raw_quality is None:
|
if raw_quality is None:
|
||||||
return default_scale
|
return _DEFAULT_ARCHIVE_SCALE
|
||||||
|
|
||||||
if isinstance(raw_quality, (int, float)):
|
if isinstance(raw_quality, (int, float)):
|
||||||
try:
|
|
||||||
val = int(raw_quality)
|
val = int(raw_quality)
|
||||||
except Exception:
|
return val if val > 0 else _DEFAULT_ARCHIVE_SCALE
|
||||||
return default_scale
|
|
||||||
return val if val > 0 else default_scale
|
q = str(raw_quality).strip().lower()
|
||||||
|
if not q:
|
||||||
|
return _DEFAULT_ARCHIVE_SCALE
|
||||||
|
|
||||||
|
mapped = _QUALITY_TO_ARCHIVE_SCALE.get(q)
|
||||||
|
if isinstance(mapped, int) and mapped > 0:
|
||||||
|
return mapped
|
||||||
|
|
||||||
try:
|
try:
|
||||||
q = str(raw_quality).strip().lower()
|
val = int(q)
|
||||||
|
return val if val > 0 else _DEFAULT_ARCHIVE_SCALE
|
||||||
except Exception:
|
except Exception:
|
||||||
return default_scale
|
return _DEFAULT_ARCHIVE_SCALE
|
||||||
if not q:
|
|
||||||
return default_scale
|
|
||||||
|
|
||||||
mapped = cls.QUALITY_TO_ARCHIVE_SCALE.get(q)
|
|
||||||
if isinstance(mapped, int) and mapped > 0:
|
if isinstance(mapped, int) and mapped > 0:
|
||||||
return mapped
|
return mapped
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,24 @@ class Soulseek(Provider):
|
|||||||
}
|
}
|
||||||
"""Search provider for Soulseek P2P network."""
|
"""Search provider for Soulseek P2P network."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "username",
|
||||||
|
"label": "Soulseek Username",
|
||||||
|
"default": "",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"label": "Soulseek Password",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"secret": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
MUSIC_EXTENSIONS = {
|
MUSIC_EXTENSIONS = {
|
||||||
".flac",
|
".flac",
|
||||||
".mp3",
|
".mp3",
|
||||||
|
|||||||
@@ -149,6 +149,30 @@ class Telegram(Provider):
|
|||||||
"""
|
"""
|
||||||
URL = ("t.me", "telegram.me")
|
URL = ("t.me", "telegram.me")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "app_id",
|
||||||
|
"label": "API ID (from my.telegram.org)",
|
||||||
|
"default": "",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "api_hash",
|
||||||
|
"label": "API Hash",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"secret": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "bot_token",
|
||||||
|
"label": "Bot Token (optional)",
|
||||||
|
"default": "",
|
||||||
|
"secret": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
telegram_conf = (
|
telegram_conf = (
|
||||||
|
|||||||
@@ -142,6 +142,21 @@ class Provider(ABC):
|
|||||||
self.config = config or {}
|
self.config = config or {}
|
||||||
self.name = self.__class__.__name__.lower()
|
self.name = self.__class__.__name__.lower()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
"""Return configuration schema for this provider.
|
||||||
|
|
||||||
|
Returns a list of dicts, each defining a field:
|
||||||
|
{
|
||||||
|
"key": "api_key",
|
||||||
|
"label": "API Key",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"secret": True
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def required_config_keys(cls) -> List[str]:
|
def required_config_keys(cls) -> List[str]:
|
||||||
keys = getattr(cls, "REQUIRED_CONFIG_KEYS", None)
|
keys = getattr(cls, "REQUIRED_CONFIG_KEYS", None)
|
||||||
|
|||||||
@@ -42,10 +42,22 @@ class Folder(Store):
|
|||||||
Dict[str,
|
Dict[str,
|
||||||
int]]] = {}
|
int]]] = {}
|
||||||
|
|
||||||
def __new__(cls, *args: Any, **kwargs: Any) -> "Folder":
|
@classmethod
|
||||||
return super().__new__(cls)
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
setattr(__new__, "keys", ("NAME", "PATH"))
|
{
|
||||||
|
"key": "NAME",
|
||||||
|
"label": "Store Name",
|
||||||
|
"default": "",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "PATH",
|
||||||
|
"label": "Folder Path",
|
||||||
|
"default": "",
|
||||||
|
"required": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -29,6 +29,32 @@ class HydrusNetwork(Store):
|
|||||||
Maintains its own HydrusClient.
|
Maintains its own HydrusClient.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": "NAME",
|
||||||
|
"label": "Store Name",
|
||||||
|
"default": "",
|
||||||
|
"placeholder": "e.g. home_hydrus",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "URL",
|
||||||
|
"label": "Hydrus URL",
|
||||||
|
"default": "http://127.0.0.1:45869",
|
||||||
|
"placeholder": "http://127.0.0.1:45869",
|
||||||
|
"required": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "API",
|
||||||
|
"label": "API Key",
|
||||||
|
"default": "",
|
||||||
|
"required": True,
|
||||||
|
"secret": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def _log_prefix(self) -> str:
|
def _log_prefix(self) -> str:
|
||||||
store_name = getattr(self, "NAME", None) or "unknown"
|
store_name = getattr(self, "NAME", None) or "unknown"
|
||||||
return f"[hydrusnetwork:{store_name}]"
|
return f"[hydrusnetwork:{store_name}]"
|
||||||
@@ -46,8 +72,6 @@ class HydrusNetwork(Store):
|
|||||||
setattr(instance, "URL", str(url))
|
setattr(instance, "URL", str(url))
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
setattr(__new__, "keys", ("NAME", "API", "URL"))
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
instance_name: Optional[str] = None,
|
instance_name: Optional[str] = None,
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
class Store(ABC):
|
class Store(ABC):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
"""Return configuration schema for this store.
|
||||||
|
|
||||||
|
Returns a list of dicts:
|
||||||
|
{
|
||||||
|
"key": "PATH",
|
||||||
|
"label": "Store Location",
|
||||||
|
"default": "",
|
||||||
|
"required": True
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def add_file(self, file_path: Path, **kwargs: Any) -> str:
|
def add_file(self, file_path: Path, **kwargs: Any) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@@ -80,6 +80,23 @@ def _discover_store_classes() -> Dict[str, Type[BaseStore]]:
|
|||||||
|
|
||||||
|
|
||||||
def _required_keys_for(store_cls: Type[BaseStore]) -> list[str]:
|
def _required_keys_for(store_cls: Type[BaseStore]) -> list[str]:
|
||||||
|
# Support new config() schema
|
||||||
|
if hasattr(store_cls, "config") and callable(store_cls.config):
|
||||||
|
try:
|
||||||
|
schema = store_cls.config()
|
||||||
|
keys = []
|
||||||
|
if isinstance(schema, list):
|
||||||
|
for field in schema:
|
||||||
|
if isinstance(field, dict) and field.get("required"):
|
||||||
|
k = field.get("key")
|
||||||
|
if k:
|
||||||
|
keys.append(str(k))
|
||||||
|
if keys:
|
||||||
|
return keys
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Legacy __new__.keys support
|
||||||
keys = getattr(store_cls.__new__, "keys", None)
|
keys = getattr(store_cls.__new__, "keys", None)
|
||||||
if keys is None:
|
if keys is None:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -209,15 +209,41 @@ class ConfigModal(ModalScreen):
|
|||||||
item_type = str(self.editing_item_type or "")
|
item_type = str(self.editing_item_type or "")
|
||||||
item_name = str(self.editing_item_name or "")
|
item_name = str(self.editing_item_name or "")
|
||||||
|
|
||||||
|
provider_schema_map = {}
|
||||||
|
|
||||||
# Parse item_type for store-{stype} or just provider
|
# Parse item_type for store-{stype} or just provider
|
||||||
if item_type.startswith("store-"):
|
if item_type.startswith("store-"):
|
||||||
stype = item_type.replace("store-", "")
|
stype = item_type.replace("store-", "")
|
||||||
container.mount(Label(f"Editing Store: {item_name} ({stype})", classes="config-label"))
|
container.mount(Label(f"Editing Store: {item_name} ({stype})", classes="config-label"))
|
||||||
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
||||||
|
|
||||||
|
# Fetch Store schema
|
||||||
|
classes = _discover_store_classes()
|
||||||
|
if stype in classes:
|
||||||
|
cls = classes[stype]
|
||||||
|
if hasattr(cls, "config") and callable(cls.config):
|
||||||
|
for field_def in cls.config():
|
||||||
|
k = field_def.get("key")
|
||||||
|
if k:
|
||||||
|
provider_schema_map[k.upper()] = field_def
|
||||||
|
|
||||||
else:
|
else:
|
||||||
container.mount(Label(f"Editing {item_type.capitalize()}: {item_name}", classes="config-label"))
|
container.mount(Label(f"Editing {item_type.capitalize()}: {item_name}", classes="config-label"))
|
||||||
section = self.config_data.get(item_type, {}).get(item_name, {})
|
section = self.config_data.get(item_type, {}).get(item_name, {})
|
||||||
|
|
||||||
|
# Fetch Provider schema
|
||||||
|
if item_type == "provider":
|
||||||
|
from ProviderCore.registry import get_provider_class
|
||||||
|
try:
|
||||||
|
pcls = get_provider_class(item_name)
|
||||||
|
if pcls and hasattr(pcls, "config") and callable(pcls.config):
|
||||||
|
for field_def in pcls.config():
|
||||||
|
k = field_def.get("key")
|
||||||
|
if k:
|
||||||
|
provider_schema_map[k.upper()] = field_def
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Show all existing keys
|
# Show all existing keys
|
||||||
existing_keys_upper = set()
|
existing_keys_upper = set()
|
||||||
for k, v in section.items():
|
for k, v in section.items():
|
||||||
@@ -229,10 +255,43 @@ class ConfigModal(ModalScreen):
|
|||||||
continue
|
continue
|
||||||
existing_keys_upper.add(k_upper)
|
existing_keys_upper.add(k_upper)
|
||||||
|
|
||||||
container.mount(Label(k))
|
# Determine display props from schema
|
||||||
container.mount(Input(value=str(v), id=f"item-{k}", classes="config-input"))
|
label_text = k
|
||||||
|
is_secret = False
|
||||||
|
schema = provider_schema_map.get(k_upper)
|
||||||
|
if schema:
|
||||||
|
label_text = schema.get("label") or k
|
||||||
|
if schema.get("required"):
|
||||||
|
label_text += " *"
|
||||||
|
if schema.get("secret"):
|
||||||
|
is_secret = True
|
||||||
|
|
||||||
# If it's a store, we might have required keys
|
container.mount(Label(label_text))
|
||||||
|
inp = Input(value=str(v), id=f"item-{k}", classes="config-input")
|
||||||
|
if is_secret:
|
||||||
|
inp.password = True
|
||||||
|
container.mount(inp)
|
||||||
|
|
||||||
|
# Add required/optional fields from schema that are missing
|
||||||
|
for k_upper, field_def in provider_schema_map.items():
|
||||||
|
if k_upper not in existing_keys_upper:
|
||||||
|
existing_keys_upper.add(k_upper)
|
||||||
|
key = field_def["key"]
|
||||||
|
label_text = field_def.get("label") or key
|
||||||
|
if field_def.get("required"):
|
||||||
|
label_text += " *"
|
||||||
|
|
||||||
|
default_val = str(field_def.get("default") or "")
|
||||||
|
inp = Input(value=default_val, id=f"item-{key}", classes="config-input")
|
||||||
|
if field_def.get("secret"):
|
||||||
|
inp.password = True
|
||||||
|
if field_def.get("placeholder"):
|
||||||
|
inp.placeholder = field_def.get("placeholder")
|
||||||
|
|
||||||
|
container.mount(Label(label_text))
|
||||||
|
container.mount(inp)
|
||||||
|
|
||||||
|
# If it's a store, we might have required keys (legacy check fallback)
|
||||||
if item_type.startswith("store-"):
|
if item_type.startswith("store-"):
|
||||||
stype = item_type.replace("store-", "")
|
stype = item_type.replace("store-", "")
|
||||||
classes = _discover_store_classes()
|
classes = _discover_store_classes()
|
||||||
@@ -244,8 +303,9 @@ class ConfigModal(ModalScreen):
|
|||||||
container.mount(Label(rk))
|
container.mount(Label(rk))
|
||||||
container.mount(Input(value="", id=f"item-{rk}", classes="config-input"))
|
container.mount(Input(value="", id=f"item-{rk}", classes="config-input"))
|
||||||
|
|
||||||
# If it's a provider, we might have required keys
|
# If it's a provider, we might have required keys (legacy check fallback)
|
||||||
if item_type == "provider":
|
if item_type == "provider":
|
||||||
|
# 2. Legacy required_config_keys
|
||||||
from ProviderCore.registry import get_provider_class
|
from ProviderCore.registry import get_provider_class
|
||||||
try:
|
try:
|
||||||
pcls = get_provider_class(item_name)
|
pcls = get_provider_class(item_name)
|
||||||
@@ -307,10 +367,28 @@ class ConfigModal(ModalScreen):
|
|||||||
self.editing_item_type = "provider"
|
self.editing_item_type = "provider"
|
||||||
self.refresh_view()
|
self.refresh_view()
|
||||||
elif bid == "add-store-btn":
|
elif bid == "add-store-btn":
|
||||||
options = list(_discover_store_classes().keys())
|
all_classes = _discover_store_classes()
|
||||||
|
options = []
|
||||||
|
for stype, cls in all_classes.items():
|
||||||
|
if hasattr(cls, "config") and callable(cls.config):
|
||||||
|
try:
|
||||||
|
if cls.config():
|
||||||
|
options.append(stype)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.app.push_screen(SelectionModal("Select Store Type", options), callback=self.on_store_type_selected)
|
self.app.push_screen(SelectionModal("Select Store Type", options), callback=self.on_store_type_selected)
|
||||||
elif bid == "add-provider-btn":
|
elif bid == "add-provider-btn":
|
||||||
options = list(list_providers().keys())
|
provider_names = list(list_providers().keys())
|
||||||
|
options = []
|
||||||
|
from ProviderCore.registry import get_provider_class
|
||||||
|
for ptype in provider_names:
|
||||||
|
pcls = get_provider_class(ptype)
|
||||||
|
if pcls and hasattr(pcls, "config") and callable(pcls.config):
|
||||||
|
try:
|
||||||
|
if pcls.config():
|
||||||
|
options.append(ptype)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected)
|
self.app.push_screen(SelectionModal("Select Provider Type", options), callback=self.on_provider_type_selected)
|
||||||
elif bid.startswith("del-store-"):
|
elif bid.startswith("del-store-"):
|
||||||
parts = bid.replace("del-store-", "").split("-", 1)
|
parts = bid.replace("del-store-", "").split("-", 1)
|
||||||
@@ -338,7 +416,20 @@ class ConfigModal(ModalScreen):
|
|||||||
new_config = {"NAME": new_name}
|
new_config = {"NAME": new_name}
|
||||||
classes = _discover_store_classes()
|
classes = _discover_store_classes()
|
||||||
if stype in classes:
|
if stype in classes:
|
||||||
required = _required_keys_for(classes[stype])
|
cls = classes[stype]
|
||||||
|
# Use schema for defaults if present
|
||||||
|
if hasattr(cls, "config") and callable(cls.config):
|
||||||
|
for field_def in cls.config():
|
||||||
|
key = field_def.get("key")
|
||||||
|
if key:
|
||||||
|
val = field_def.get("default", "")
|
||||||
|
# Don't override NAME if we already set it to new_stype
|
||||||
|
if key.upper() == "NAME":
|
||||||
|
continue
|
||||||
|
new_config[key] = val
|
||||||
|
else:
|
||||||
|
# Fallback to required keys list
|
||||||
|
required = _required_keys_for(cls)
|
||||||
for rk in required:
|
for rk in required:
|
||||||
if rk.upper() != "NAME":
|
if rk.upper() != "NAME":
|
||||||
new_config[rk] = ""
|
new_config[rk] = ""
|
||||||
@@ -355,12 +446,19 @@ class ConfigModal(ModalScreen):
|
|||||||
|
|
||||||
# For providers, they are usually top-level entries in 'provider' dict
|
# For providers, they are usually top-level entries in 'provider' dict
|
||||||
if ptype not in self.config_data["provider"]:
|
if ptype not in self.config_data["provider"]:
|
||||||
# Get required keys if possible
|
|
||||||
from ProviderCore.registry import get_provider_class
|
from ProviderCore.registry import get_provider_class
|
||||||
try:
|
try:
|
||||||
pcls = get_provider_class(ptype)
|
pcls = get_provider_class(ptype)
|
||||||
new_config = {}
|
new_config = {}
|
||||||
if pcls:
|
if pcls:
|
||||||
|
# Use schema for defaults
|
||||||
|
if hasattr(pcls, "config") and callable(pcls.config):
|
||||||
|
for field_def in pcls.config():
|
||||||
|
key = field_def.get("key")
|
||||||
|
if key:
|
||||||
|
new_config[key] = field_def.get("default", "")
|
||||||
|
else:
|
||||||
|
# Fallback to legacy required keys
|
||||||
required = pcls.required_config_keys()
|
required = pcls.required_config_keys()
|
||||||
for rk in required:
|
for rk in required:
|
||||||
new_config[rk] = ""
|
new_config[rk] = ""
|
||||||
@@ -420,14 +518,25 @@ class ConfigModal(ModalScreen):
|
|||||||
stype = item_type.replace("store-", "")
|
stype = item_type.replace("store-", "")
|
||||||
classes = _discover_store_classes()
|
classes = _discover_store_classes()
|
||||||
if stype in classes:
|
if stype in classes:
|
||||||
required_keys = _required_keys_for(classes[stype])
|
required_keys = list(_required_keys_for(classes[stype]))
|
||||||
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
||||||
elif item_type == "provider":
|
elif item_type == "provider":
|
||||||
from ProviderCore.registry import get_provider_class
|
from ProviderCore.registry import get_provider_class
|
||||||
try:
|
try:
|
||||||
pcls = get_provider_class(item_name)
|
pcls = get_provider_class(item_name)
|
||||||
if pcls:
|
if pcls:
|
||||||
required_keys = pcls.required_config_keys()
|
# Collect required keys from schema
|
||||||
|
if hasattr(pcls, "config") and callable(pcls.config):
|
||||||
|
for field_def in pcls.config():
|
||||||
|
if field_def.get("required"):
|
||||||
|
k = field_def.get("key")
|
||||||
|
if k and k not in required_keys:
|
||||||
|
required_keys.append(k)
|
||||||
|
|
||||||
|
# Merge with legacy required keys
|
||||||
|
for rk in pcls.required_config_keys():
|
||||||
|
if rk not in required_keys:
|
||||||
|
required_keys.append(rk)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
section = self.config_data.get("provider", {}).get(item_name, {})
|
section = self.config_data.get("provider", {}).get(item_name, {})
|
||||||
|
|||||||
Reference in New Issue
Block a user