# How the Automatic Update Mechanism Works in biliTickerBuy's app_update.py

> Explore how biliTickerBuy's app_update.py automatically updates your app. Learn about GitHub API queries, release filtering, and structured release info for seamless UI updates.

- Repository: [Qizhuo Xie/biliTickerBuy](https://github.com/mikumifa/biliTickerBuy)
- Tags: internals
- Published: 2026-06-23

---

**The automatic update mechanism queries the GitHub Releases API, filters releases by update channel and semantic version, and returns a structured `ReleaseInfo` object that the UI layer consumes to display available updates to users.**

The open-source ticketing application **biliTickerBuy** implements a self-contained update pipeline in the [`app_update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/app_update.py) module. This component automatically polls the project's GitHub releases to determine whether a newer stable or pre-release version is available, handling version normalization, channel filtering, and asset parsing without requiring external update servers.

## The Core Update Pipeline in app_update.py

The automatic update mechanism is designed as a stateless pipeline that transforms raw GitHub API responses into structured, UI-ready data. All core logic resides in [`app_update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/app_update.py), which operates independently of the presentation layer.

### Entry Point: The fetch_update Function

In [`app_update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/app_update.py), the **`fetch_update()`** function serves as the primary entry point. It accepts two parameters: the current application version string and an update channel identifier (typically `稳定版` for stable or `测试版` for pre-release). The function creates a `requests.Session` instance and performs a GET request to the GitHub API endpoint `https://api.github.com/repos/mikumifa/biliTickerBuy/releases`.

The returned JSON payload—a list of release objects—is immediately passed to the internal `select_update()` function for candidate evaluation. This design keeps network logic separate from selection logic.

### Version Normalization with normalize_version

Before semantic comparison, release tag names (e.g., "v1.2.3") are sanitized via **`normalize_version()`**. This helper strips any leading **"v"** or **"V"** prefix and instantiates a `packaging.version.Version` object. If the conversion fails due to malformed tag names, the function raises an `UpdateError`, ensuring that only valid semantic versions propagate through the pipeline.

### Channel-Based Selection Logic

The **`select_update()`** function validates the requested channel against the constants **`UPDATE_CHANNEL_STABLE`** and **`UPDATE_CHANNEL_PRERELEASE`**, which are defined in [`util/Constant.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/util/Constant.py). The function iterates over the release list, applying three filters:

- **Draft exclusion**: Skips any release where `draft` is true.
- **Channel matching**: For stable channels, ignores releases where `prerelease` is true.
- **Version comparison**: Collects releases where the normalized version is strictly greater than the current version.

The newest candidate is selected using Python's built-in `max()` function with a lambda key: `max(candidates, key=lambda item: item[0])`. This ensures the highest semantic version is returned regardless of release order.

### The ReleaseInfo Dataclass

Once a valid update candidate is identified, it is encapsulated in a **`ReleaseInfo`** dataclass. This structure exposes critical metadata fields:

- **`version`**: The normalized `Version` object.
- **`tag_name`**: The original GitHub tag string.
- **`html_url`**: Direct link to the release page.
- **`body`**: Release notes in HTML or Markdown.
- **`prerelease`**: Boolean flag indicating pre-release status.
- **`published_at`**: ISO 8601 timestamp of publication.
- **`assets`**: Tuple of dictionaries containing asset name, browser download URL, and size in bytes.

The **`ReleaseInfo.to_dict()`** method facilitates JSON serialization, allowing the Gradio UI to consume the data without additional parsing logic.

## Error Handling and Edge Cases

The update mechanism implements defensive error handling to prevent network or API anomalies from crashing the application. All `requests.RequestException` instances (timeouts, connection errors, HTTP 4xx/5xx) are caught and wrapped in **`UpdateError`** exceptions. Similarly, invalid channel strings or malformed version tags trigger `UpdateError` with descriptive messages.

This exception-based flow allows the calling code to distinguish between "no updates available" (indicated by a `None` return value) and "update check failed" (indicated by an exception).

## UI Integration in tab/update.py

The Gradio-based interface in **[`tab/update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/tab/update.py)** consumes this pipeline through the `_check_updates()` function. It retrieves the current installed version via **`get_app_version()`** from [`app_version.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/app_version.py), then invokes `fetch_update()` with the user-selected channel retrieved from the configuration database.

The UI handles three distinct states:

- **Update available**: Formats a status block using `_format_update()`, displaying the new version number, release notes, and a hyperlink to the GitHub release page.
- **Up to date**: Displays a confirmation banner when `fetch_update()` returns `None`.
- **Error state**: Catches `UpdateError` in lines 100-110 of the source to render a red error banner, informing the user of connectivity issues.

The update mechanism itself remains agnostic to the runtime mode (bundled executable, source code, or pip installation), leaving environment-specific messaging to the UI layer.

## Practical Implementation Examples

### Example 1: Manual Update Check

```python
from app_update import fetch_update, UPDATE_CHANNEL_STABLE, UpdateError

current_version = "1.2.3"
try:
    release = fetch_update(current_version, UPDATE_CHANNEL_STABLE)
    if release:
        print(f"New version available: {release.version}")
        print(f"Release notes: {release.body}")
        for asset in release.assets:
            print(f"- {asset['name']} ({asset['size']} bytes): {asset['browser_download_url']}")
    else:
        print("You are already on the latest stable version.")
except UpdateError as exc:
    print(f"Update lookup failed: {exc}")

```

### Example 2: CLI Integration with Channel Selection

```python
import argparse
from app_update import fetch_update, UPDATE_CHANNEL_PRERELEASE, UpdateError
from app_version import get_app_version

parser = argparse.ArgumentParser()
parser.add_argument("--pre", action="store_true", help="Check pre-release channel")
args = parser.parse_args()

channel = UPDATE_CHANNEL_PRERELEASE if args.pre else UPDATE_CHANNEL_STABLE
try:
    release = fetch_update(get_app_version(), channel)
    if release:
        print(f"Update found: {release.version} – {release.html_url}")
    else:
        print("No updates available.")
except UpdateError as e:
    print(f"Could not check for updates: {e}")

```

## Summary

- The **`fetch_update()`** function in [`app_update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/app_update.py) orchestrates the entire update check by querying the GitHub Releases API and returning either a `ReleaseInfo` object or `None`.
- **Version normalization** ensures semantic versioning compliance by stripping "v" prefixes and utilizing `packaging.version.Version` for reliable comparison.
- **Channel filtering** supports dual release tracks (stable and pre-release) via the `UPDATE_CHANNEL_STABLE` and `UPDATE_CHANNEL_PRERELEASE` constants defined in [`util/Constant.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/util/Constant.py).
- The **`ReleaseInfo`** dataclass provides a structured, JSON-serializable representation of release metadata, including downloadable asset descriptors.
- Errors are centralized through the **`UpdateError`** exception class, allowing the UI in [`tab/update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/tab/update.py) to gracefully handle network failures and API changes.
- The system is fully stateless, requiring only the current version string and channel identifier to operate without persistent local state.

## Frequently Asked Questions

### How does fetch_update determine if a release is newer?

The **`select_update()`** function converts both the current version and candidate tag names into `packaging.version.Version` objects. It collects all releases with versions strictly greater than the current version, then selects the maximum value using Python's `max()` built-in with a lambda key. This ensures the latest semantic version wins, even if GitHub returns releases out of chronological order.

### What happens if the GitHub API is unreachable?

Network failures, DNS errors, or HTTP non-success codes during the `requests.Session.get()` call raise a `requests.RequestException`, which is immediately caught and re-raised as an `UpdateError`. The calling code in [`tab/update.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/tab/update.py) catches this specific exception and renders an error banner, preventing the application from crashing while informing the user of the connectivity issue.

### Can I check for pre-release updates?

Yes. Pass the **`UPDATE_CHANNEL_PRERELEASE`** constant (typically the string `测试版`) to `fetch_update()`. When this channel is selected, `select_update()` includes GitHub releases where `prerelease: true` in the candidate pool. Passing **`UPDATE_CHANNEL_STABLE`** explicitly filters out these pre-releases.

### Where is the update channel configuration stored?

The channel preference is persisted in the application's configuration database and defined in **[`util/Constant.py`](https://github.com/mikumifa/biliTickerBuy/blob/main/util/Constant.py)** alongside other constants like `PACKAGE_NAME`. The UI retrieves this value before invoking `fetch_update()`, making the core update mechanism agnostic to storage implementation details.