diff --git a/.changes/README.md b/.changes/README.md index a2a8454b6..e41282f03 100644 --- a/.changes/README.md +++ b/.changes/README.md @@ -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 diff --git a/.changes/backup-hidden-categories.md b/.changes/backup-hidden-categories.md new file mode 100644 index 000000000..064f99137 --- /dev/null +++ b/.changes/backup-hidden-categories.md @@ -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. diff --git a/.changes/downloads-pause-resume.md b/.changes/downloads-pause-resume.md new file mode 100644 index 000000000..2bde5dffb --- /dev/null +++ b/.changes/downloads-pause-resume.md @@ -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. diff --git a/.changes/embedded-mpv-frame-copy.md b/.changes/embedded-mpv-frame-copy.md new file mode 100644 index 000000000..6a866ae2f --- /dev/null +++ b/.changes/embedded-mpv-frame-copy.md @@ -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. diff --git a/.changes/epg-manual-channel-mapping.md b/.changes/epg-manual-channel-mapping.md new file mode 100644 index 000000000..7cb429b15 --- /dev/null +++ b/.changes/epg-manual-channel-mapping.md @@ -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. diff --git a/.changes/favorites-lost-updates.md b/.changes/favorites-lost-updates.md new file mode 100644 index 000000000..a54c32de3 --- /dev/null +++ b/.changes/favorites-lost-updates.md @@ -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. diff --git a/.changes/i18n-hungarian.md b/.changes/i18n-hungarian.md new file mode 100644 index 000000000..691f77900 --- /dev/null +++ b/.changes/i18n-hungarian.md @@ -0,0 +1,7 @@ +--- +type: feature +area: i18n +issues: [1192] +--- + +IPTVnator speaks Hungarian, its 19th language — contributed by @htibcsike. diff --git a/.changes/m3u-dash-clearkey.md b/.changes/m3u-dash-clearkey.md new file mode 100644 index 000000000..c2853da6b --- /dev/null +++ b/.changes/m3u-dash-clearkey.md @@ -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. diff --git a/.changes/m3u-long-stream-urls.md b/.changes/m3u-long-stream-urls.md new file mode 100644 index 000000000..e084e55ee --- /dev/null +++ b/.changes/m3u-long-stream-urls.md @@ -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. diff --git a/.changes/playback-shared-player-controls.md b/.changes/playback-shared-player-controls.md new file mode 100644 index 000000000..d2f27b104 --- /dev/null +++ b/.changes/playback-shared-player-controls.md @@ -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. diff --git a/.changes/playback-theater-stage.md b/.changes/playback-theater-stage.md new file mode 100644 index 000000000..b8f67a5c4 --- /dev/null +++ b/.changes/playback-theater-stage.md @@ -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. diff --git a/.changes/playback-up-next-rail.md b/.changes/playback-up-next-rail.md new file mode 100644 index 000000000..13738a792 --- /dev/null +++ b/.changes/playback-up-next-rail.md @@ -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. diff --git a/.changes/playlists-auto-refresh.md b/.changes/playlists-auto-refresh.md new file mode 100644 index 000000000..2104099f2 --- /dev/null +++ b/.changes/playlists-auto-refresh.md @@ -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. diff --git a/.changes/search-punctuation-words.md b/.changes/search-punctuation-words.md new file mode 100644 index 000000000..b33302a09 --- /dev/null +++ b/.changes/search-punctuation-words.md @@ -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". diff --git a/.changes/settings-strip-country-prefix.md b/.changes/settings-strip-country-prefix.md new file mode 100644 index 000000000..bc8980306 --- /dev/null +++ b/.changes/settings-strip-country-prefix.md @@ -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. diff --git a/.changes/stalker-detail-requests.md b/.changes/stalker-detail-requests.md new file mode 100644 index 000000000..f8651f05d --- /dev/null +++ b/.changes/stalker-detail-requests.md @@ -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. diff --git a/.changes/stalker-embedded-series-episodes.md b/.changes/stalker-embedded-series-episodes.md new file mode 100644 index 000000000..f808d4cbf --- /dev/null +++ b/.changes/stalker-embedded-series-episodes.md @@ -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. diff --git a/.changes/stalker-live-tv-channel-list.md b/.changes/stalker-live-tv-channel-list.md new file mode 100644 index 000000000..d93bfe8c4 --- /dev/null +++ b/.changes/stalker-live-tv-channel-list.md @@ -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. diff --git a/.changes/tmdb-detail-page-extras.md b/.changes/tmdb-detail-page-extras.md new file mode 100644 index 000000000..8b57a6181 --- /dev/null +++ b/.changes/tmdb-detail-page-extras.md @@ -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. diff --git a/.changes/tmdb-title-matching.md b/.changes/tmdb-title-matching.md new file mode 100644 index 000000000..025cb790a --- /dev/null +++ b/.changes/tmdb-title-matching.md @@ -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. diff --git a/.changes/xtream-catchup-collections.md b/.changes/xtream-catchup-collections.md new file mode 100644 index 000000000..b8aa5dda6 --- /dev/null +++ b/.changes/xtream-catchup-collections.md @@ -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. diff --git a/.changes/xtream-portal-connection.md b/.changes/xtream-portal-connection.md new file mode 100644 index 000000000..6d680359d --- /dev/null +++ b/.changes/xtream-portal-connection.md @@ -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. diff --git a/.changes/xtream-series-resume.md b/.changes/xtream-series-resume.md new file mode 100644 index 000000000..8f4b89b8a --- /dev/null +++ b/.changes/xtream-series-resume.md @@ -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. diff --git a/tools/release/build-release-notes.mjs b/tools/release/build-release-notes.mjs index 83745334b..39a921d97 100644 --- a/tools/release/build-release-notes.mjs +++ b/tools/release/build-release-notes.mjs @@ -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}`); } diff --git a/tools/release/build-release-notes.test.mjs b/tools/release/build-release-notes.test.mjs new file mode 100644 index 000000000..e7d77f9dd --- /dev/null +++ b/tools/release/build-release-notes.test.mjs @@ -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/); + }); +}); diff --git a/tools/release/project.json b/tools/release/project.json index ed024a440..0dec473ec 100644 --- a/tools/release/project.json +++ b/tools/release/project.json @@ -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}" } },