Serrebi Projects

Serrebi Radio Mastodon project catalog

All apps

nvda-addon-mirror

A self-updating mirror of NVDA add-on catalogs and direct author releases, served in the exact wire format NVDA's built-in Add-on Store consumes. Refreshes every 10 minutes via GitHub Actions.

Downloads

Latest release: v1.2.2, published 3 September 2026. Every file from that release is here.

Questions, bugs, or release news? Join the SerrebiProjects Telegram group, the fastest place to get help.

Readme

NVDA Add-on Update Mirror

A self-updating mirror of NVDA add-on catalogs, direct author releases, and explicitly pinned GitHub releases, published in the exact wire format NVDA’s built-in Add-on Store consumes. It refreshes hourly via GitHub Actions and is served from GitHub Pages.

Sources:

  • NV Access Add-on Store — the official catalog, including its upstream hashes and VirusTotal results.
  • bestmidi.com/addons/ — the GitHub-discovered “bleeding edge” list (addons.json).
  • nvda-addons.ru — the Russian community catalog (get.php?addonslist, the same JSON its own TiendaNVDA/Store add-ons use), which hosts many add-ons that never publish GitHub releases (synthesizers, voice packs, localized forks, etc.).
  • nvda.es with nvda-addons.org as failover — these two domains serve the same Spanish-community catalog byte for byte. The mirror monitors it for original add-on IDs and drops aliases or add-ons already covered by a stronger source.
  • Configured GitHub authors — release assets from the requested author list are accepted only when the filename ends in .nvda-addon and the downloaded ZIP has a valid root manifest.ini. Known add-on repositories are checked on every hourly run; a lightweight daily account scan discovers new repos. Original repositories are always eligible. A fork is eligible only when its numeric release version is strictly newer than its parent repository’s release; equal, older, missing, or incomparable fork versions are rejected. An owner may exclude every fork; serrebidev does this because those forks are contribution/PR branches. The mirror therefore uses the original keyang556/tdesktopnvda, not serrebidev/tdesktopnvda. Explicitly pinned variants can opt into fork_policy: include when they are intentionally different products published under separate manifest IDs. The four release-bearing Eloquence 64 variants are pinned this way where needed, and are published with unique IDs and display names so installing one cannot overwrite another.

What it does

  1. Fetches every catalog (using nvda-addons.org only if nvda.es fails).
  2. Rejects candidates that cannot be safely installed through NVDA’s store:
    • no download URL,
    • missing / template add-on id,
    • a version string with no parseable numeric parts.
  3. Merges sources case-insensitively by add-on id and channel. Explicitly pinned releases win, followed by direct author releases, the official store, nvda-addons.ru, bestmidi, and Spanish-catalog originals. A dev or beta entry whose version is the release the stable channel already carries is then dropped, so one add-on is listed once. nvda-addons.ru labels most of its links “Dev” regardless of what upstream published, which otherwise put the same version of ~370 add-ons in the store twice. Versions are compared as the numbers NVDA itself compares, so 2026.05.03 and 2026.5.3 count as one release.
  4. Downloads each remaining .nvda-addon, computes its SHA-256 (NVDA enforces this checksum on install), and emits the NVDA store schema.
  5. Writes cacheHash.json, addons.json, and {lang}/{channel}/{apiVersion}.json for every NVDA locale, channel, and a curated set of recent API versions.
  6. Publishes everything to GitHub Pages.

These add-ons are untested. bestmidi’s disclaimer: “These add-ons have not been tested and this is not an official NVDA add-on repository.” The SHA-256 hash still guarantees immutability of what you download, but nothing here is audited.

Using the mirror

The mirror serves metadata only — it does not re-host the .nvda-addon files. Each entry’s URL points at the original host (GitHub release or nvda.ru upload), and NVDA downloads directly from there. The build downloads uncached files to compute the SHA-256 checksum NVDA enforces on install, then reuses that result between daily checks. The helper does not crawl packages; NVDA downloads an add-on when the user installs or updates it.

Point NVDA’s Add-on Store at the live mirror:

https://serrebidev.github.io/nvda-addon-mirror

