This commit is contained in:
2026-01-15 12:04:50 +00:00
parent d1a8526d2d
commit 066d1ff2a4

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from dataclasses import dataclass
from urllib.parse import urlparse
from urllib.parse import quote_plus, urlparse
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -45,27 +45,38 @@ def _split_owner_repo(repo_url: str) -> tuple[str | None, str | None]:
def detect_provider(repo_url: str) -> str:
host = urlparse(repo_url).netloc.lower()
# Keep explicit providers stable
if "github.com" in host:
return "github"
if "gitlab.com" in host:
if "gitlab" in host: # covers gitlab.com and many self-hosted domains containing "gitlab"
return "gitlab"
# Heuristic: if it looks like a standard <host>/<owner>/<repo> URL, treat as gitea-like
owner, repo = _split_owner_repo(repo_url)
if owner and repo:
return "gitea"
return "generic"
async def _safe_json(session, url: str, *, headers: dict | None = None, timeout: int = 15):
async def _safe_json(session, url: str, *, headers: dict | None = None, timeout: int = 20):
try:
async with session.get(url, timeout=timeout, headers=headers) as resp:
if resp.status != 200:
return None, resp.status
return await resp.json(), resp.status
status = resp.status
if status != 200:
return None, status
return await resp.json(), status
except Exception:
return None, None
async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) -> tuple[str | None, str | None]:
"""
Robust strategy:
1) /releases/latest (can be 404 if no releases)
2) /releases?per_page=1 (some repos have releases but latest endpoint can still behave differently)
3) /tags?per_page=1
"""
session = async_get_clientsession(hass)
headers = {
"Accept": "application/vnd.github+json",
@@ -73,13 +84,24 @@ async def _github_latest_version(hass: HomeAssistant, owner: str, repo: str) ->
}
# 1) Releases latest
data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers)
data, status = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases/latest", headers=headers)
if isinstance(data, dict):
tag = data.get("tag_name") or data.get("name")
if isinstance(tag, str) and tag.strip():
return tag.strip(), "release"
# 2) Fallback: newest tag
# If rate-limited, log and continue with tags (tags may also be blocked but try)
if status == 403:
_LOGGER.debug("GitHub rate limit or forbidden on releases/latest for %s/%s", owner, repo)
# 2) Releases list fallback
data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/releases?per_page=1", headers=headers)
if isinstance(data, list) and data:
tag = data[0].get("tag_name") or data[0].get("name")
if isinstance(tag, str) and tag.strip():
return tag.strip(), "release"
# 3) Tag fallback
data, _ = await _safe_json(session, f"https://api.github.com/repos/{owner}/{repo}/tags?per_page=1", headers=headers)
if isinstance(data, list) and data:
tag = data[0].get("name")
@@ -107,6 +129,42 @@ async def _gitea_latest_version(hass: HomeAssistant, base: str, owner: str, repo
return None, None
async def _gitlab_latest_version(hass: HomeAssistant, base: str, owner: str, repo: str) -> tuple[str | None, str | None]:
"""
GitLab API:
- releases: /api/v4/projects/:id/releases
- tags: /api/v4/projects/:id/repository/tags
Project id can be URL-encoded full path (owner/repo).
"""
session = async_get_clientsession(hass)
headers = {"User-Agent": UA}
project = quote_plus(f"{owner}/{repo}")
# 1) Releases (newest first)
data, _ = await _safe_json(
session,
f"{base}/api/v4/projects/{project}/releases?per_page=1&order_by=released_at&sort=desc",
headers=headers,
)
if isinstance(data, list) and data:
tag = data[0].get("tag_name") or data[0].get("name")
if isinstance(tag, str) and tag.strip():
return tag.strip(), "release"
# 2) Tags (newest first)
data, _ = await _safe_json(
session,
f"{base}/api/v4/projects/{project}/repository/tags?per_page=1&order_by=updated&sort=desc",
headers=headers,
)
if isinstance(data, list) and data:
tag = data[0].get("name")
if isinstance(tag, str) and tag.strip():
return tag.strip(), "tag"
return None, None
async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo:
provider = detect_provider(repo_url)
owner, repo = _split_owner_repo(repo_url)
@@ -146,6 +204,27 @@ async def fetch_repo_info(hass: HomeAssistant, repo_url: str) -> RepoInfo:
info.latest_version_source = src
return info
if provider == "gitlab":
u = urlparse(repo_url.rstrip("/"))
base = f"{u.scheme}://{u.netloc}"
headers = {"User-Agent": UA}
project = quote_plus(f"{owner}/{repo}")
# Project info
data, _ = await _safe_json(session, f"{base}/api/v4/projects/{project}", headers=headers)
if isinstance(data, dict):
info.description = data.get("description")
info.repo_name = _normalize_repo_name(data.get("path")) or repo
info.default_branch = data.get("default_branch") or "main"
ns = data.get("namespace")
if isinstance(ns, dict) and ns.get("path"):
info.owner = ns.get("path")
ver, src = await _gitlab_latest_version(hass, base, owner, repo)
info.latest_version = ver
info.latest_version_source = src
return info
if provider == "gitea":
u = urlparse(repo_url.rstrip("/"))
base = f"{u.scheme}://{u.netloc}"