Files
musicseerr/backend/repositories/github_repository.py
T
2026-04-16 00:51:24 +01:00

84 lines
2.6 KiB
Python

import logging
import httpx
import msgspec
from api.v1.schemas.version import GitHubRelease
from infrastructure.cache.cache_keys import GITHUB_RELEASES_PREFIX
from infrastructure.cache.memory_cache import CacheInterface
logger = logging.getLogger(__name__)
GITHUB_API_URL = "https://api.github.com/repos/HabiRabbu/Musicseerr/releases"
GITHUB_RELEASES_CACHE_KEY = f"{GITHUB_RELEASES_PREFIX}all"
GITHUB_RELEASES_CACHE_TTL = 3600
class _GitHubReleaseRaw(msgspec.Struct):
"""Raw GitHub API response struct for decoding."""
tag_name: str
published_at: str
html_url: str
name: str | None = None
body: str | None = None
prerelease: bool = False
draft: bool = False
class GitHubRepository:
def __init__(self, http_client: httpx.AsyncClient, cache: CacheInterface):
self._client = http_client
self._cache = cache
async def fetch_releases(self) -> list[GitHubRelease]:
"""Fetch all non-draft releases from GitHub, with 1hr server-side cache."""
cached = await self._cache.get(GITHUB_RELEASES_CACHE_KEY)
if cached is not None:
return cached
try:
response = await self._client.get(
GITHUB_API_URL,
headers={"Accept": "application/vnd.github+json"},
timeout=10.0,
)
if response.status_code != 200:
logger.warning("GitHub releases API returned %s", response.status_code)
return []
raw_releases = msgspec.json.decode(
response.content, type=list[_GitHubReleaseRaw]
)
releases = [
GitHubRelease(
tag_name=r.tag_name,
name=r.name or r.tag_name,
body=r.body or "",
published_at=r.published_at,
html_url=r.html_url,
prerelease=r.prerelease,
)
for r in raw_releases
if not r.draft
]
await self._cache.set(
GITHUB_RELEASES_CACHE_KEY,
releases,
ttl_seconds=GITHUB_RELEASES_CACHE_TTL,
)
return releases
except (httpx.HTTPError, msgspec.DecodeError) as e:
logger.error("Failed to fetch GitHub releases: %s", e)
return []
async def fetch_latest_release(self) -> GitHubRelease | None:
"""Get the latest non-prerelease release."""
releases = await self.fetch_releases()
for release in releases:
if not release.prerelease:
return release
return None