Three ways to do this:

  1. Install the helper add-on — latest build: dist/addonStoreMirror-1.2.2.nvda-addon (raw link: https://raw.githubusercontent.com/serrebidev/nvda-addon-mirror/main/dist/addonStoreMirror-1.2.2.nvda-addon). It sets [addonStore] baseServerURL to the mirror on startup and restores it when disabled — the same mechanism nvdacn/NVDAUpdateMirror uses. Version 1.2.2 marks NVDA 2026.2, the current stable release, as the last tested version. 1.2.1 corrected the minimum NVDA version to 2025.1 (see below) and stopped a failure there from leaving the Add-on Store list modified. 1.2.0 added source visibility and source-aware search; 1.1.1 fixed a crash caused by replacing NVDA’s live Add-on Store data manager; 1.0.0 had a trailing-slash URL bug.
  2. Edit nvda.ini manually:
    [addonStore]
    baseServerURL = https://serrebidev.github.io/nvda-addon-mirror
    then restart NVDA.
  3. Use NVDA’s built-in mirror setting — NVDA menu > Preferences > Settings > Add-on Store > Mirror server > Change…, paste https://serrebidev.github.io/nvda-addon-mirror, then OK. Available since NVDA 2025.1; it writes the same [addonStore] baseServerURL key as the two options above, so it works exactly as well as they do — it just won’t restore the official URL for you when you want to stop using the mirror, and it doesn’t add the helper add-on’s Source column / source-aware search.

Browsing what was rejected

The site publishes rejected.html — every candidate excluded while building the mirror, grouped by reason (voice/data packs skipped, no download URL, unparseable version, …), with an in-page filter. The same data is available as JSON at rejected.json.

Repo layout

  • mirror.py — the whole pipeline (stdlib only, Python 3.11+).
  • audit_translations.py — reports add-ons still published in a language other than English (see below).
  • .github/workflows/update.yml — hourly cron, with persistent build caches. GitHub may delay or drop scheduled events.
  • helper/ — source of the addonStoreMirror helper add-on; build_helper.py packs it into dist/.
  • public/ — generated site (published to GitHub Pages by Actions).

Keeping the English overlay complete

Many add-ons ship only Spanish, Russian, Turkish, French, Portuguese, German or Chinese metadata. translations.json overlays English summary, description, author and changelog text onto them, keyed by add-on id. The catalogs keep growing, so the overlay decays unless somebody notices the new arrivals.

audit_translations.py finds them. It reads the published addons.json rather than the overlay, because a non-English field that survives into the output is a real gap no matter what the overlay claims — a key whose spelling drifted from the add-on id looks present and does nothing:

python audit_translations.py                        # audit the live mirror
python audit_translations.py --addons public/addons.json --json gaps.json

It exits 0 when nothing needs translating and 1 when it found candidates, so a scheduled job can branch on it. Detection combines a Unicode-script test (reusing mirror‘s own), per-language function words, lowercase accented words, and — when langdetect happens to be installed — a statistical check on longer text. It is biased towards precision: a very short Latin-script product name carries too little signal to separate from English, so a few of those are missed, but every description long enough to read as prose is caught.

Running locally

python mirror.py --out public              # full build (all sources)
python mirror.py --sources ru --limit 6 --skip-download --locales en   # fast smoke test

The mirror writes real files rather than symlinks because GitHub Pages rejects artifacts that contain symlinks.

Notes and trade-offs

  • File layout: NVDA requests {base}/{lang}/{channel}/{apiVersion}.json, using the language/channel/apiVersion only as cache keys — the returned list is identical for all of them. The apiVersion is the running NVDA’s own add-on API version (e.g. 2026.2.0), so the mirror must emit a file for every released NVDA version still in use or those users get a 404 and an empty “compatible” list.
    • Old NVDA support has a hard floor of NVDA 2025.1. The Add-on Store client shipped earlier, in NVDA 2023.2, and 2023.2–2024.4 do fetch {lang}/{channel}/{version}.json — but from a hardcoded address: addonStore.network.BASE_URL = "https://nvaccess.org/addonStore", with no setting to change it. The [addonStore] baseServerURL key this mirror depends on was added in 2025.1, where _getBaseURL() first consults it. So no mirror of this kind can serve any NVDA older than 2025.1, and NVDA 2018–2022 has no Add-on Store at all. The mirror publishes no endpoint below 2025.1.0: files for older API versions could never be requested by any NVDA ever released, and they were about a third of the deployed site.
    • GitHub Pages forbids symlinks in Actions artifacts (and dereferences them on upload anyway), so the mirror writes real copies for every locale. Every build reads NV Access’s live addon-datastore metadata and publishes every API version from NVDA 2025.1 onward, including experimental versions. Endpoints at or above that floor are never pruned when a newer version appears. The bundled nvdaAPIVersions.json is the offline fallback, and also supplies each version’s BACK_COMPAT_TO. Users on a version whose file is absent still get the latest (incompatible) view.
  • Version sanitization: many non-GitHub add-ons use versions NVDA’s MajorMinorPatch can’t natively hold (4.1.1009.12, 2023.12.10.06.44.50, v20, 1.0-beta). The mirror keeps the first up-to-three integer runs and pads with 0; addonVersionName keeps the original string for display.
  • Bandwidth + hashes: the combined catalogs are large, so the first run downloads each unhashed package once. hashcache.json stores the SHA-256, version, size, HTTP validators, and the next permitted check time. Unchanged versions make no package requests for 24 hours, then use a conditional GET. HTTP 304 keeps the existing hash; a full response is hashed once. Hosts without validators may require one download per day. New catalog versions bypass the daily interval; same-version replacements are detected at the next daily check. Failed downloads wait six hours before retrying. Existing legacy hashes seed the daily interval without a bulk download during migration. --no-head-check explicitly forces downloads and bypasses these protections. GitHub Actions saves build caches even after build/publication failures. On a cache miss, it requires a valid cache from the last deployment before building. Pinned bundles are cached by release asset identity and update time, so repackaging also avoids downloading unchanged assets on each run. githubOwnerCache.json stores validated manifests, repository discovery, and conditional GitHub release ETags so unchanged hourly checks normally use quota-free HTTP 304 responses. A repository that still hits the API rate limit reuses its last verified release state until the next run re-checks it; only a rate-limited repository with no verified state yet blocks publication.
  • No vetting: neither source is audited. bestmidi’s disclaimer applies (“not tested, not an official repository”); nvda-addons.ru carries the same caveat. The SHA-256 hash guarantees immutability of what is downloaded, not safety.