GitHub Releases update check — minimal, non-blocking, headless-friendly.
Hits https://api.github.com/repos/<owner>/<repo>/releases/latest in a
daemon thread and emits :attr:UpdateChecker.update_available with the new
version label and the release page URL. The main window uses the signal to
surface a dismissable banner.
The check is gated by two things:
SETTING_AUTO_UPDATE_CHECK — user-facing toggle (Settings → General).
SETTING_LAST_UPDATE_CHECK — ISO timestamp; we skip if the previous
check ran within the last 24 hours so every launch doesn't pound the API.
If UPDATE_REPO_OWNER or UPDATE_REPO_NAME is empty the checker never
runs — lets the repo ship without a GitHub coordinate configured.
UpdateChecker
Bases: QObject
Runs the update check in a daemon thread; emits a Qt signal on hit.
check_async
check_async(current_version)
Starts the background check if gating conditions allow it.
Safe to call on every app launch — internal gating handles the
setting toggle, throttling, and missing-repo cases.
ソースコード位置: src/utils/update_checker.py
| def check_async(self, current_version: str) -> None:
"""Starts the background check if gating conditions allow it.
Safe to call on every app launch — internal gating handles the
setting toggle, throttling, and missing-repo cases.
"""
if not _should_check_now():
return
thread = threading.Thread(
target=self._run,
args=(current_version,),
daemon=True,
name="update-check",
)
thread.start()
|
_run
Worker body — fetches and emits on the GUI thread via signal.
ソースコード位置: src/utils/update_checker.py
| def _run(self, current_version: str) -> None:
"""Worker body — fetches and emits on the GUI thread via signal."""
result = _fetch_latest_release(UPDATE_REPO_OWNER, UPDATE_REPO_NAME)
_mark_checked()
if result is None:
return
tag, url = result
if _is_newer(tag, current_version):
self.update_available.emit(tag, url)
|
_parse_version
Parses a tag / version string into a comparable integer tuple.
Strips a leading v / V so v1.2.3 compares equal to 1.2.3.
Non-numeric trailing segments (-beta, +build) are dropped to
keep the comparison well-defined without pulling in packaging.
ソースコード位置: src/utils/update_checker.py
| def _parse_version(raw: str) -> tuple[int, ...]:
"""Parses a tag / version string into a comparable integer tuple.
Strips a leading ``v`` / ``V`` so ``v1.2.3`` compares equal to ``1.2.3``.
Non-numeric trailing segments (``-beta``, ``+build``) are dropped to
keep the comparison well-defined without pulling in ``packaging``.
"""
stripped = raw.strip().lstrip("vV")
# Cut at the first non-numeric-dot character.
core = ""
for ch in stripped:
if ch.isdigit() or ch == ".":
core += ch
else:
break
parts = [int(p) for p in core.split(".") if p.isdigit()]
return tuple(parts) if parts else (0,)
|
_is_newer
_is_newer(remote, current)
Returns True when the remote version tuple sorts above the current one.
ソースコード位置: src/utils/update_checker.py
| def _is_newer(remote: str, current: str) -> bool:
"""Returns True when the remote version tuple sorts above the current one."""
return _parse_version(remote) > _parse_version(current)
|
_fetch_latest_release
_fetch_latest_release(owner, repo)
Returns (tag_name, html_url) for the latest GitHub release, or None.
ソースコード位置: src/utils/update_checker.py
| def _fetch_latest_release(owner: str, repo: str) -> tuple[str, str] | None:
"""Returns ``(tag_name, html_url)`` for the latest GitHub release, or None."""
url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
req = Request(
url,
headers={
"Accept": "application/vnd.github+json",
"User-Agent": "ai-translate-update-check",
},
)
try:
with urlopen(req, timeout=_REQUEST_TIMEOUT) as response: # noqa: S310
payload = json.loads(response.read().decode("utf-8"))
except (URLError, TimeoutError, ValueError, OSError) as exc:
logger.debug("Update check failed: %s", exc)
return None
tag = payload.get("tag_name") or ""
html_url = payload.get("html_url") or ""
if not tag or not html_url:
return None
return tag, html_url
|
_should_check_now
Returns True when the setting is enabled and the throttle has elapsed.
ソースコード位置: src/utils/update_checker.py
| def _should_check_now() -> bool:
"""Returns True when the setting is enabled and the throttle has elapsed."""
if not UPDATE_REPO_OWNER or not UPDATE_REPO_NAME:
return False
if not load_setting(SETTING_AUTO_UPDATE_CHECK, True):
return False
last_raw = load_setting(SETTING_LAST_UPDATE_CHECK, "")
if not last_raw:
return True
try:
last = datetime.fromisoformat(str(last_raw))
except ValueError:
return True
delta = (datetime.now(tz=UTC) - last).total_seconds()
return delta >= _THROTTLE_SECONDS
|
_mark_checked
Records the current time as the last update-check timestamp.
ソースコード位置: src/utils/update_checker.py
| def _mark_checked() -> None:
"""Records the current time as the last update-check timestamp."""
save_setting(
SETTING_LAST_UPDATE_CHECK,
datetime.now(tz=UTC).isoformat(),
)
|