mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-12 16:19:26 -07:00
Windows update (#49)
This commit is contained in:
@@ -310,3 +310,186 @@ test('verifyPullRequestChangelog requires fragments for user-facing changes and
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion writes cumulative beta notes without mutating stable changelog artifacts', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-beta-notes');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
const changelogPath = path.join(projectRoot, 'CHANGELOG.md');
|
||||
const docsChangelogPath = path.join(projectRoot, 'docs-site', 'changelog.md');
|
||||
const existingChangelog = '# Changelog\n\n## v0.11.2 (2026-04-07)\n- Stable release.\n';
|
||||
const existingDocsChangelog = '# Changelog\n\n## v0.11.2 (2026-04-07)\n- Stable docs release.\n';
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.mkdirSync(path.join(projectRoot, 'docs-site'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-beta.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(changelogPath, existingChangelog, 'utf8');
|
||||
fs.writeFileSync(docsChangelogPath, existingDocsChangelog, 'utf8');
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Added prerelease coverage.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '002.md'),
|
||||
['type: fixed', 'area: launcher', '', '- Fixed prerelease packaging checks.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
const outputPath = writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-beta.1',
|
||||
});
|
||||
|
||||
assert.equal(outputPath, path.join(projectRoot, 'release', 'prerelease-notes.md'));
|
||||
assert.equal(
|
||||
fs.readFileSync(changelogPath, 'utf8'),
|
||||
existingChangelog,
|
||||
'stable CHANGELOG.md should remain unchanged',
|
||||
);
|
||||
assert.equal(
|
||||
fs.readFileSync(docsChangelogPath, 'utf8'),
|
||||
existingDocsChangelog,
|
||||
'docs-site changelog should remain unchanged',
|
||||
);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '001.md')), true);
|
||||
assert.equal(fs.existsSync(path.join(projectRoot, 'changes', '002.md')), true);
|
||||
|
||||
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
|
||||
assert.match(prereleaseNotes, /^> This is a prerelease build for testing\./m);
|
||||
assert.match(prereleaseNotes, /## Highlights\n### Added\n- Overlay: Added prerelease coverage\./);
|
||||
assert.match(
|
||||
prereleaseNotes,
|
||||
/### Fixed\n- Launcher: Fixed prerelease packaging checks\./,
|
||||
);
|
||||
assert.match(prereleaseNotes, /## Installation\n\nSee the README and docs\/installation guide/);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion supports rc prereleases', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-rc-notes');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-rc.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: changed', 'area: release', '', '- Prepared release candidate notes.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
const outputPath = writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-rc.1',
|
||||
});
|
||||
|
||||
const prereleaseNotes = fs.readFileSync(outputPath, 'utf8');
|
||||
assert.match(
|
||||
prereleaseNotes,
|
||||
/## Highlights\n### Changed\n- Release: Prepared release candidate notes\./,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion rejects unsupported prerelease identifiers', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-alpha-reject');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-alpha.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Unsupported alpha prerelease.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-alpha.1',
|
||||
}),
|
||||
/Unsupported prerelease version \(0\.11\.3-alpha\.1\)/,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion rejects mismatched package versions', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-version-mismatch');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-beta.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'changes', '001.md'),
|
||||
['type: added', 'area: overlay', '', '- Mismatched prerelease.'].join('\n'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-beta.2',
|
||||
}),
|
||||
/package\.json version \(0\.11\.3-beta\.1\) does not match requested release version \(0\.11\.3-beta\.2\)/,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('writePrereleaseNotesForVersion rejects empty prerelease note generation when no fragments exist', async () => {
|
||||
const { writePrereleaseNotesForVersion } = await loadModule();
|
||||
const workspace = createWorkspace('prerelease-no-fragments');
|
||||
const projectRoot = path.join(workspace, 'SubMiner');
|
||||
|
||||
fs.mkdirSync(path.join(projectRoot, 'changes'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(projectRoot, 'package.json'),
|
||||
JSON.stringify({ name: 'subminer', version: '0.11.3-beta.1' }, null, 2),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
writePrereleaseNotesForVersion({
|
||||
cwd: projectRoot,
|
||||
version: '0.11.3-beta.1',
|
||||
}),
|
||||
/No changelog fragments found in changes\//,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ type PullRequestChangelogOptions = {
|
||||
};
|
||||
|
||||
const RELEASE_NOTES_PATH = path.join('release', 'release-notes.md');
|
||||
const PRERELEASE_NOTES_PATH = path.join('release', 'prerelease-notes.md');
|
||||
const CHANGELOG_HEADER = '# Changelog';
|
||||
const CHANGE_TYPES: FragmentType[] = ['added', 'changed', 'fixed', 'docs', 'internal'];
|
||||
const CHANGE_TYPE_HEADINGS: Record<FragmentType, string> = {
|
||||
@@ -75,6 +76,10 @@ function resolveVersion(options: Pick<ChangelogOptions, 'cwd' | 'version' | 'dep
|
||||
return normalizeVersion(options.version ?? resolvePackageVersion(cwd, readFileSync));
|
||||
}
|
||||
|
||||
function isSupportedPrereleaseVersion(version: string): boolean {
|
||||
return /^\d+\.\d+\.\d+-(beta|rc)\.\d+$/u.test(normalizeVersion(version));
|
||||
}
|
||||
|
||||
function verifyRequestedVersionMatchesPackageVersion(
|
||||
options: Pick<ChangelogOptions, 'cwd' | 'version' | 'deps'>,
|
||||
): void {
|
||||
@@ -314,8 +319,15 @@ export function resolveChangelogOutputPaths(options?: { cwd?: string }): string[
|
||||
return [path.join(cwd, 'CHANGELOG.md')];
|
||||
}
|
||||
|
||||
function renderReleaseNotes(changes: string): string {
|
||||
function renderReleaseNotes(
|
||||
changes: string,
|
||||
options?: {
|
||||
disclaimer?: string;
|
||||
},
|
||||
): string {
|
||||
const prefix = options?.disclaimer ? [options.disclaimer, ''] : [];
|
||||
return [
|
||||
...prefix,
|
||||
'## Highlights',
|
||||
changes,
|
||||
'',
|
||||
@@ -334,13 +346,21 @@ function renderReleaseNotes(changes: string): string {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function writeReleaseNotesFile(cwd: string, changes: string, deps?: ChangelogFsDeps): string {
|
||||
function writeReleaseNotesFile(
|
||||
cwd: string,
|
||||
changes: string,
|
||||
deps?: ChangelogFsDeps,
|
||||
options?: {
|
||||
disclaimer?: string;
|
||||
outputPath?: string;
|
||||
},
|
||||
): string {
|
||||
const mkdirSync = deps?.mkdirSync ?? fs.mkdirSync;
|
||||
const writeFileSync = deps?.writeFileSync ?? fs.writeFileSync;
|
||||
const releaseNotesPath = path.join(cwd, RELEASE_NOTES_PATH);
|
||||
const releaseNotesPath = path.join(cwd, options?.outputPath ?? RELEASE_NOTES_PATH);
|
||||
|
||||
mkdirSync(path.dirname(releaseNotesPath), { recursive: true });
|
||||
writeFileSync(releaseNotesPath, renderReleaseNotes(changes), 'utf8');
|
||||
writeFileSync(releaseNotesPath, renderReleaseNotes(changes, options), 'utf8');
|
||||
return releaseNotesPath;
|
||||
}
|
||||
|
||||
@@ -613,6 +633,30 @@ export function writeReleaseNotesForVersion(options?: ChangelogOptions): string
|
||||
return writeReleaseNotesFile(cwd, changes, options?.deps);
|
||||
}
|
||||
|
||||
export function writePrereleaseNotesForVersion(options?: ChangelogOptions): string {
|
||||
verifyRequestedVersionMatchesPackageVersion(options ?? {});
|
||||
|
||||
const cwd = options?.cwd ?? process.cwd();
|
||||
const version = resolveVersion(options ?? {});
|
||||
if (!isSupportedPrereleaseVersion(version)) {
|
||||
throw new Error(
|
||||
`Unsupported prerelease version (${version}). Expected x.y.z-beta.N or x.y.z-rc.N.`,
|
||||
);
|
||||
}
|
||||
|
||||
const fragments = readChangeFragments(cwd, options?.deps);
|
||||
if (fragments.length === 0) {
|
||||
throw new Error('No changelog fragments found in changes/.');
|
||||
}
|
||||
|
||||
const changes = renderGroupedChanges(fragments);
|
||||
return writeReleaseNotesFile(cwd, changes, options?.deps, {
|
||||
disclaimer:
|
||||
'> This is a prerelease build for testing. Stable changelog and docs-site updates remain pending until the final stable release.',
|
||||
outputPath: PRERELEASE_NOTES_PATH,
|
||||
});
|
||||
}
|
||||
|
||||
function parseCliArgs(argv: string[]): {
|
||||
baseRef?: string;
|
||||
cwd?: string;
|
||||
@@ -710,6 +754,11 @@ function main(): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'prerelease-notes') {
|
||||
writePrereleaseNotesForVersion(options);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === 'docs') {
|
||||
generateDocsChangelog(options);
|
||||
return;
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
param(
|
||||
[ValidateSet('geometry')]
|
||||
[string]$Mode = 'geometry',
|
||||
[string]$SocketPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
try {
|
||||
Add-Type -TypeDefinition @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
public static class SubMinerWindowsHelper {
|
||||
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool GetWindowRect(IntPtr hWnd, out RECT rect);
|
||||
|
||||
[DllImport("dwmapi.dll")]
|
||||
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);
|
||||
}
|
||||
"@
|
||||
|
||||
$DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
||||
|
||||
function Get-WindowBounds {
|
||||
param([IntPtr]$hWnd)
|
||||
|
||||
$rect = New-Object SubMinerWindowsHelper+RECT
|
||||
$size = [System.Runtime.InteropServices.Marshal]::SizeOf($rect)
|
||||
$dwmResult = [SubMinerWindowsHelper]::DwmGetWindowAttribute(
|
||||
$hWnd,
|
||||
$DWMWA_EXTENDED_FRAME_BOUNDS,
|
||||
[ref]$rect,
|
||||
$size
|
||||
)
|
||||
|
||||
if ($dwmResult -ne 0) {
|
||||
if (-not [SubMinerWindowsHelper]::GetWindowRect($hWnd, [ref]$rect)) {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$width = $rect.Right - $rect.Left
|
||||
$height = $rect.Bottom - $rect.Top
|
||||
if ($width -le 0 -or $height -le 0) {
|
||||
return $null
|
||||
}
|
||||
|
||||
return [PSCustomObject]@{
|
||||
X = $rect.Left
|
||||
Y = $rect.Top
|
||||
Width = $width
|
||||
Height = $height
|
||||
Area = $width * $height
|
||||
}
|
||||
}
|
||||
|
||||
$commandLineByPid = @{}
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
foreach ($process in Get-CimInstance Win32_Process) {
|
||||
$commandLineByPid[[uint32]$process.ProcessId] = $process.CommandLine
|
||||
}
|
||||
}
|
||||
|
||||
$mpvMatches = New-Object System.Collections.Generic.List[object]
|
||||
$foregroundWindow = [SubMinerWindowsHelper]::GetForegroundWindow()
|
||||
$callback = [SubMinerWindowsHelper+EnumWindowsProc]{
|
||||
param([IntPtr]$hWnd, [IntPtr]$lParam)
|
||||
|
||||
if (-not [SubMinerWindowsHelper]::IsWindowVisible($hWnd)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ([SubMinerWindowsHelper]::IsIconic($hWnd)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
[uint32]$windowProcessId = 0
|
||||
[void][SubMinerWindowsHelper]::GetWindowThreadProcessId($hWnd, [ref]$windowProcessId)
|
||||
if ($windowProcessId -eq 0) {
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$process = Get-Process -Id $windowProcessId -ErrorAction Stop
|
||||
} catch {
|
||||
return $true
|
||||
}
|
||||
|
||||
if ($process.ProcessName -ine 'mpv') {
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($SocketPath)) {
|
||||
$commandLine = $commandLineByPid[[uint32]$windowProcessId]
|
||||
if ([string]::IsNullOrWhiteSpace($commandLine)) {
|
||||
return $true
|
||||
}
|
||||
if (
|
||||
($commandLine -notlike "*--input-ipc-server=$SocketPath*") -and
|
||||
($commandLine -notlike "*--input-ipc-server $SocketPath*")
|
||||
) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
$bounds = Get-WindowBounds -hWnd $hWnd
|
||||
if ($null -eq $bounds) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$mpvMatches.Add([PSCustomObject]@{
|
||||
HWnd = $hWnd
|
||||
X = $bounds.X
|
||||
Y = $bounds.Y
|
||||
Width = $bounds.Width
|
||||
Height = $bounds.Height
|
||||
Area = $bounds.Area
|
||||
IsForeground = ($foregroundWindow -ne [IntPtr]::Zero -and $hWnd -eq $foregroundWindow)
|
||||
})
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
[void][SubMinerWindowsHelper]::EnumWindows($callback, [IntPtr]::Zero)
|
||||
|
||||
$focusedMatch = $mpvMatches | Where-Object { $_.IsForeground } | Select-Object -First 1
|
||||
if ($null -ne $focusedMatch) {
|
||||
[Console]::Error.WriteLine('focus=focused')
|
||||
} else {
|
||||
[Console]::Error.WriteLine('focus=not-focused')
|
||||
}
|
||||
|
||||
if ($mpvMatches.Count -eq 0) {
|
||||
Write-Output 'not-found'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$bestMatch = if ($null -ne $focusedMatch) {
|
||||
$focusedMatch
|
||||
} else {
|
||||
$mpvMatches | Sort-Object -Property Area, Width, Height -Descending | Select-Object -First 1
|
||||
}
|
||||
Write-Output "$($bestMatch.X),$($bestMatch.Y),$($bestMatch.Width),$($bestMatch.Height)"
|
||||
} catch {
|
||||
[Console]::Error.WriteLine($_.Exception.Message)
|
||||
exit 1
|
||||
}
|
||||
@@ -28,6 +28,27 @@ USAGE
|
||||
force=0
|
||||
generate_webp=0
|
||||
input=""
|
||||
ffmpeg_bin="${FFMPEG_BIN:-ffmpeg}"
|
||||
|
||||
normalize_path() {
|
||||
local value="$1"
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
case "$value" in
|
||||
[A-Za-z]:\\* | [A-Za-z]:/*)
|
||||
cygpath -u "$value"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ "$value" =~ ^([A-Za-z]):[\\/](.*)$ ]]; then
|
||||
local drive="${BASH_REMATCH[1],,}"
|
||||
local rest="${BASH_REMATCH[2]}"
|
||||
rest="${rest//\\//}"
|
||||
printf '/mnt/%s/%s\n' "$drive" "$rest"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -63,9 +84,19 @@ if [[ -z "$input" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ffmpeg > /dev/null 2>&1; then
|
||||
echo "Error: ffmpeg is not installed or not in PATH." >&2
|
||||
exit 1
|
||||
input="$(normalize_path "$input")"
|
||||
ffmpeg_bin="$(normalize_path "$ffmpeg_bin")"
|
||||
|
||||
if [[ "$ffmpeg_bin" == */* ]]; then
|
||||
if [[ ! -x "$ffmpeg_bin" ]]; then
|
||||
echo "Error: ffmpeg binary is not executable: $ffmpeg_bin" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v "$ffmpeg_bin" > /dev/null 2>&1; then
|
||||
echo "Error: ffmpeg is not installed or not in PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -f "$input" ]]; then
|
||||
@@ -102,7 +133,7 @@ fi
|
||||
|
||||
has_encoder() {
|
||||
local encoder="$1"
|
||||
ffmpeg -hide_banner -encoders 2> /dev/null | awk -v encoder="$encoder" '$2 == encoder { found = 1 } END { exit(found ? 0 : 1) }'
|
||||
"$ffmpeg_bin" -hide_banner -encoders 2> /dev/null | awk -v encoder="$encoder" '$2 == encoder { found = 1 } END { exit(found ? 0 : 1) }'
|
||||
}
|
||||
|
||||
pick_webp_encoder() {
|
||||
@@ -123,7 +154,7 @@ webm_vf="${crop_vf},fps=30"
|
||||
echo "Generating MP4: $mp4_out"
|
||||
if has_encoder "h264_nvenc"; then
|
||||
echo "Trying GPU encoder for MP4: h264_nvenc"
|
||||
if ffmpeg "$overwrite_flag" -i "$input" \
|
||||
if "$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "$crop_vf" \
|
||||
-c:v h264_nvenc -preset p6 -rc:v vbr -cq:v 20 -b:v 0 \
|
||||
-pix_fmt yuv420p -movflags +faststart \
|
||||
@@ -132,7 +163,7 @@ if has_encoder "h264_nvenc"; then
|
||||
:
|
||||
else
|
||||
echo "GPU MP4 encode failed; retrying with CPU encoder: libx264"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
"$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "$crop_vf" \
|
||||
-c:v libx264 -preset slow -crf 20 \
|
||||
-profile:v high -level 4.1 -pix_fmt yuv420p \
|
||||
@@ -142,7 +173,7 @@ if has_encoder "h264_nvenc"; then
|
||||
fi
|
||||
else
|
||||
echo "Using CPU encoder for MP4: libx264"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
"$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "$crop_vf" \
|
||||
-c:v libx264 -preset slow -crf 20 \
|
||||
-profile:v high -level 4.1 -pix_fmt yuv420p \
|
||||
@@ -154,7 +185,7 @@ fi
|
||||
echo "Generating WebM: $webm_out"
|
||||
if has_encoder "av1_nvenc"; then
|
||||
echo "Trying GPU encoder for WebM: av1_nvenc"
|
||||
if ffmpeg "$overwrite_flag" -i "$input" \
|
||||
if "$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "$webm_vf" \
|
||||
-c:v av1_nvenc -preset p6 -cq:v 34 -b:v 0 \
|
||||
-c:a libopus -b:a 96k \
|
||||
@@ -162,7 +193,7 @@ if has_encoder "av1_nvenc"; then
|
||||
:
|
||||
else
|
||||
echo "GPU WebM encode failed; retrying with CPU encoder: libvpx-vp9"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
"$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "$webm_vf" \
|
||||
-c:v libvpx-vp9 -crf 34 -b:v 0 \
|
||||
-row-mt 1 -threads 8 \
|
||||
@@ -171,7 +202,7 @@ if has_encoder "av1_nvenc"; then
|
||||
fi
|
||||
else
|
||||
echo "Using CPU encoder for WebM: libvpx-vp9"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
"$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "$webm_vf" \
|
||||
-c:v libvpx-vp9 -crf 34 -b:v 0 \
|
||||
-row-mt 1 -threads 8 \
|
||||
@@ -185,7 +216,7 @@ if [[ "$generate_webp" -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "Generating animated WebP with $webp_encoder: $webp_out"
|
||||
ffmpeg "$overwrite_flag" -i "$input" \
|
||||
"$ffmpeg_bin" "$overwrite_flag" -i "$input" \
|
||||
-vf "${crop_vf},fps=24,scale=960:-1:flags=lanczos" \
|
||||
-c:v "$webp_encoder" \
|
||||
-q:v 80 \
|
||||
@@ -195,7 +226,7 @@ if [[ "$generate_webp" -eq 1 ]]; then
|
||||
fi
|
||||
|
||||
echo "Generating poster: $poster_out"
|
||||
ffmpeg "$overwrite_flag" -ss 00:00:05 -i "$input" \
|
||||
"$ffmpeg_bin" "$overwrite_flag" -ss 00:00:05 -i "$input" \
|
||||
-vf "$crop_vf" \
|
||||
-vframes 1 \
|
||||
-q:v 2 \
|
||||
|
||||
@@ -19,11 +19,33 @@ function writeExecutable(filePath: string, contents: string): void {
|
||||
fs.chmodSync(filePath, 0o755);
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
||||
}
|
||||
|
||||
function toBashPath(filePath: string): string {
|
||||
if (process.platform !== 'win32') return filePath;
|
||||
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
||||
if (!match) return normalized;
|
||||
|
||||
const drive = match[1]!;
|
||||
const rest = match[2]!;
|
||||
const probe = spawnSync('bash', ['-c', 'uname -s'], { encoding: 'utf8' });
|
||||
if (probe.status === 0 && /linux/i.test(probe.stdout)) {
|
||||
return `/mnt/${drive.toLowerCase()}/${rest}`;
|
||||
}
|
||||
|
||||
return `${drive.toUpperCase()}:/${rest}`;
|
||||
}
|
||||
|
||||
test('mkv-to-readme-video accepts libwebp_anim when libwebp is unavailable', () => {
|
||||
withTempDir((root) => {
|
||||
const binDir = path.join(root, 'bin');
|
||||
const inputPath = path.join(root, 'sample.mkv');
|
||||
const ffmpegLogPath = path.join(root, 'ffmpeg-args.log');
|
||||
const ffmpegLogPathBash = toBashPath(ffmpegLogPath);
|
||||
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
fs.writeFileSync(inputPath, 'fake-video', 'utf8');
|
||||
@@ -44,22 +66,33 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\\n' "$*" >> "${ffmpegLogPath}"
|
||||
if [[ "$#" -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '%s\\n' "$*" >> "${ffmpegLogPathBash}"
|
||||
output=""
|
||||
for arg in "$@"; do
|
||||
output="$arg"
|
||||
done
|
||||
if [[ -z "$output" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p "$(dirname "$output")"
|
||||
touch "$output"
|
||||
`,
|
||||
);
|
||||
|
||||
const result = spawnSync('bash', ['scripts/mkv-to-readme-video.sh', '--webp', inputPath], {
|
||||
const ffmpegShimPath = toBashPath(path.join(binDir, 'ffmpeg'));
|
||||
const ffmpegShimDir = toBashPath(binDir);
|
||||
const inputBashPath = toBashPath(inputPath);
|
||||
const command = [
|
||||
`chmod +x ${shellQuote(ffmpegShimPath)}`,
|
||||
`PATH=${shellQuote(`${ffmpegShimDir}:`)}"$PATH"`,
|
||||
`scripts/mkv-to-readme-video.sh --webp ${shellQuote(inputBashPath)}`,
|
||||
].join('; ');
|
||||
const result = spawnSync('bash', ['-lc', command], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH || ''}`,
|
||||
},
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,6 @@ const repoRoot = path.resolve(scriptDir, '..');
|
||||
const rendererSourceDir = path.join(repoRoot, 'src', 'renderer');
|
||||
const rendererOutputDir = path.join(repoRoot, 'dist', 'renderer');
|
||||
const scriptsOutputDir = path.join(repoRoot, 'dist', 'scripts');
|
||||
const windowsHelperSourcePath = path.join(scriptDir, 'get-mpv-window-windows.ps1');
|
||||
const windowsHelperOutputPath = path.join(scriptsOutputDir, 'get-mpv-window-windows.ps1');
|
||||
const macosHelperSourcePath = path.join(scriptDir, 'get-mpv-window-macos.swift');
|
||||
const macosHelperBinaryPath = path.join(scriptsOutputDir, 'get-mpv-window-macos');
|
||||
const macosHelperSourceCopyPath = path.join(scriptsOutputDir, 'get-mpv-window-macos.swift');
|
||||
@@ -33,11 +31,6 @@ function copyRendererAssets() {
|
||||
process.stdout.write(`Staged renderer assets in ${rendererOutputDir}\n`);
|
||||
}
|
||||
|
||||
function stageWindowsHelper() {
|
||||
copyFile(windowsHelperSourcePath, windowsHelperOutputPath);
|
||||
process.stdout.write(`Staged Windows helper: ${windowsHelperOutputPath}\n`);
|
||||
}
|
||||
|
||||
function fallbackToMacosSource() {
|
||||
copyFile(macosHelperSourcePath, macosHelperSourceCopyPath);
|
||||
process.stdout.write(`Staged macOS helper source fallback: ${macosHelperSourceCopyPath}\n`);
|
||||
@@ -77,7 +70,6 @@ function buildMacosHelper() {
|
||||
|
||||
function main() {
|
||||
copyRendererAssets();
|
||||
stageWindowsHelper();
|
||||
buildMacosHelper();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,26 @@ appimage=
|
||||
wrapper=
|
||||
assets=
|
||||
|
||||
normalize_path() {
|
||||
local value="$1"
|
||||
if command -v cygpath >/dev/null 2>&1; then
|
||||
case "$value" in
|
||||
[A-Za-z]:\\* | [A-Za-z]:/*)
|
||||
cygpath -u "$value"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ "$value" =~ ^([A-Za-z]):[\\/](.*)$ ]]; then
|
||||
local drive="${BASH_REMATCH[1],,}"
|
||||
local rest="${BASH_REMATCH[2]}"
|
||||
rest="${rest//\\//}"
|
||||
printf '/mnt/%s/%s\n' "$drive" "$rest"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--pkg-dir)
|
||||
@@ -53,6 +73,10 @@ if [[ -z "$pkg_dir" || -z "$version" || -z "$appimage" || -z "$wrapper" || -z "$
|
||||
fi
|
||||
|
||||
version="${version#v}"
|
||||
pkg_dir="$(normalize_path "$pkg_dir")"
|
||||
appimage="$(normalize_path "$appimage")"
|
||||
wrapper="$(normalize_path "$wrapper")"
|
||||
assets="$(normalize_path "$assets")"
|
||||
pkgbuild="${pkg_dir}/PKGBUILD"
|
||||
srcinfo="${pkg_dir}/.SRCINFO"
|
||||
|
||||
@@ -82,6 +106,9 @@ awk \
|
||||
found_pkgver = 0
|
||||
found_sha_block = 0
|
||||
}
|
||||
{
|
||||
sub(/\r$/, "")
|
||||
}
|
||||
/^pkgver=/ {
|
||||
print "pkgver=" version
|
||||
found_pkgver = 1
|
||||
@@ -140,6 +167,9 @@ awk \
|
||||
found_source_wrapper = 0
|
||||
found_source_assets = 0
|
||||
}
|
||||
{
|
||||
sub(/\r$/, "")
|
||||
}
|
||||
/^\tpkgver = / {
|
||||
print "\tpkgver = " version
|
||||
found_pkgver = 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -9,6 +10,23 @@ function createWorkspace(name: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), `${name}-`));
|
||||
}
|
||||
|
||||
function toBashPath(filePath: string): string {
|
||||
if (process.platform !== 'win32') return filePath;
|
||||
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const match = normalized.match(/^([A-Za-z]):\/(.*)$/);
|
||||
if (!match) return normalized;
|
||||
|
||||
const drive = match[1]!;
|
||||
const rest = match[2]!;
|
||||
const probe = spawnSync('bash', ['-c', 'uname -s'], { encoding: 'utf8' });
|
||||
if (probe.status === 0 && /linux/i.test(probe.stdout)) {
|
||||
return `/mnt/${drive.toLowerCase()}/${rest}`;
|
||||
}
|
||||
|
||||
return `${drive.toUpperCase()}:/${rest}`;
|
||||
}
|
||||
|
||||
test('update-aur-package updates PKGBUILD and .SRCINFO without makepkg', () => {
|
||||
const workspace = createWorkspace('subminer-aur-package');
|
||||
const pkgDir = path.join(workspace, 'aur-subminer-bin');
|
||||
@@ -29,15 +47,15 @@ test('update-aur-package updates PKGBUILD and .SRCINFO without makepkg', () => {
|
||||
[
|
||||
'scripts/update-aur-package.sh',
|
||||
'--pkg-dir',
|
||||
pkgDir,
|
||||
toBashPath(pkgDir),
|
||||
'--version',
|
||||
'v0.6.3',
|
||||
'--appimage',
|
||||
appImagePath,
|
||||
toBashPath(appImagePath),
|
||||
'--wrapper',
|
||||
wrapperPath,
|
||||
toBashPath(wrapperPath),
|
||||
'--assets',
|
||||
assetsPath,
|
||||
toBashPath(assetsPath),
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
@@ -47,8 +65,8 @@ test('update-aur-package updates PKGBUILD and .SRCINFO without makepkg', () => {
|
||||
|
||||
const pkgbuild = fs.readFileSync(path.join(pkgDir, 'PKGBUILD'), 'utf8');
|
||||
const srcinfo = fs.readFileSync(path.join(pkgDir, '.SRCINFO'), 'utf8');
|
||||
const expectedSums = [appImagePath, wrapperPath, assetsPath].map(
|
||||
(filePath) => execFileSync('sha256sum', [filePath], { encoding: 'utf8' }).split(/\s+/)[0],
|
||||
const expectedSums = [appImagePath, wrapperPath, assetsPath].map((filePath) =>
|
||||
crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'),
|
||||
);
|
||||
|
||||
assert.match(pkgbuild, /^pkgver=0\.6\.3$/m);
|
||||
|
||||
Reference in New Issue
Block a user