Files
Satsuma/Install.ps1
T
2026-10-22 23:04:57 +00:00

248 lines
11 KiB
PowerShell

# Satsuma installer. Run it with Install.bat.
# Adds a "Convert to (Satsuma)" entry to the right-click menu. Nothing needs admin rights.
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Join-Path $env:LOCALAPPDATA 'Satsuma'
$bin = Join-Path $root 'bin'
$tmp = Join-Path $env:TEMP ('satsuma_setup_' + [guid]::NewGuid().ToString('N'))
function Say([string]$text) { Write-Host $text }
function Good([string]$text) { Write-Host $text -ForegroundColor Green }
function Warn([string]$text) { Write-Host $text -ForegroundColor Yellow }
function Set-Reg([string]$subKey, [string]$name, [string]$value) {
$k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey($subKey)
$k.SetValue($name, $value)
$k.Close()
}
function Remove-Reg([string]$subKey) {
try { [Microsoft.Win32.Registry]::CurrentUser.DeleteSubKeyTree($subKey, $false) } catch { }
}
function Find-Magick {
$dir = Join-Path $bin 'magick'
if (Test-Path -LiteralPath $dir) {
$f = Get-ChildItem -LiteralPath $dir -Filter 'magick.exe' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($f) { return $f.FullName }
}
foreach ($base in @($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
if ($base -and (Test-Path -LiteralPath $base)) {
$d = Get-ChildItem -LiteralPath $base -Filter 'ImageMagick*' -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1
if ($d) {
$m = Join-Path $d.FullName 'magick.exe'
if (Test-Path -LiteralPath $m) { return $m }
}
}
}
return $null
}
Write-Host ''
Write-Host ' Satsuma setup' -ForegroundColor Yellow
Write-Host ' Right-click file converter for Windows'
Write-Host ''
try {
[void](New-Item -ItemType Directory -Force -Path $root)
[void](New-Item -ItemType Directory -Force -Path $bin)
[void](New-Item -ItemType Directory -Force -Path $tmp)
# ---- 1. copy the program files ----
Say 'Step 1 of 4: copying Satsuma files...'
foreach ($f in @('Convert.ps1', 'Run.vbs', 'satsuma.ico')) {
$from = Join-Path $here $f
if (-not (Test-Path -LiteralPath $from)) { throw ('A file is missing next to the installer: ' + $f + '. Unzip the whole folder first.') }
$to = Join-Path $root $f
Copy-Item -LiteralPath $from -Destination $to -Force
try { Unblock-File -LiteralPath $to } catch { }
}
Good ' done'
# ---- 2. ffmpeg (video, audio, webp) ----
$ffmpegExe = Join-Path $bin 'ffmpeg.exe'
if (Test-Path -LiteralPath $ffmpegExe) {
Say 'Step 2 of 4: ffmpeg is already here.'
} else {
Say 'Step 2 of 4: downloading ffmpeg (about 90 MB, this can take a minute)...'
$zip = Join-Path $tmp 'ffmpeg.zip'
$urls = @(
'https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip',
'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip'
)
$got = $false
foreach ($u in $urls) {
try {
Invoke-WebRequest -Uri $u -OutFile $zip -UseBasicParsing
$got = $true
break
} catch {
Warn (' that source failed, trying the next one: ' + $_.Exception.Message)
}
}
if (-not $got) { throw 'Could not download ffmpeg. Check the internet connection and run Install.bat again.' }
$x = Join-Path $tmp 'ffmpeg_x'
Expand-Archive -LiteralPath $zip -DestinationPath $x -Force
$found = Get-ChildItem -LiteralPath $x -Filter 'ffmpeg.exe' -Recurse | Select-Object -First 1
if (-not $found) { throw 'ffmpeg.exe was not inside the download.' }
Copy-Item -LiteralPath $found.FullName -Destination $ffmpegExe -Force
Good ' done'
}
try {
$v = (& $ffmpegExe -version 2>&1 | Select-Object -First 1) | Out-String
Say (' ' + $v.Trim())
} catch {
Warn ' ffmpeg was installed but did not start. Video and audio may not work.'
}
# ---- 3. ImageMagick (HEIC, WebP, PNG, JPG) ----
$magickPath = Find-Magick
if ($magickPath) {
Say 'Step 3 of 4: ImageMagick is already here.'
} else {
Say 'Step 3 of 4: downloading ImageMagick (for HEIC and other pictures)...'
$base = 'https://imagemagick.org/archive/binaries/'
$name = $null
try {
$html = (Invoke-WebRequest -Uri $base -UseBasicParsing).Content
$best = $null
$bestKey = $null
foreach ($m in [regex]::Matches($html, 'ImageMagick-(\d+)\.(\d+)\.(\d+)-(\d+)-portable-Q16-HDRI-x64\.7z')) {
$key = [version]('{0}.{1}.{2}.{3}' -f $m.Groups[1].Value, $m.Groups[2].Value, $m.Groups[3].Value, $m.Groups[4].Value)
if (($null -eq $bestKey) -or ($key -gt $bestKey)) { $bestKey = $key; $best = $m.Value }
}
$name = $best
} catch { }
if (-not $name) { $name = 'ImageMagick-7.1.2-25-portable-Q16-HDRI-x64.7z' }
$ok = $false
try {
$arc = Join-Path $tmp 'magick.7z'
$z = Join-Path $tmp '7zr.exe'
Invoke-WebRequest -Uri ($base + $name) -OutFile $arc -UseBasicParsing
Invoke-WebRequest -Uri 'https://www.7-zip.org/a/7zr.exe' -OutFile $z -UseBasicParsing
$dest = Join-Path $bin 'magick'
[void](New-Item -ItemType Directory -Force -Path $dest)
& $z x $arc ('-o' + $dest) -y | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'The 7-Zip extractor reported an error.' }
$ok = $true
} catch {
Warn (' portable download did not work: ' + $_.Exception.Message)
}
if (-not $ok) {
Say ' trying winget instead (Windows may ask for permission)...'
try {
$wg = Get-Command winget.exe -ErrorAction SilentlyContinue
if ($wg) {
& $wg.Source install --id ImageMagick.ImageMagick -e --silent --accept-package-agreements --accept-source-agreements
}
} catch { }
}
$magickPath = Find-Magick
if ($magickPath) { Good ' done' }
else { Warn ' ImageMagick could not be installed. Pictures will not convert, but video, audio and documents still can.' }
}
if ($magickPath) {
try {
$fmt = & $magickPath -list format 2>&1 | Out-String
if ($fmt -match '(?m)^\s*HEIC\b') { Good ' HEIC pictures are supported.' }
else { Warn ' This ImageMagick build does not list HEIC support, so iPhone pictures may fail.' }
} catch {
Warn ' Could not check HEIC support.'
}
}
# ---- 4. the right-click menu ----
Say 'Step 4 of 4: adding the right-click menu...'
$iconPath = Join-Path $root 'satsuma.ico'
$runVbs = Join-Path $root 'Run.vbs'
$video = @(
@('mp4', 'MP4'), @('mkv', 'MKV'), @('webm', 'WebM'), @('avi', 'AVI'),
@('mov', 'MOV'), @('wmv', 'WMV'), @('flv', 'FLV'), @('mpg', 'MPG')
)
$audio = @(
@('mp3', 'MP3'), @('wav', 'WAV'), @('flac', 'FLAC'),
@('ogg', 'OGG'), @('m4a', 'M4A'), @('opus', 'Opus')
)
$image = @(
@('jpg', 'JPG'), @('png', 'PNG'), @('webp', 'WebP'),
@('bmp', 'BMP'), @('tiff', 'TIFF'), @('pdf', 'PDF')
)
$same = @{ 'm4v' = 'mp4'; 'mpeg' = 'mpg'; 'jpeg' = 'jpg'; 'tif' = 'tiff'; 'heif' = 'heic' }
$groups = @(
@{ Exts = @('mp4', 'm4v', 'mkv', 'webm', 'avi', 'mov', 'wmv', 'flv', 'mpg', 'mpeg', '3gp', 'ogv'); Items = ($video + @(, @('mp3', 'MP3 (sound only)'))) },
@{ Exts = @('mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'opus', 'wma'); Items = $audio },
@{ Exts = @('heic', 'heif', 'webp', 'png', 'jpg', 'jpeg', 'bmp', 'tif', 'tiff'); Items = $image },
@{ Exts = @('gif'); Items = ($image + @(, @('mp4', 'MP4 video'))) },
@{ Exts = @('doc', 'docx', 'rtf', 'txt', 'odt', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'odp'); Items = @(, @('pdf', 'PDF')) }
)
$count = 0
foreach ($g in $groups) {
foreach ($ext in $g.Exts) {
$menuKey = 'Software\Classes\SystemFileAssociations\.' + $ext + '\shell\Satsuma'
Remove-Reg $menuKey
$mine = $ext
if ($same.ContainsKey($ext)) { $mine = $same[$ext] }
Set-Reg $menuKey 'MUIVerb' 'Convert to (Satsuma)'
Set-Reg $menuKey 'SubCommands' ''
Set-Reg $menuKey 'Icon' $iconPath
$n = 0
foreach ($item in $g.Items) {
$t = $item[0]
$label = $item[1]
if ($t -eq $mine) { continue }
$n++
$verbKey = '{0}\shell\{1:D2}_{2}' -f $menuKey, $n, $t
Set-Reg $verbKey '' $label
Set-Reg ($verbKey + '\command') '' ('wscript.exe "{0}" {1} "%1"' -f $runVbs, $t)
}
$count++
}
}
Good (' done (' + $count + ' file types)')
# ---- Windows 11 note ----
$build = 0
try { $build = [int](Get-CimInstance Win32_OperatingSystem).BuildNumber } catch { }
if ($build -ge 22000) {
$clsid = 'Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32'
$existing = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($clsid)
if ($null -eq $existing) {
Write-Host ''
Warn 'Windows 11 hides this menu under "Show more options" in the right-click menu.'
$a = Read-Host 'Turn on the classic full right-click menu so Satsuma shows right away? (Y/N)'
if ($a -match '^[Yy]') {
Set-Reg $clsid '' ''
Set-Content -LiteralPath (Join-Path $root 'classic-menu.txt') -Value 'set by Satsuma'
Good ' classic menu turned on. Sign out and back in (or restart) to see it.'
}
} else {
$existing.Close()
}
}
Write-Host ''
Good 'All set.'
Say 'Right-click a picture, video, sound or document file and look for "Convert to (Satsuma)".'
Say 'The converted file lands next to the original. The original is never touched.'
}
catch {
Write-Host ''
Write-Host ('Setup stopped: ' + $_.Exception.Message) -ForegroundColor Red
}
finally {
if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Recurse -Force -ErrorAction SilentlyContinue }
}