From c7ad449e4e80731e24482d18c88231cb0d7b5cfd Mon Sep 17 00:00:00 2001 From: maziggy Date: Sun, 19 Apr 2026 12:27:06 +0200 Subject: [PATCH] fix(firmware): parse P2S/X2D wiki anchors without dash and full-width parens (#1030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wiki scraper silently returned no versions for P2S and X2D, causing Bambuddy to fall back to the Bambu Lab download page, which still listed 01.01.01.00 as "latest" even though 01.02.00.00 shipped on 2026-04-09. Two regex mismatches in _fetch_all_versions_from_wiki(): 1. Heading anchor ids require an optional dash between version bytes and date. H2D/X1/H2C/H2S use "h-01020000-20260409"; P2S and X2D publish "h-0102000020260409" (no dash). 2. The text fallback only matched ASCII parens around release dates, but P2S, X2D, A1 and A1-mini render dates in full-width parens (YYYYMMDD) (U+FF08/U+FF09). Anchor regex now accepts an optional dash; fallback accepts both paren styles. Added regression tests for both shapes. --- CHANGELOG.md | 1 + backend/app/services/firmware_check.py | 21 ++++++--- backend/tests/unit/test_firmware_versions.py | 47 ++++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1e14f237..1a1dc541b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file. ## [0.2.4b1] - Unreleased ### Fixed +- **P2S Firmware Check Shows Stale "Latest" Version** ([#1030](https://github.com/maziggy/bambuddy/issues/1030)) — On P2S (and X2D) the Firmware Info modal reported `01.01.01.00` as the newest available release even though `01.02.00.00` had shipped on the Bambu Lab wiki weeks earlier, so the "update available" badge never appeared. Two silent regex mismatches in the wiki scraper caused `_fetch_all_versions_from_wiki()` to return an empty list: (1) the section-heading anchor parser required a dash between the version bytes and the release date (`id="h-01020000-20260409"`), but P2S and X2D publish anchors without the dash (`id="h-0102000020260409"`); (2) the text-based fallback only accepted ASCII parens around the date, while P2S, X2D, A1 and A1-mini headings render dates in full-width `(YYYYMMDD)` (U+FF08/U+FF09). When both paths failed, the code silently fell back to the Bambu Lab download page, which still lagged at `01.01.01.00`. The anchor regex now accepts an optional dash and the fallback accepts both paren styles; added regression tests for the no-dash anchor and full-width paren shapes. Thanks to @Minebuddy for reporting. - **Library File Print-Usage Tracking** ([#1008](https://github.com/maziggy/bambuddy/issues/1008)) — `LibraryFile.print_count` and `last_printed_at` are now updated on every successful queued print completion. Previously both fields were defined on the model and displayed in the File Manager, but nothing ever wrote to them — every file in every library showed as never printed. Now counts increment cumulatively and `last_printed_at` stamps the completion timestamp (UTC). Failed, cancelled and user-aborted prints are intentionally excluded, so the fields represent "successful usage" rather than "attempted usage." This unblocks sorting the File Manager by last-printed date and is a prerequisite for the scheduled-purge feature requested in #1008. Thanks to @cadtoolbox for the report. ### Improved diff --git a/backend/app/services/firmware_check.py b/backend/app/services/firmware_check.py index beef47af8..4ad78cd5e 100644 --- a/backend/app/services/firmware_check.py +++ b/backend/app/services/firmware_check.py @@ -161,8 +161,11 @@ class FirmwareCheckService: Fetch all firmware versions from the wiki release history page. Only extracts versions that appear in section-heading anchors - (e.g. `id="h-01030000-20260303"`) — this excludes version-like - numbers mentioned incidentally in release-note text. + (e.g. `id="h-01030000-20260303"` or `id="h-0102000020260409"`) — + this excludes version-like numbers mentioned incidentally in + release-note text. The dash separator between version and date is + optional: H2D/X1/H2C/H2S still use it, but P2S and X2D publish + anchors without the dash. Returns list of (version, release_date_YYYYMMDD | None) tuples, newest first. """ @@ -176,8 +179,9 @@ class FirmwareCheckService: if response.status_code != 200: return [] - # Primary: heading anchor ids like id="h-01030000-20260303" - anchor_matches = re.findall(r'id="h-(\d{2})(\d{2})(\d{2})(\d{2})-(\d{8})"', response.text) + # Primary: heading anchor ids like id="h-01030000-20260303" (dash) + # or id="h-0102000020260409" (no dash, P2S/X2D-style). + anchor_matches = re.findall(r'id="h-(\d{2})(\d{2})(\d{2})(\d{2})-?(\d{8})"', response.text) seen: set[str] = set() versions: list[tuple[str, str | None]] = [] for a, b, c, d, date in anchor_matches: @@ -190,8 +194,13 @@ class FirmwareCheckService: if versions: return versions - # Fallback: heading text with "XX.XX.XX.XX (YYYYMMDD)" - text_matches = re.findall(r"(\d{2}\.\d{2}\.\d{2}\.\d{2})\s*\((\d{8})\)", response.text) + # Fallback: heading text with "XX.XX.XX.XX (YYYYMMDD)" — + # accept both ASCII "()" and full-width "()" (U+FF08/U+FF09) + # which some pages (A1, A1-mini, P2S) use. + text_matches = re.findall( + r"(\d{2}\.\d{2}\.\d{2}\.\d{2})\s*[(\uff08](\d{8})[)\uff09]", + response.text, + ) for v, date in text_matches: if v in seen: continue diff --git a/backend/tests/unit/test_firmware_versions.py b/backend/tests/unit/test_firmware_versions.py index 31eecba10..bb0012581 100644 --- a/backend/tests/unit/test_firmware_versions.py +++ b/backend/tests/unit/test_firmware_versions.py @@ -50,6 +50,53 @@ async def test_wiki_extraction_returns_empty_for_unknown_api_key(): assert await svc._fetch_all_versions_from_wiki("no-such-key") == [] +# P2S and X2D wiki pages publish anchor ids without a dash between the +# version bytes and the date (e.g. h-0102000020260409). Regression for #1030 +# where the anchor regex required a dash and silently returned no versions, +# causing the UI to fall back to the stale download-page "Latest" value. +P2S_NODASH_ANCHOR_SAMPLE = """ +

01.02.00.00(20260409)

+

01.01.03.00(20260209)

+

01.01.01.00(20251208)

+""" + + +@pytest.mark.asyncio +async def test_wiki_extraction_accepts_nodash_anchors(): + """P2S/X2D anchors concatenate version+date with no dash — must still parse.""" + svc = FirmwareCheckService() + mock_resp = AsyncMock() + mock_resp.status_code = 200 + mock_resp.text = P2S_NODASH_ANCHOR_SAMPLE + with patch.object(svc._client, "get", AsyncMock(return_value=mock_resp)): + versions = await svc._fetch_all_versions_from_wiki("p2s") + + assert [v for v, _ in versions] == ["01.02.00.00", "01.01.03.00", "01.01.01.00"] + assert versions[0][1] == "20260409" + + +# A1, A1-mini and P2S pages render dates in full-width parens (YYYYMMDD) +# rather than ASCII parens (YYYYMMDD). Pages without version-anchors fall +# through to the text-based regex, so it must accept both paren styles. +FULLWIDTH_PAREN_FALLBACK_SAMPLE = """ +

01.04.00.01 (20260401)

+

01.03.00.00 (20260101)

+""" + + +@pytest.mark.asyncio +async def test_wiki_extraction_fallback_accepts_fullwidth_parens(): + svc = FirmwareCheckService() + mock_resp = AsyncMock() + mock_resp.status_code = 200 + mock_resp.text = FULLWIDTH_PAREN_FALLBACK_SAMPLE + with patch.object(svc._client, "get", AsyncMock(return_value=mock_resp)): + versions = await svc._fetch_all_versions_from_wiki("a1") + + assert [v for v, _ in versions] == ["01.04.00.01", "01.03.00.00"] + assert versions[0][1] == "20260401" + + @pytest.mark.asyncio async def test_get_available_versions_merges_sources(): """