diff --git a/.changes/tmdb-cyrillic-search-spelling.md b/.changes/tmdb-cyrillic-search-spelling.md index bd7fb6d05..7ea93f6d2 100644 --- a/.changes/tmdb-cyrillic-search-spelling.md +++ b/.changes/tmdb-cyrillic-search-spelling.md @@ -3,8 +3,7 @@ type: fix area: tmdb --- -Russian titles containing "й" or "ё" ("Фейк", "Волшебный участок", "Молодой -Шерлок") and Arabic titles with hamza letters ("أطرق بابي") now match on TMDB. -The search used to send a folded spelling ("феик") that TMDB never recognised -and cached the miss for a week; those cached misses are cleared on the next -start. +Russian titles containing "й" or "ё" and Arabic titles with hamza letters now +match on TMDB. The search used to send a folded spelling — dropping the breve +from "й", for instance — that TMDB never recognised, and cached the miss for a +week; those cached misses are cleared on the next start. diff --git a/tools/embedded-mpv/README.md b/tools/embedded-mpv/README.md index fc4c423ae..fca1985c6 100644 --- a/tools/embedded-mpv/README.md +++ b/tools/embedded-mpv/README.md @@ -103,6 +103,18 @@ updater: pnpm embedded-mpv:windows-runtime-pin:refresh -- --force ``` +That workflow validates its own result with +`windows-runtime-pin.test.mjs`, so nothing in those refresh tests may read the +CHECKED-IN pin's `publishedAt`. Building a fixture from it makes the outcome a +function of the very data the job replaces, and the assertion then fails +exactly when the job succeeds: a rotation asserted that the pin it had just +written was already past the 14-day threshold, the validation step went red, +the bot PR was never opened, and the pin sat until upstream retention deleted +its asset — turning every Windows build red on a cold cache. Refresh tests +build their own pin at a chosen age (`createPinFixture({ ageDays })`); +`CURRENT_PIN` is for the schema, naming and licence-statement checks, which +hold for any pin. + The upstream archive is checksum- and layout-verified, not independently certified as a complete LGPL closure. It contains no corresponding source or license notices, so IPTVnator does not mirror it. Any future stable mirror must diff --git a/tools/embedded-mpv/windows-runtime-pin.json b/tools/embedded-mpv/windows-runtime-pin.json index 8305b6373..8b088058e 100644 --- a/tools/embedded-mpv/windows-runtime-pin.json +++ b/tools/embedded-mpv/windows-runtime-pin.json @@ -1,17 +1,17 @@ { "schemaVersion": 1, "repository": "zhongfly/mpv-winbuild", - "releaseTag": "2026-08-21-49418246f3", - "publishedAt": "2026-08-21T12:20:41Z", + "releaseTag": "2026-09-21-e76a35ec95", + "publishedAt": "2026-09-21T12:23:42Z", "retentionDays": 30, "asset": { - "name": "mpv-dev-lgpl-x86_64-20260821-git-49418246f3.7z", - "url": "https://github.com/zhongfly/mpv-winbuild/releases/download/2026-08-21-49418246f3/mpv-dev-lgpl-x86_64-20260821-git-49418246f3.7z", - "sha256": "317dfd9ee814be76e5f6e20b45efcc07440389a62b55dd85201829b4880510e0" + "name": "mpv-dev-lgpl-x86_64-20260921-git-e76a35ec95.7z", + "url": "https://github.com/zhongfly/mpv-winbuild/releases/download/2026-09-21-e76a35ec95/mpv-dev-lgpl-x86_64-20260921-git-e76a35ec95.7z", + "sha256": "e0b8202aa8269795e41076898fd5ec0a9038ae8267a42d6fbbe94c727d586f33" }, "upstream": { - "mpvCommit": "49418246f30a9c24af31ac184aa24f39755db89a", - "buildRunUrl": "https://github.com/zhongfly/mpv-winbuild/actions/runs/32479875364", + "mpvCommit": "e76a35ec95b27f5cf2d27b043b5e2e0d90e468ae", + "buildRunUrl": "https://github.com/zhongfly/mpv-winbuild/actions/runs/35597141455", "licenseClaim": "Upstream labels this libmpv build LGPLv2.1+ with statically linked LGPLv3 FFmpeg; IPTVnator verifies the checksum and archive layout, not the complete transitive license closure." } } diff --git a/tools/embedded-mpv/windows-runtime-pin.test.mjs b/tools/embedded-mpv/windows-runtime-pin.test.mjs index bbf95961a..e518f77eb 100644 --- a/tools/embedded-mpv/windows-runtime-pin.test.mjs +++ b/tools/embedded-mpv/windows-runtime-pin.test.mjs @@ -65,11 +65,44 @@ function response({ ok = true, status = 200, json } = {}) { }; } -function createPinFixture() { +/** The clock every refresh-rule test reasons against. */ +const REFRESH_CLOCK = new Date('2026-08-29T00:00:00Z'); +const DAY_MS = 86_400_000; + +/** + * A checked-out pin whose AGE the test chooses, built from the synthetic + * release fixture rather than from `CURRENT_PIN`. + * + * The refresh tests are about the rule, so they must not read the checked-in + * pin's publish date. Reading it makes the outcome a function of production + * data that this very job exists to replace, and the assertion then fails + * precisely when the job succeeds: the weekly refresh has been red since + * 2026-09-07 because its validation step asserted that the pin it had just + * rotated was already over two weeks old, so the rotation never reached a + * pull request and the checked-in pin was left to expire out of upstream + * retention. `CURRENT_PIN` stays where it belongs — the schema, naming and + * licence-statement checks, which exist to validate the checked-in file and + * hold for any pin. + * + * The fixture's commit differs from `releaseFixture()`'s default so a + * rotation is observable as a change. + */ +function createPinFixture({ + ageDays = 0, + commit = 'a1b2c3d4e5123456789012345678901234567890', +} = {}) { + const publishedAt = new Date(REFRESH_CLOCK.getTime() - ageDays * DAY_MS); + const pin = pinFromUpstreamRelease( + releaseFixture({ + date: publishedAt.toISOString().slice(0, 10), + commit, + publishedAt: publishedAt.toISOString(), + }) + ); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'impv-win-pin-')); const pinPath = path.join(root, 'windows-runtime-pin.json'); - fs.writeFileSync(pinPath, serializeWindowsRuntimePin(CURRENT_PIN)); - return { root, pinPath }; + fs.writeFileSync(pinPath, serializeWindowsRuntimePin(pin)); + return { root, pinPath, pin }; } test('checked-in Windows runtime pin is internally consistent', () => { @@ -124,13 +157,15 @@ test('upstream release must provide GitHub digest and build evidence', () => { ); }); -test('young available pin does not query releases or rewrite the file', async () => { - const fixture = createPinFixture(); +test('available pin one day under the threshold is left alone', async () => { + const fixture = createPinFixture({ + ageDays: WINDOWS_RUNTIME_REFRESH_AFTER_DAYS - 1, + }); let requestCount = 0; try { const result = await refreshWindowsRuntimePin({ pinPath: fixture.pinPath, - now: new Date('2026-08-29T00:00:00Z'), + now: REFRESH_CLOCK, fetchImpl: async () => { requestCount += 1; return response(); @@ -141,24 +176,42 @@ test('young available pin does not query releases or rewrite the file', async () assert.equal(requestCount, 1); assert.equal( fs.readFileSync(fixture.pinPath, 'utf8'), - serializeWindowsRuntimePin(CURRENT_PIN) + serializeWindowsRuntimePin(fixture.pin) ); } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); } }); +test('a pin rotated moments ago does not immediately ask to rotate again', async () => { + // The invariant the refresh job depends on: its own freshly written pin + // must read as current on the next run, whatever date that run falls on. + const fixture = createPinFixture({ ageDays: 0 }); + try { + const result = await refreshWindowsRuntimePin({ + pinPath: fixture.pinPath, + now: REFRESH_CLOCK, + fetchImpl: async () => response(), + }); + assert.equal(result.reason, 'current'); + assert.equal(result.changed, false); + } finally { + fs.rmSync(fixture.root, { recursive: true, force: true }); + } +}); + test('unavailable pin rotates to the newest downloadable release', async () => { - const fixture = createPinFixture(); + // Deliberately young, so only the 404 can explain the rotation. + const fixture = createPinFixture({ ageDays: 1 }); const latest = releaseFixture(); const requests = []; try { const result = await refreshWindowsRuntimePin({ pinPath: fixture.pinPath, - now: new Date('2026-08-29T00:00:00Z'), + now: REFRESH_CLOCK, fetchImpl: async (url, options = {}) => { requests.push([url, options.method ?? 'GET']); - if (url === CURRENT_PIN.asset.url) { + if (url === fixture.pin.asset.url) { return response({ ok: false, status: 404 }); } if (url.startsWith('https://api.github.com/')) { @@ -183,24 +236,29 @@ test('unavailable pin rotates to the newest downloadable release', async () => { }); test('age threshold rotates an available pin before upstream retention', async () => { - const fixture = createPinFixture(); + const fixture = createPinFixture({ + ageDays: WINDOWS_RUNTIME_REFRESH_AFTER_DAYS, + }); const latest = releaseFixture(); try { + assert.equal( + runtimePinAgeDays(fixture.pin, REFRESH_CLOCK), + WINDOWS_RUNTIME_REFRESH_AFTER_DAYS + ); const result = await refreshWindowsRuntimePin({ pinPath: fixture.pinPath, - now: new Date('2026-09-05T13:00:00Z'), + now: REFRESH_CLOCK, fetchImpl: async (url) => url.startsWith('https://api.github.com/') ? response({ json: [latest] }) : response(), }); - assert.equal( - runtimePinAgeDays(CURRENT_PIN, new Date('2026-09-05T13:00:00Z')) >= - WINDOWS_RUNTIME_REFRESH_AFTER_DAYS, - true - ); assert.equal(result.changed, true); assert.equal(result.reason, 'age-threshold'); + assert.equal( + readWindowsRuntimePin(fixture.pinPath).releaseTag, + latest.tag_name + ); } finally { fs.rmSync(fixture.root, { recursive: true, force: true }); }