docs(release): backfill curated 0.23.0 release notes, fix the notes CLI -- trap (#1263)

Backfills the 0.23.0 release notes the .changes/ pipeline missed: it landed
after most of the release was already merged, leaving 4 notes for 79 commits.

Adds 22 curated notes (26 total: 14 features, 11 fixes, 1 perf). Curated
rather than exhaustive — the GitHub release body renders these above
GitHub's own list of every merged PR, so related PRs are folded into one
note per user-facing story: shared player controls (8 PRs), embedded MPV
frame-copy (4), manual EPG mapping (3), plus five more pairs. Tooling-only
scopes get no note. No screenshot slugs: none of the five manifest shots
depicts a 0.23 headline feature, and the capture run asserts TMDB
enrichment stays disabled.

Two tooling fixes found while writing them:

- parseArgs now ignores a bare `--`. npm needs it to forward arguments past
  the script name; pnpm hands it to the script verbatim, so
  `pnpm run release:notes:github -- --version 0.24.0` died on the very
  separator typed to make forwarding work. Unknown flags and missing values
  still fail as before. Covered by new subprocess CLI tests wired into the
  release-tools target.
- .changes/README.md claimed every non-consume mode was a safe dry run;
  --format changelog and --format blog write their target file.

No app or lib code, no version bump, no --consume, no CHANGELOG.md or
website changes — those stay owned by release-cut.
This commit is contained in:
4gray authored and GitHub committed 2026-07-27 01:36:11 +02:00
1 parent 75c45c9e91
commit 02b966895d
26 files changed
+340 -3

No files matched your search

+13 -2
View File
@@ -69,9 +69,20 @@ node tools/release/build-release-notes.mjs --consume
```
The release version comes from the root `package.json` — bump it first, then
generate. `--version 0.24.0` overrides it for a dry run before the bump.
generate. `--version 0.24.0` overrides it to preview a release before the bump:
Only `--consume` deletes anything; every other mode is a safe dry run.
```bash
pnpm run release:notes:github --version 0.24.0
```
A bare `--` separator is accepted and ignored, so the npm habit of
`pnpm run release:notes:github -- --version 0.24.0` works too: pnpm forwards
that separator to the script rather than consuming it the way npm does.
`--validate` and `--format github` only read and print. `--format changelog`
and `--format blog` write their target file (rerunning `changelog` for the same
version replaces that section rather than duplicating it). Only `--consume`
deletes anything.
The release sequence is: bump the version → `release:notes:changelog` →
`release:notes:blog` → `--consume` → commit → tag → push. The tag build then
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: backup
issues: [1017]
---
Backups carry hidden Xtream categories correctly. An export used to lose which
categories you had hidden, and restoring such a backup then hid every category
of that kind.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: downloads
---
Downloads can be paused and picked up later. A paused transfer keeps what it
already fetched and continues from that point instead of starting over, and
downloads cut short by a crash or a closed app come back as paused rather than
lost.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: embedded-mpv
---
Experimental Embedded MPV can draw video inside the app window instead of into a
separate layer pinned on top of it, so menus, dialogs and the player controls
stop being swallowed by the picture. Available on macOS (Apple Silicon), Windows
and Linux x64; switching it on needs a restart.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: epg
---
Channels whose guide never matched can be mapped by hand: right-click a channel
in any list and pick "Map EPG channel" to attach it to a channel from your
uploaded XMLTV guide. The mapping is remembered and used everywhere the guide is
read — M3U playlists, Xtream and Stalker portals alike.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: favorites
issues: [1137]
---
Favorites stop losing changes. The custom drag-and-drop order of an Xtream
playlist's own favorites is saved again, and two favorites or history entries
added at almost the same moment no longer quietly overwrite each other.
+7
View File
@@ -0,0 +1,7 @@
---
type: feature
area: i18n
issues: [1192]
---
IPTVnator speaks Hungarian, its 19th language — contributed by @htibcsike.
+10
View File
@@ -0,0 +1,10 @@
---
type: feature
area: m3u
issues: [86, 614, 656, 733, 752]
---
MPEG-DASH channels play in the built-in player, ClearKey-encrypted ones
included — the keys are read from the playlist's #KODIPROP lines, whether they
sit above or below the channel entry. Streams locked with Widevine or PlayReady
still cannot be played, but they now say so instead of failing silently.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: m3u
issues: [1189]
---
Playlists with very long stream URLs — Pluto TV style lists that carry a session
token in every link — import in full again instead of collapsing into a single
channel.
@@ -0,0 +1,9 @@
---
type: feature
area: playback
---
An optional new set of player controls that looks and behaves the same in the
HTML5, Video.js and ArtPlayer players, with picture-in-picture and, in
fullscreen, the name of what you are watching. Enable it in Settings → Playback;
left off, each of the three keeps its own controls exactly as before.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: playback
---
The inline player on movie and series pages fills the whole content area like a
theater: the video sits centered in black instead of leaving a strip of app
background beside it. In the built-in web players, an optional ambient mode
fills that space with a blurred, dimmed copy of the poster.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: playback
---
A series playing inline on a wide window shows an "Up Next" rail beside the
video: the rest of the season and the start of the next one, the current episode
highlighted, watch progress on every card. Click one to jump straight to it.
Built-in web players only; switch the rail off in Settings → Playback.
+10
View File
@@ -0,0 +1,10 @@
---
type: fix
area: playlists
issues: [931]
---
One unreachable playlist no longer holds up the rest. Refreshes give up after 30
seconds and run a few at a time, so your other playlists still update, and the
message on startup names how many actually failed instead of always claiming
success.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: search
issues: [1161]
---
Searching for names that carry punctuation inside them — "A&E", "X-Men",
"L'Equipe" — finds them anywhere in a title, including channels the provider
prefixes, like "US: A&E".
@@ -0,0 +1,9 @@
---
type: feature
area: settings
---
A new setting drops the country prefix from live channel names, turning
"UK - BBC One" into "BBC One" in lists, the guide and the player. Movie and
series titles are left alone, and names that only look like a prefix — "Sky -
Sports F1" — stay intact.
+9
View File
@@ -0,0 +1,9 @@
---
type: perf
area: stalker
---
Detail pages for anything that is not a series — a movie, a live channel, a VOD
item that only looks like a series — no longer fire an episode-list request
before they can show anything, so they open faster on every route in, from
browsing and search to Favorites, Recent and the dashboard.
@@ -0,0 +1,9 @@
---
type: fix
area: stalker
---
Series opened from Favorites, Recent or the dashboard show their current
episodes. One saved back when a single episode existed used to keep showing that
one episode forever; the list is refreshed from the portal in the background
instead.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: stalker
---
The Live TV section loads its full channel list up front: search covers every
channel instead of only the page you are on, genres show how many channels they
hold, and Live TV opens on a grid of all channels. Guide data for the visible
rows loads in bulk, so it shows up without playing a channel first.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: tmdb
---
Series pages show whether a show has ended or is still returning, so you know
before committing to it. Directors and creators became clickable avatar chips
like the cast — they open the person's page, where directing credits now sit
alongside acting ones.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: tmdb
---
Titles that providers dress up with language or quality tags — "|ALB| Fallout",
"4K-DE - The Pitt (2025)", "Breaking Bad-eng" — now match against TMDB, so they
get artwork, plot and cast like the rest of the catalog. Shows split into one
entry per season also pull the season that entry really contains.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: xtream
issues: [1138]
---
Catch-up is available from Favorites and Recent, not just Live TV, so an
archived programme is reachable wherever the channel is. The programme currently
on air can also be restarted from the beginning.
+9
View File
@@ -0,0 +1,9 @@
---
type: fix
area: xtream
---
Portals sitting behind Cloudflare or a similar firewall connect again. Those
setups answered the app with a challenge page instead of data, so "Test
connection" failed on portals that worked fine in every other player; requests
now identify themselves the way an ordinary IPTV player does.
+9
View File
@@ -0,0 +1,9 @@
---
type: feature
area: xtream
---
Continue Watching resumes a series where you left it: opening one from the
dashboard starts the exact episode at its saved position, and the series page
offers "Play episode N" instead of always starting at the first one. Episodes
launched in MPV or VLC count towards this too.
+7
View File
@@ -90,6 +90,13 @@ function parseArgs(argv) {
case '--force':
options.force = true;
break;
// npm needs `--` to forward arguments past the script name; pnpm
// hands it to the script verbatim. Ignore it so the habitual
// `pnpm run release:notes:github -- --version 0.24.0` works
// instead of dying on its own separator. There are no positional
// arguments for it to delimit.
case '--':
break;
default:
throw new Error(`unknown argument: ${arg}`);
}
+119
View File
@@ -0,0 +1,119 @@
/**
* CLI-level tests for build-release-notes.mjs.
*
* The script parses its arguments and runs on import, so these drive the real
* entry point in a subprocess rather than importing parseArgs directly.
*/
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { after, describe, it } from 'node:test';
const workspaceRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
'../..'
);
const CLI = path.join(workspaceRoot, 'tools/release/build-release-notes.mjs');
const tempDirs = [];
/** A notes directory holding one valid note, so runs have something to render. */
function makeNotesDir() {
const directory = mkdtempSync(path.join(tmpdir(), 'release-cli-'));
tempDirs.push(directory);
writeFileSync(
path.join(directory, 'playback-example.md'),
'---\ntype: feature\narea: playback\n---\n\nAn example note.\n',
'utf8'
);
return directory;
}
/**
* @returns {{ status: number, stdout: string, stderr: string }}
*/
function runCli(args) {
try {
const stdout = execFileSync(process.execPath, [CLI, ...args], {
cwd: workspaceRoot,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return { status: 0, stdout, stderr: '' };
} catch (error) {
return {
status: error.status ?? 1,
stdout: error.stdout ?? '',
stderr: error.stderr ?? '',
};
}
}
after(() => {
for (const directory of tempDirs) {
rmSync(directory, { recursive: true, force: true });
}
});
describe('build-release-notes CLI arguments', () => {
it('validates a notes directory', () => {
const result = runCli(['--validate', '--dir', makeNotesDir()]);
assert.equal(result.status, 0);
assert.match(result.stdout, /1 release note\(s\) valid\./);
});
it('ignores a bare `--` separator', () => {
// pnpm forwards `--` to the script instead of consuming it, so the
// npm-style `pnpm run release:notes:validate -- --dir x` reaches us
// with the separator still attached.
const result = runCli(['--validate', '--', '--dir', makeNotesDir()]);
assert.equal(result.status, 0);
assert.match(result.stdout, /1 release note\(s\) valid\./);
});
it('ignores a leading `--` separator', () => {
const result = runCli(['--', '--validate', '--dir', makeNotesDir()]);
assert.equal(result.status, 0);
assert.match(result.stdout, /1 release note\(s\) valid\./);
});
it('renders the github body with a version passed after `--`', () => {
const result = runCli([
'--format',
'github',
'--',
'--version',
'0.24.0',
'--dir',
makeNotesDir(),
]);
assert.equal(result.status, 0);
assert.match(result.stdout, /## Features/);
assert.match(result.stdout, /\*\*playback\*\* — An example note\./);
});
it('still rejects a genuinely unknown argument', () => {
const result = runCli(['--validate', '--nope']);
assert.equal(result.status, 1);
assert.match(result.stderr, /unknown argument: --nope/);
});
it('still rejects a flag whose value is missing', () => {
const result = runCli(['--format']);
assert.equal(result.status, 1);
assert.match(result.stderr, /--format requires a value/);
});
});
+3 -1
View File
@@ -12,14 +12,16 @@
"{workspaceRoot}/tools/release/release-notes-render.mjs",
"{workspaceRoot}/tools/release/extract-changelog-section.mjs",
"{workspaceRoot}/tools/release/check-release-note-gate.mjs",
"{workspaceRoot}/tools/release/build-release-notes.mjs",
"{workspaceRoot}/tools/release/screenshot-guards.mjs",
"{workspaceRoot}/tools/release/screenshots.manifest.json",
"{workspaceRoot}/tools/release/release-notes.test.mjs",
"{workspaceRoot}/tools/release/release-note-gate.test.mjs",
"{workspaceRoot}/tools/release/build-release-notes.test.mjs",
"{workspaceRoot}/tools/release/screenshot-guards.test.mjs"
],
"options": {
"command": "node --test tools/release/release-notes.test.mjs tools/release/release-note-gate.test.mjs tools/release/screenshot-guards.test.mjs",
"command": "node --test tools/release/release-notes.test.mjs tools/release/release-note-gate.test.mjs tools/release/build-release-notes.test.mjs tools/release/screenshot-guards.test.mjs",
"cwd": "{workspaceRoot}"
}
},