diff --git a/custom_components/bahmcloud_store/core.py b/custom_components/bahmcloud_store/core.py index d70a74c..a246bf8 100644 --- a/custom_components/bahmcloud_store/core.py +++ b/custom_components/bahmcloud_store/core.py @@ -14,7 +14,8 @@ from homeassistant.util import yaml as ha_yaml from .storage import BCSStorage, CustomRepo from .views import StaticAssetsView, BCSApiView from .custom_repo_view import BCSCustomRepoView -from .providers import fetch_repo_info, detect_provider +from .providers import fetch_repo_info, detect_provider, RepoInfo +from .metadata import fetch_repo_metadata, RepoMetadata _LOGGER = logging.getLogger(__name__) @@ -32,22 +33,28 @@ class BCSConfig: @dataclass class RepoItem: - """Merged repository item (from store.yaml + custom repos).""" - id: str name: str url: str source: str # "index" | "custom" - # Enrichment fields (best-effort) + # Provider enrichment owner: str | None = None - description: str | None = None provider: str | None = None + provider_repo_name: str | None = None + provider_description: str | None = None + default_branch: str | None = None + + # Metadata resolver + meta_source: str | None = None + meta_name: str | None = None + meta_description: str | None = None + meta_category: str | None = None + meta_author: str | None = None + meta_maintainer: str | None = None class BCSCore: - """Core services for BCS: indexing + storage + HTTP views.""" - def __init__(self, hass: HomeAssistant, config: BCSConfig) -> None: self.hass = hass self.config = config @@ -60,10 +67,6 @@ class BCSCore: self.version: str = self._read_manifest_version() def _read_manifest_version(self) -> str: - """ - Read the integration version from manifest.json to avoid version mismatches - between backend and frontend strings. - """ try: manifest_path = Path(__file__).resolve().parent / "manifest.json" data = json.loads(manifest_path.read_text(encoding="utf-8")) @@ -83,13 +86,11 @@ class BCSCore: pass async def register_http_views(self) -> None: - """Register static assets and API routes.""" self.hass.http.register_view(StaticAssetsView()) self.hass.http.register_view(BCSApiView(self)) self.hass.http.register_view(BCSCustomRepoView(self)) async def refresh(self) -> None: - """Refresh merged repo list (index + custom) and enrich metadata.""" index_repos, refresh_seconds = await self._load_index_repos() self.refresh_seconds = refresh_seconds @@ -97,55 +98,67 @@ class BCSCore: merged: dict[str, RepoItem] = {} - # Index repos for item in index_repos: merged[item.id] = item - # Custom repos override by id (or add) for c in custom_repos: merged[c.id] = RepoItem( id=c.id, - name=c.name or c.url, # Will be replaced by provider repo_name if name missing + name=(c.name or c.url), url=c.url, source="custom", ) - # Basic provider detection (cheap) for r in merged.values(): r.provider = detect_provider(r.url) - # Enrich owner/description/name (best-effort; never break the store) - await self._enrich_repos(merged) + await self._enrich_and_resolve(merged) self.repos = merged - async def _enrich_repos(self, merged: dict[str, RepoItem]) -> None: - """ - Enrich repositories using provider APIs (GitHub/Gitea). - This is intentionally best-effort and concurrency-limited. - """ + async def _enrich_and_resolve(self, merged: dict[str, RepoItem]) -> None: sem = asyncio.Semaphore(6) - async def enrich_one(r: RepoItem) -> None: + async def process_one(r: RepoItem) -> None: async with sem: - info = await fetch_repo_info(self.hass, r.url) - + # 1) Provider info (owner/description/default_branch/repo_name) + info: RepoInfo = await fetch_repo_info(self.hass, r.url) r.provider = info.provider or r.provider - if info.owner: - r.owner = info.owner - if info.description: - r.description = info.description + r.owner = info.owner or r.owner + r.provider_repo_name = info.repo_name + r.provider_description = info.description + r.default_branch = info.default_branch or r.default_branch - # If the custom repo had no name provided, use repo_name from provider - if r.source == "custom": - if (not r.name) or (r.name == r.url) or (r.name.startswith("http")): - if info.repo_name: - r.name = info.repo_name + # 2) Metadata resolver (bcs.yaml -> hacs.yaml -> hacs.json) + md: RepoMetadata = await fetch_repo_metadata(self.hass, r.url, r.default_branch) + r.meta_source = md.source + r.meta_name = md.name + r.meta_description = md.description + r.meta_category = md.category + r.meta_author = md.author + r.meta_maintainer = md.maintainer - await asyncio.gather(*(enrich_one(r) for r in merged.values()), return_exceptions=True) + # 3) Resolve final display name (priority) + # 1) metadata name + # 2) existing name (store.yaml or user-provided custom name) + # 3) provider repo name + # 4) url + has_user_or_index_name = bool(r.name) and (r.name != r.url) and (not str(r.name).startswith("http")) + if r.meta_name: + r.name = r.meta_name + elif not has_user_or_index_name and r.provider_repo_name: + # Only use provider repo name if no metadata AND no user/index name was given. + r.name = r.provider_repo_name + elif not r.name: + r.name = r.url + + # 4) Resolve description shown in UI: metadata first, then provider + # (The UI will also prefer meta_description; we just keep fields.) + return + + await asyncio.gather(*(process_one(r) for r in merged.values()), return_exceptions=True) async def _load_index_repos(self) -> tuple[list[RepoItem], int]: - """Load store.yaml and return (repos, refresh_seconds).""" session = async_get_clientsession(self.hass) try: async with session.get(self.config.store_url, timeout=20) as resp: @@ -188,8 +201,6 @@ class BCSCore: except Exception as e: raise BCSError(f"Invalid store.yaml: {e}") from e - # --- Custom repo management (used by API) --- - async def add_custom_repo(self, url: str, name: str | None) -> CustomRepo: repo = await self.storage.add_custom_repo(url=url, name=name) await self.refresh() @@ -205,7 +216,6 @@ class BCSCore: return await self.storage.list_custom_repos() def list_repos_public(self) -> list[dict[str, Any]]: - """Return repo list for UI.""" out: list[dict[str, Any]] = [] for r in self.repos.values(): out.append( @@ -216,7 +226,16 @@ class BCSCore: "source": r.source, "owner": r.owner, "provider": r.provider, - "description": r.description, + + "meta_source": r.meta_source, + "meta_name": r.meta_name, + "meta_description": r.meta_description, + "meta_category": r.meta_category, + "meta_author": r.meta_author, + "meta_maintainer": r.meta_maintainer, + + "provider_repo_name": r.provider_repo_name, + "provider_description": r.provider_description, } ) return out