diff --git a/docs/size-exemptions.md b/docs/size-exemptions.md
index 2f3f7db..ae37226 100644
--- a/docs/size-exemptions.md
+++ b/docs/size-exemptions.md
@@ -55,8 +55,8 @@ without a *signed* exemption" is unenforceable.
## Planned, NOT exempt (owned by split plans — listed so nothing falls between states)
core `static/app.js` (11,852) · `static/highway.js` (4,168, whole file) · `server.py`
-(7,275 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
-extractions and twelve `routers/` modules) ·
+(7,216 — was 14,037; ratcheted by the R3 `MetadataDB` + `AudioEffectsMappingDB`
+extractions and thirteen `routers/` modules) ·
`lib/metadata_db.py` (4,373 — new in R3; the `MetadataDB` class alone is 4,018 lines
and is a monolith in its own right, to be split per-table once the router train
lands) · `static/v3/songs.js` (4,134) · `static/capabilities/audio-session.js`
diff --git a/lib/routers/version.py b/lib/routers/version.py
new file mode 100644
index 0000000..45403de
--- /dev/null
+++ b/lib/routers/version.py
@@ -0,0 +1,81 @@
+"""App version + source/license URLs (/api/version).
+
+Extracted verbatim from ``server.py`` (R3) except the decorator (``@app`` ->
+``@router``) and the VERSION-file lookup: ``Path(__file__).parent`` (app root
+when this lived at the top level) -> ``Path(__file__).resolve().parents[2]``
+(routers -> lib -> app root). VERSION ships at the app root in every packaging
+path (Dockerfile COPY, desktop bundle).
+"""
+
+import os
+from pathlib import Path
+
+from fastapi import APIRouter
+
+router = APIRouter()
+
+
+def _safe_http_url(raw):
+ """Return `raw` stripped + trailing-slash-stripped if it parses as an
+ http(s) URL with a non-empty host; else None.
+
+ Used to validate operator-supplied `APP_SOURCE_URL` / `APP_LICENSE_URL`
+ env vars before they reach `` in the UI. A bare prefix check
+ like `startswith(("http://","https://"))` accepts malformed inputs
+ such as `"https://"` (no host) or `"https:///foo"` (empty host) that
+ still produce broken hrefs — and, when used as a base for the default
+ `license_url`, garbage like `"https:///blob/main/LICENSE"`.
+ """
+ from urllib.parse import urlsplit
+ if not raw:
+ return None
+ s = raw.strip().rstrip("/")
+ if not s:
+ return None
+ try:
+ parsed = urlsplit(s)
+ except ValueError:
+ return None
+ if parsed.scheme.lower() not in ("http", "https"):
+ return None
+ # `netloc` includes any `user:pass@` and `:port` — strings like
+ # "http://:80/path" have non-empty netloc (":80") but no real
+ # hostname. Validate `hostname` so only URLs with an actual host
+ # are accepted.
+ if not parsed.hostname:
+ return None
+ return s
+
+
+@router.get("/api/version")
+def get_version():
+ env_version = os.environ.get("APP_VERSION", "").strip()
+ if env_version:
+ version = env_version
+ else:
+ version_file = Path(__file__).resolve().parents[2] / "VERSION" # R3: app root from lib/routers/
+ version = "unknown"
+ if version_file.exists():
+ try:
+ version = version_file.read_text().strip()
+ except (OSError, UnicodeDecodeError):
+ pass
+ default_source_url = "https://github.com/got-feedback/feedBack"
+ # APP_SOURCE_URL / APP_LICENSE_URL flow straight into in the UI,
+ # so validate with urllib.parse rather than a bare prefix check — a prefix
+ # check accepts malformed values like "https://" (no host) which produce
+ # broken hrefs (and a constructed license_url like "https:///blob/main/LICENSE").
+ # _safe_http_url requires scheme in {http,https} AND a non-empty hostname
+ # (not just netloc — that would still accept port-only authorities like
+ # "http://:80/path"); fall back to the safe default otherwise.
+ source_url = _safe_http_url(os.environ.get("APP_SOURCE_URL")) or default_source_url
+ # APP_LICENSE_URL: explicit override for the LICENSE link. The default
+ # constructed value (source_url + "/blob/main/LICENSE") is GitHub-
+ # specific and assumes the repo's default branch is `main`; non-GitHub
+ # hosts (GitLab, Gitea, self-hosted) need an explicit value.
+ license_url = _safe_http_url(os.environ.get("APP_LICENSE_URL")) or (source_url + "/blob/main/LICENSE")
+ return {
+ "version": version,
+ "source_url": source_url,
+ "license_url": license_url,
+ }
diff --git a/server.py b/server.py
index a65e72b..01bd11e 100644
--- a/server.py
+++ b/server.py
@@ -55,7 +55,7 @@ from dlc_paths import _get_dlc_dir, _resolve_dlc_path
# Lives in lib/ because that is the one core dir every packaging path copies.
import appstate
# Extracted route modules. They import `appstate`, never `server` — one-way graph.
-from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras, shop, progression, profile, stats
+from routers import audio_effects, artist_aliases, loops, playlists, ws_highway, chart, wanted, library_extras, shop, progression, profile, stats, version
import sloppak as sloppak_mod
import loosefolder as loosefolder_mod
# Pure text-matching engine for MusicBrainz enrichment (P8): denoise/score/
@@ -3193,70 +3193,11 @@ def _periodic_rescan():
time.sleep(300)
-def _safe_http_url(raw):
- """Return `raw` stripped + trailing-slash-stripped if it parses as an
- http(s) URL with a non-empty host; else None.
-
- Used to validate operator-supplied `APP_SOURCE_URL` / `APP_LICENSE_URL`
- env vars before they reach `` in the UI. A bare prefix check
- like `startswith(("http://","https://"))` accepts malformed inputs
- such as `"https://"` (no host) or `"https:///foo"` (empty host) that
- still produce broken hrefs — and, when used as a base for the default
- `license_url`, garbage like `"https:///blob/main/LICENSE"`.
- """
- from urllib.parse import urlsplit
- if not raw:
- return None
- s = raw.strip().rstrip("/")
- if not s:
- return None
- try:
- parsed = urlsplit(s)
- except ValueError:
- return None
- if parsed.scheme.lower() not in ("http", "https"):
- return None
- # `netloc` includes any `user:pass@` and `:port` — strings like
- # "http://:80/path" have non-empty netloc (":80") but no real
- # hostname. Validate `hostname` so only URLs with an actual host
- # are accepted.
- if not parsed.hostname:
- return None
- return s
-@app.get("/api/version")
-def get_version():
- env_version = os.environ.get("APP_VERSION", "").strip()
- if env_version:
- version = env_version
- else:
- version_file = Path(__file__).parent / "VERSION"
- version = "unknown"
- if version_file.exists():
- try:
- version = version_file.read_text().strip()
- except (OSError, UnicodeDecodeError):
- pass
- default_source_url = "https://github.com/got-feedback/feedBack"
- # APP_SOURCE_URL / APP_LICENSE_URL flow straight into in the UI,
- # so validate with urllib.parse rather than a bare prefix check — a prefix
- # check accepts malformed values like "https://" (no host) which produce
- # broken hrefs (and a constructed license_url like "https:///blob/main/LICENSE").
- # _safe_http_url requires scheme in {http,https} AND a non-empty hostname
- # (not just netloc — that would still accept port-only authorities like
- # "http://:80/path"); fall back to the safe default otherwise.
- source_url = _safe_http_url(os.environ.get("APP_SOURCE_URL")) or default_source_url
- # APP_LICENSE_URL: explicit override for the LICENSE link. The default
- # constructed value (source_url + "/blob/main/LICENSE") is GitHub-
- # specific and assumes the repo's default branch is `main`; non-GitHub
- # hosts (GitLab, Gitea, self-hosted) need an explicit value.
- license_url = _safe_http_url(os.environ.get("APP_LICENSE_URL")) or (source_url + "/blob/main/LICENSE")
- return {
- "version": version,
- "source_url": source_url,
- "license_url": license_url,
- }
+# ── App version / source URLs ────────────────────────────────────────────────
+# Mounted here (registration order). Implementation in lib/routers/version.py.
+app.include_router(version.router)
@app.get("/api/scan-status")