From da8787e93a74b46d0db1ad8d3d520bcda1a7dd19 Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Tue, 14 Jul 2026 11:07:45 +0530 Subject: [PATCH 1/2] feat: add check-new-releases tools; fix msdls_v3.py; add missing 25H2 V2 China variants Moves msdls_v3.py from repo root into scripts/check-new-releases/, alongside a new Go tool, documented as Method 1 (Python brute-force range scan, can write results to a catalog-shaped JSON) and Method 2 (Go auto-discovery via page-scrape + bounded adjacent-ID probe, no range-guessing needed). Only Windows 11 is checked -- 8.1 is fully frozen and Windows 10 is past end-of-life, so neither will produce a new consumer ISO again. Fixed msdls_v3.py's release-name extraction: it checked EditionName/ ReleaseName/FriendlyName, none of which exist in the real API response -- the actual field is ProductDisplayName, confirmed live. Also added the vlscppe session-permit call to setup_session(), which was skipped entirely; higher-risk gap for this script's brute-force range-scan usage pattern than for a single lookup. Both tools' live runs surfaced a real catalog gap: product IDs 3322/3323/3325/3326 (Home/Pro China variants of the 25H2 "V2" refresh) exist on Microsoft's side but weren't in our catalog. Added them to cli/catalog.go, products.json, validContributeProducts, and sitemap.xml, and renamed 3321/3324 from our own guessed "(Updated Oct)" label to Microsoft's actual name for this refresh, "(V2)" (their internal name is "Windows 11 25H2__V2"; cleaned up the double-underscore for user-facing display). Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 +- backend/main.go | 3 +- cli/catalog.go | 8 +- frontend/public/data/products.json | 48 ++- frontend/public/sitemap.xml | 4 + scripts/check-new-releases/README.md | 35 ++ scripts/check-new-releases/go.mod | 3 + scripts/check-new-releases/main.go | 321 ++++++++++++++++++ .../check-new-releases/msdls_v3.py | 31 +- 9 files changed, 445 insertions(+), 12 deletions(-) create mode 100644 scripts/check-new-releases/README.md create mode 100644 scripts/check-new-releases/go.mod create mode 100644 scripts/check-new-releases/main.go rename msdls_v3.py => scripts/check-new-releases/msdls_v3.py (79%) diff --git a/README.md b/README.md index e88c0fc..69874f0 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,8 @@ Returns real-time cache statistics for the running instance. Auth via `?secret=` |---|---|---| | Windows 11 25H2 | 3262 | x64 | | Windows 11 25H2 | 3265 | ARM64 | -| Windows 11 25H2 (updated) | 3321 | x64 | -| Windows 11 25H2 (updated) | 3324 | ARM64 | +| Windows 11 25H2 (V2) | 3321 | x64 | +| Windows 11 25H2 (V2) | 3324 | ARM64 | | Windows 11 24H2 | 3113 | x64 | | Windows 11 24H2 | 3131 | ARM64 | | Windows 10 22H2 | 2618 | x64 / x86 | diff --git a/backend/main.go b/backend/main.go index d2ea1b4..c544f57 100644 --- a/backend/main.go +++ b/backend/main.go @@ -479,7 +479,8 @@ var validContributeProducts = map[string]bool{ "3131": true, "3132": true, "3133": true, "3262": true, "3263": true, "3264": true, "3265": true, "3266": true, "3267": true, - "3321": true, "3324": true, + "3321": true, "3322": true, "3323": true, + "3324": true, "3325": true, "3326": true, } // allowedCDNSuffixes is the CDN host allow-list for contributed download URLs. diff --git a/cli/catalog.go b/cli/catalog.go index ffa8188..5263ff1 100644 --- a/cli/catalog.go +++ b/cli/catalog.go @@ -29,8 +29,12 @@ var consumerProducts = []Product{ {"3265", "Windows 11 Arm64 25H2 (26200.6584)"}, {"3266", "Windows 11 Arm64 25H2 Home China (26200.6584)"}, {"3267", "Windows 11 Arm64 25H2 Pro China (26200.6584)"}, - {"3321", "Windows 11 25H2 (Updated Oct)"}, - {"3324", "Windows 11 Arm64 25H2 (Updated Oct)"}, + {"3321", "Windows 11 25H2 (V2)"}, + {"3322", "Windows 11 25H2 Home China (V2)"}, + {"3323", "Windows 11 25H2 Pro China (V2)"}, + {"3324", "Windows 11 Arm64 25H2 (V2)"}, + {"3325", "Windows 11 Arm64 25H2 Home China (V2)"}, + {"3326", "Windows 11 Arm64 25H2 Pro China (V2)"}, } var evalProducts = []EvalProduct{ diff --git a/frontend/public/data/products.json b/frontend/public/data/products.json index ee9f298..e4b9c13 100644 --- a/frontend/public/data/products.json +++ b/frontend/public/data/products.json @@ -169,7 +169,7 @@ ] }, "3321": { - "name": "Windows 11 25H2 (Updated Oct)", + "name": "Windows 11 25H2 (V2)", "badge": "LATEST", "archs": [ "x64" @@ -180,8 +180,30 @@ "3113" ] }, + "3322": { + "name": "Windows 11 25H2 Home China (V2)", + "badge": "LATEST", + "archs": [ + "x64" + ], + "related": [ + "3321", + "3323" + ] + }, + "3323": { + "name": "Windows 11 25H2 Pro China (V2)", + "badge": "LATEST", + "archs": [ + "x64" + ], + "related": [ + "3321", + "3322" + ] + }, "3324": { - "name": "Windows 11 Arm64 25H2 (Updated Oct)", + "name": "Windows 11 Arm64 25H2 (V2)", "badge": "LATEST", "archs": [ "ARM64" @@ -191,5 +213,27 @@ "3262", "3265" ] + }, + "3325": { + "name": "Windows 11 Arm64 25H2 Home China (V2)", + "badge": "LATEST", + "archs": [ + "ARM64" + ], + "related": [ + "3324", + "3326" + ] + }, + "3326": { + "name": "Windows 11 Arm64 25H2 Pro China (V2)", + "badge": "LATEST", + "archs": [ + "ARM64" + ], + "related": [ + "3324", + "3325" + ] } } \ No newline at end of file diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index c7a6ecb..b4d34be 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -63,5 +63,9 @@ https://msdl.tech-latest.com/product/32662026-05-120.8monthly https://msdl.tech-latest.com/product/32672026-05-120.8monthly https://msdl.tech-latest.com/product/33212026-05-120.8monthly + https://msdl.tech-latest.com/product/33222026-07-140.8monthly + https://msdl.tech-latest.com/product/33232026-07-140.8monthly https://msdl.tech-latest.com/product/33242026-05-120.8monthly + https://msdl.tech-latest.com/product/33252026-07-140.8monthly + https://msdl.tech-latest.com/product/33262026-07-140.8monthly diff --git a/scripts/check-new-releases/README.md b/scripts/check-new-releases/README.md new file mode 100644 index 0000000..20fc510 --- /dev/null +++ b/scripts/check-new-releases/README.md @@ -0,0 +1,35 @@ +# check-new-releases + +Checks Microsoft's public Windows 11 download page for a new "flagship" product +edition ID not yet in `cli/catalog.go`, and if found, probes nearby IDs to +discover the accompanying variant family (Home China, Pro China, ARM64, ...). + +Windows 8.1 and Windows 10 aren't checked -- 8.1 is fully frozen and Windows 10 +is past end-of-life, so neither will ever produce a new consumer ISO release. +Windows 11 is the only OS still shipping feature updates. + +This tool only *discovers* candidate IDs. It doesn't touch `cli/catalog.go`, +`products.json`, or `validContributeProducts` automatically -- naming a new +entry correctly (build number, "Updated Oct"-style qualifiers, etc.) needs a +human checking Microsoft's own release-health pages. + +## Usage + +```bash +cd scripts/check-new-releases +go run . +``` + +If a new release is found, it makes real requests to Microsoft to probe the +variant family -- let it finish rather than re-running immediately. + +Flags: +- `-catalog ` — path to `cli/catalog.go` (default: `../../cli/catalog.go`) +- `-probe-range ` — how many adjacent IDs to check after a new flagship is found (default: 20) + +## Known limitation + +The ARM64 family's offset from the flagship isn't consistent release to +release (observed: +3 for the 25H2 family, +18 for 24H2's). The default probe +range of 20 covers both known cases, but if no ARM64 variant shows up, try a +wider `-probe-range`. diff --git a/scripts/check-new-releases/go.mod b/scripts/check-new-releases/go.mod new file mode 100644 index 0000000..da35d70 --- /dev/null +++ b/scripts/check-new-releases/go.mod @@ -0,0 +1,3 @@ +module check-new-releases + +go 1.25.6 diff --git a/scripts/check-new-releases/main.go b/scripts/check-new-releases/main.go new file mode 100644 index 0000000..977a789 --- /dev/null +++ b/scripts/check-new-releases/main.go @@ -0,0 +1,321 @@ +// check-new-releases scrapes Microsoft's public Windows 11 download page for +// the current "flagship" product edition ID and compares it against the +// catalog already tracked in cli/catalog.go. Windows 8.1 and Windows 10 are +// intentionally not checked here -- 8.1 is fully frozen and Windows 10 is +// past end-of-life, so neither will ever produce a new consumer ISO release. +// +// If a new flagship ID is found, it probes a bounded range of adjacent IDs to +// discover the accompanying variant family (Home China, Pro China, ARM64, +// ...), since Microsoft allocates those in a cluster near the flagship ID +// but the exact offset isn't consistent release to release (observed gaps: +// +3 for the 25H2 family, +18 for 24H2's ARM64 family). +// +// This tool only *discovers* candidate IDs -- it does not touch cli/catalog.go, +// products.json, or validContributeProducts automatically. Naming a new entry +// correctly (build number, "Updated Oct"-style qualifiers, etc.) still needs a +// human checking Microsoft's own release-health pages. +// +// Usage: go run . [-catalog path/to/cli/catalog.go] [-probe-range N] +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "regexp" + "strconv" + "sync" + "time" +) + +// --- Copied from cli/microsoft.go (not imported -- cli/ is package main and +// can't be imported from elsewhere). This is a standalone maintenance tool, +// not part of the shipped CLI/backend; keep in sync manually if the real +// session flow ever changes. --- + +const ( + msUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + msProfile = "606624d44113" + msLocale = "en-US" + msOrgID = "y6jn8c31" + msCustomer = "560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175" +) + +var ( + reW = regexp.MustCompile(`[&?]w=([^&"'\s]+)`) + reRt = regexp.MustCompile(`rticks[="]+\+?\s*(\d{10,})`) +) + +type skuLang struct { + ID string `json:"Id"` + Language string `json:"Language"` + ProductDisplayName string `json:"ProductDisplayName"` +} + +type simpleCookieJar struct { + mu sync.Mutex + cookies []*http.Cookie +} + +func (j *simpleCookieJar) SetCookies(_ *url.URL, cookies []*http.Cookie) { + j.mu.Lock() + defer j.mu.Unlock() + for _, c := range cookies { + found := false + for i, existing := range j.cookies { + if existing.Name == c.Name { + j.cookies[i] = c + found = true + break + } + } + if !found { + j.cookies = append(j.cookies, c) + } + } +} + +func (j *simpleCookieJar) Cookies(_ *url.URL) []*http.Cookie { + j.mu.Lock() + defer j.mu.Unlock() + out := make([]*http.Cookie, len(j.cookies)) + copy(out, j.cookies) + return out +} + +func newSessionID() string { + b := make([]byte, 16) + rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%s-%s-%s-%s-%s", + hex.EncodeToString(b[0:4]), hex.EncodeToString(b[4:6]), + hex.EncodeToString(b[6:8]), hex.EncodeToString(b[8:10]), + hex.EncodeToString(b[10:16])) +} + +func newSession() (*http.Client, string) { + sessionID := newSessionID() + jar := &simpleCookieJar{} + client := &http.Client{Timeout: 15 * time.Second, Jar: jar} + + q1 := url.Values{} + q1.Set("org_id", msOrgID) + q1.Set("session_id", sessionID) + req1, _ := http.NewRequest("GET", "https://vlscppe.microsoft.com/tags?"+q1.Encode(), nil) + req1.Header.Set("User-Agent", msUA) + client.Do(req1) + + q2 := url.Values{} + q2.Set("instanceId", msCustomer) + q2.Set("PageId", "si") + q2.Set("session_id", sessionID) + req2, _ := http.NewRequest("GET", "https://ov-df.microsoft.com/mdt.js?"+q2.Encode(), nil) + req2.Header.Set("User-Agent", msUA) + resp2, err := client.Do(req2) + if err != nil { + return client, sessionID + } + body, _ := io.ReadAll(resp2.Body) + resp2.Body.Close() + + wMatch := reW.FindStringSubmatch(string(body)) + rtMatch := reRt.FindStringSubmatch(string(body)) + if len(wMatch) > 1 && len(rtMatch) > 1 { + q3 := url.Values{} + q3.Set("session_id", sessionID) + q3.Set("CustomerId", msCustomer) + q3.Set("PageId", "si") + q3.Set("w", wMatch[1]) + q3.Set("mdt", fmt.Sprintf("%d", time.Now().UnixMilli())) + q3.Set("rticks", rtMatch[1]) + req3, _ := http.NewRequest("GET", "https://ov-df.microsoft.com/?"+q3.Encode(), nil) + req3.Header.Set("User-Agent", msUA) + client.Do(req3) + } + return client, sessionID +} + +type msErrorEntry struct { + Type float64 `json:"Type"` + Value string `json:"Value"` +} + +func firstError(errs []msErrorEntry) string { + if len(errs) == 0 { + return "" + } + if int(errs[0].Type) == 9 { + if errs[0].Value != "" { + return errs[0].Value + } + return "Your IP has been temporarily blocked by Microsoft (Code 715-123130)" + } + if errs[0].Value != "" { + return errs[0].Value + } + return "Microsoft API error" +} + +func fetchLanguages(client *http.Client, sessionID, productID string) ([]skuLang, error) { + q := url.Values{} + q.Set("profile", msProfile) + q.Set("productEditionId", productID) + q.Set("SKU", "undefined") + q.Set("friendlyFileName", "undefined") + q.Set("Locale", msLocale) + q.Set("sessionID", sessionID) + + req, _ := http.NewRequest("GET", "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?"+q.Encode(), nil) + req.Header.Set("User-Agent", msUA) + req.Header.Set("Referer", "https://www.microsoft.com/en-us/software-download/windows11") + req.Header.Set("Accept", "application/json") + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Microsoft returned HTTP %d", resp.StatusCode) + } + if len(body) > 0 && body[0] == '"' { + var unquoted string + json.Unmarshal(body, &unquoted) + body = []byte(unquoted) + } + + var data struct { + Skus []skuLang `json:"Skus"` + Errors []msErrorEntry `json:"Errors"` + } + if err := json.Unmarshal(body, &data); err != nil { + return nil, fmt.Errorf("invalid SKU response: %w", err) + } + if msg := firstError(data.Errors); msg != "" { + return nil, fmt.Errorf("%s", msg) + } + if len(data.Skus) == 0 { + return nil, fmt.Errorf("no languages found for this product") + } + return data.Skus, nil +} + +// --- New logic specific to this tool --- + +var catalogEntryRe = regexp.MustCompile(`\{"(\d+)",\s*"([^"]*)"\}`) + +func knownCatalogIDs(catalogPath string) (map[string]string, error) { + data, err := os.ReadFile(catalogPath) + if err != nil { + return nil, err + } + matches := catalogEntryRe.FindAllStringSubmatch(string(data), -1) + out := make(map[string]string, len(matches)) + for _, m := range matches { + out[m[1]] = m[2] + } + return out, nil +} + +var flagshipRe = regexp.MustCompile(`