Skip to content

Instantly share code, notes, and snippets.

@milnak
Created February 15, 2026 18:31
Show Gist options
  • Select an option

  • Save milnak/a0d60cab81ee3ac0aa1aeb2e6cc33089 to your computer and use it in GitHub Desktop.

Select an option

Save milnak/a0d60cab81ee3ac0aa1aeb2e6cc33089 to your computer and use it in GitHub Desktop.
My PowerShell $PROFILE
<#
.SYNOPSIS
Download best quality video and audio into default container.
.NOTES
yt-dlp now supports preset aliases: '-f mp3', '-f aac', '-t mp4', '-t mkv'
#>
function Invoke-YtDlp {
param(
[Parameter(Mandatory = $true, ParameterSetName = 'Audio')]
[switch]$Audio,
[Parameter(Mandatory = $true, ParameterSetName = 'Video')]
[switch]$Video,
[Parameter(Mandatory = $true)]
[string]$Uri,
# Audio parameters
[Parameter(Mandatory = $true, ParameterSetName = 'Audio')]
[ValidateSet('best', 'aac', 'alac', 'flac', 'm4a', 'mp3', 'opus', 'vorbis', 'wav')]
[string]$Format
)
# If the Uri is a Facebook redirect, then parse the redirect query string to get the actual URL
if (([Uri]$Uri).Host -like '*.facebook.com') {
$Uri = [Web.HttpUtility]::ParseQueryString([Uri]([Web.HttpUtility]::UrlDecode($Uri)))[0]
Write-Warning "Facebook Uri redirects to: $Uri"
}
# Common args
$ytdlp_args = `
'--windows-filenames', `
'--ignore-config', `
'--progress', `
'--no-simulate', `
# '--progress-template', '"download:[download] %(progress.downloaded_bytes)s/%(progress.total_bytes)s ETA:%(progress.eta)s"', `
'--output-na-placeholder', 'NA',
'--no-playlist'
if ($Audio) {
$ytdlp_args += `
'--extract-audio', `
'--audio-format', $Format
}
elseif ($Video) {
# '--format', 'bestvideo[ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio/best[ext=mp4]/best'
$ytdlp_args += `
'--format', '"bv*+ba"', '--embed-metadata'
}
# Cookies (if file exists)
$cookieFile = Resolve-Path -LiteralPath '~/cookies.txt' -ErrorAction SilentlyContinue
if ($cookieFile) {
Write-Warning "Using cookies file $cookieFile"
$ytdlp_args += '--cookies', """$cookieFile"""
}
if ($Uri -like '*list=*') {
# Playlist arguments
$response = Read-Host "Playlist detected - are you sure? ('`e[1myes`e[0m' to confirm)"
if ($response -ne 'yes') {
return
}
# To get name of playlist:
# yt-dlp.exe --no-warnings --playlist-start 1 --playlist-end 1 --print '%(channel)s/%(playlist_title)s' $Uri
$ytdlp_args += `
'--print', '"[%(playlist_index)d/%(n_entries+1)d] %(title).200s"', `
'--output', '"%(channel)s/%(playlist_title)s/%(title).200s [%(id)s].%(ext)s"'
}
else {
# Single file download arguments
$ytdlp_args += `
'--print', '"%(title).200s [%(id)s]"' , `
'--output', '"%(title).200s [%(id)s].%(ext)s"'
}
$ytdlp_args += """$Uri"""
Write-Verbose ('Arguments: {0}' -f ($ytdlp_args -join ' '))
Start-Process -FilePath 'yt-dlp.exe' -ArgumentList $ytdlp_args -NoNewWindow -Wait
}
<#
.SYNOPSIS
Download file (including torrents) using ARIA
#>
function Invoke-Aria {
param([Parameter(Mandatory)] [string]$Source)
# Can also use "aria2.conf" file.
$ariaArgs = @()
## Basic Options
$ariaArgs += `
'--continue=true', `
'--max-concurrent-downloads=5'
## HTTP/FTP Options
$ariaArgs += `
'--max-connection-per-server=4', `
'--max-tries=50', `
'--min-split-size=20M', `
'--retry-wait=30', `
'--split=4'
## BitTorrent Specific Options
$ariaArgs += `
'--enable-dht=true', `
'--seed-time=0'
## Advanced Options
$ariaArgs += `
'--allow-piece-length-change=true', `
'--console-log-level=warn', `
'--disk-cache=32M', `
'--disable-ipv6=true', `
'--file-allocation=none', `
'--summary-interval=120'
## URI/MAGNET/TORRENT_FILE/METALINK_FILE
$ariaArgs += `
$Source
Start-Process -FilePath 'aria2c.exe' -ArgumentList $ariaArgs -NoNewWindow -Wait
}
<#
.SYNOPSIS
Download binaries by extension from a web page.
#>
function Get-WebPageBinaries {
[CmdletBinding()]
param(
# Uri of page to download from.
[Parameter(Mandatory)] [uri]$Uri,
# Extensions to download
[string[]]$Extensions = @('htm', 'html', 'zip', 'pdf', 'mp3', 'mid'),
# Maximum recursion depth, 0=infinite
[int]$Depth = 1
)
wget.exe `
--verbose `
--no-parent `
--recursive `
--level=$Depth `
--continue `
--timestamping `
--execute robots=off `
--accept=$($Extensions -join ',') `
--user-agent='Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/52.0.2725.0 Mobile/13B143 Safari/601.1.46' `
$Uri
}
<#
.SYNOPSIS
Creates a temporary folder and cds into it.
Outputs the path to allow for assigning to a variable.
#>
function mdcdtemp {
$tempPath = [IO.Path]::Combine([IO.Path]::GetTempPath(), [IO.Path]::GetRandomFileName())
New-Item -Path $tempPath -ItemType Directory -Force | Out-Null
Push-Location -LiteralPath $tempPath
$tempPath
}
<#
.SYNOPSIS
Moves an item to the recycle bin.
.EXAMPLE
Get-Item * |Remove-ItemToRecycleBin -Verbose -Confirm
#>
function Remove-ItemToRecycleBin {
[CmdletBinding(SupportsShouldProcess)]
Param(
[Parameter(Mandatory, ValueFromPipeline)][ValidateNotNullOrEmpty()]
[string[]]$Paths
)
begin {
$shell = New-Object -ComObject 'Shell.Application'
}
process {
foreach ($path in $Paths) {
if (Test-Path -LiteralPath $path) {
if ($PSCmdlet.ShouldProcess($path, 'Move to Recycle Bin')) {
Write-Verbose "Removing to Recycle Bin: $path"
$item = Get-Item -LiteralPath $path
$directoryPath = Split-Path -Path $item -Parent
$shell.Namespace($directoryPath).ParseName($item.Name).InvokeVerb('delete')
}
}
else {
Write-Warning "Path not found: $path"
}
}
}
end {}
}
<#
.SYNOPSIS
Removes a folder recursively.
.DESCRIPTION
Force removes a folder recursively, prompting first.
#>
function rmrf {
param([Parameter(Mandatory = $true)] [string]$Path)
if (Test-Path -LiteralPath $Path -PathType Container) {
$response = Read-Host "Really remove '$PATH'? ('`e[1myes`e[0m' to confirm)"
if ($response -eq 'yes') {
Remove-Item -Force -Recurse -LiteralPath $Path
}
}
else {
Write-Warning "Path not found or not folder."
}
}
<#
.SYNOPSIS
Recursive file find.
.DESCRIPTION
By default will return all files starting in current folder tree.
#>
function rff {
param(
[Parameter(Position = 0)] [string]$Filter = '*',
[Parameter(Position = 1)] [string]$Path = '.'
)
Get-ChildItem -Recurse -File -Filter $Filter -LiteralPath $Path | Select-Object -ExpandProperty FullName
}
<#
.SYNOPSIS
Recursive Grep
.DESCRIPTION
By default will start in current folder
#>
function rgrep {
param(
[Parameter(Position = 0, Mandatory = $true)] [string]$Pattern,
[Parameter(Position = 1)] [string]$Files = '.'
)
Get-ChildItem -Recurse -File -Filter $Files | Select-String -Pattern $Pattern
}
<#
.SYNOPSIS
Similar to Get-FileHash but also returns Base64 encoded value in 'HashBase64' field.
#>
function Get-FileHashBase64 {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]]$Path,
[ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MD5', IgnoreCase)]
[string]$Algorithm = 'SHA256'
)
begin {
switch ($Algorithm) {
'SHA1' { $algorithmId = 'Security.Cryptography.SHA1Managed' }
'SHA256' { $algorithmId = 'Security.Cryptography.SHA256Managed' }
'SHA384' { $algorithmId = 'Security.Cryptography.SHA384Managed' }
'SHA512' { $algorithmId = 'Security.Cryptography.SHA512Managed' }
'MD5' { $algorithmId = 'Security.Cryptography.MD5CryptoServiceProvider' }
}
Write-Verbose "Get-FileHashBase64 begin : $algorithmId"
}
process {
foreach ($file in $Path) {
$resolvedFile = Resolve-Path $File
Write-Verbose "Get-FileHashBase64 process $resolvedFile"
$hash = (New-Object -TypeName $algorithmId).ComputeHash([IO.File]::OpenRead($resolvedFile))
[PSCustomObject]@{
'Algorithm' = $Algorithm
'Hash' = [BitConverter]::ToString($hash) -replace '-', ''
'HashBase64' = [Convert]::ToBase64String($hash)
'Path' = $resolvedFile
}
}
}
end {
Write-Verbose 'Get-FileHashBase64 end'
}
}
<#
.SYNOPSIS
Use sysinternals "du" to show child folder sizes.
#>
function Invoke-DU {
param ([string]$Path = '.')
du.exe -nobanner -c -l 1 $Path
| ConvertFrom-Csv
| Sort-Object -Descending { [uint64]$_.DirectorySizeOnDisk }
| Select-Object -First 15 @{ Name = 'Size'; Expression = { '{0,15:N0}' -f [uint64]$_.DirectorySizeOnDisk } }, Path
}
<#
.SYNOPSIS
Take ownership of a file or folder.
.NOTES
takeown.exe and icacls.exe (both included in Windows) need to be in $env:PATH
If a folder is specified, all files and subfolders of that folder will change ownership.
.EXAMPLE
Set-SelfOwnership 'd:\temp\v.ps1'
Set-SelfOwnership -Confirm 'd:\temp\v.ps1'
Get-ChildItem 'd:\temp\g*' | Set-SelfOwnership -WhatIf -Verbose
#>
function Set-SelfOwnership {
# Support -Confirm, -WhatIf
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('FullName')]
[string[]]$Path
)
begin {
Write-Verbose 'Set-SelfOwnership begin'
# Ensure admin
If (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Admin privileges required'
}
# Check for required apps.
'takeown', 'icacls' | ForEach-Object {
Get-Command -Name "$_.exe" -CommandType Application -ErrorAction Stop | Out-Null
}
}
process {
foreach ($item in $Path) {
Write-Verbose "Taking ownership of $item"
$ResolvedPath = Resolve-Path -LiteralPath $Path -ErrorAction Stop
if ($PSCmdlet.ShouldProcess($ResolvedPath, 'Take Ownership')) {
if (Test-Path -LiteralPath $ResolvedPath -PathType Container) {
takeown.exe /f $ResolvedPath /r /d y
if ($LASTEXITCODE -ne 0) {
throw "takeown.exe $ResolvedPath failed: $LASTEXITCODE"
}
}
else {
takeown.exe /f $ResolvedPath
if ($LASTEXITCODE -ne 0) {
throw "takeown $ResolvedPath failed: $LASTEXITCODE"
}
}
icacls.exe $ResolvedPath /grant administrators:F /t
if ($LASTEXITCODE -ne 0) {
throw "icacls $ResolvedPath failed: $LASTEXITCODE"
}
}
}
}
end {
Write-Verbose 'Set-SelfOwnership end'
}
}
<#
.SYNOPSIS
Display disk information in human readable format.
#>
function Get-DiskUsage {
Get-Volume `
| Where-Object DriveLetter -ne $null `
| Sort-Object DriveLetter `
| Select-Object -Property DriveLetter, FileSystemLabel, `
@{Label = 'FreeGb'; Expression = { ($_.SizeRemaining / 1GB).ToString('F2') } }, `
@{Label = 'TotalGb'; Expression = { ($_.Size / 1GB).ToString('F2') } }, `
@{Label = 'Used %'; Expression = {
$pct = 100 - ($_.SizeRemaining / $_.Size) * 100
$blocks = [Math]::Floor($pct / 10)
'{0,6:F2}% {1}{2}' -f $pct, ('■' * $blocks), ('□' * (10 - $blocks))
}
} `
| Format-Table
}
<#
.SYNOPSIS
Open git repo homepage.
.DESCRIPTION
Run from anywhere inside of a local git project.
#>
function Invoke-GitRepo {
Param(
# Launch browser to root of repo?
[switch]$Root
)
if ($origin = [Uri](git.exe config --get remote.origin.url)) {
if ($Root) {
$uri = $origin.AbsoluteUri
}
else {
$uri = [uri]($origin.AbsoluteUri + '?path=/' + (git.exe rev-parse --show-prefix))
}
Start-Process -FilePath $uri
}
}
<#
.SYNOPSIS
Backup modified git files
#>
function gitbackup {
Param([Parameter(mandatory = $true, position = 0)][string]$Destination)
$backupDir = (Join-Path $Destination (Get-Date -Format FileDateTime))
git.exe status --porcelain=v1 | Where-Object { $_ -notlike 'D *' } | ForEach-Object {
$item = $_.SubString(3)
Copy-Item -LiteralPath $item -Destination (New-Item -Type Directory -Force (Join-Path $backupDir (Split-Path -Parent $item))) -Verbose
git.exe status | Out-File -Encoding UTF8 (Join-Path $backupDir 'gitstatus.txt')
}
}
<#
.SYNOPSIS
Combination of "git grep" and "git blame".
#>
function Get-GitGrepBlame {
Param([Parameter(Mandatory)][string]$Query)
# grep "--null" uses null separators, blame "-c" uses tab separators.
git.exe --no-pager grep --null --line-number $Query `
| Select-String -Pattern '(?<Filename>[^\0]+)\0(?<Line>[1-9][0-9]*)\0\s*(?<Text>.+)' `
| Select-Object -ExpandProperty Matches `
| ForEach-Object {
$filename = $_.Groups['Filename'].Value
$lineno = [int]$_.Groups['Line'].Value
# We'll grab line text from blame instead ("$line = $_.Groups['Text'].Value")
git.exe --no-pager blame -c --show-name --show-number -L "$lineno,$lineno" -- $filename `
| Select-String -Pattern '(?<Hash>[0-9a-fA-F]+)\t\((?<Author>.+)\t(?<Date>.+)\t\s*(?<Line>[1-9][0-9]*)\)\s*(?<Text>.+)' `
| Select-Object -ExpandProperty Matches `
| ForEach-Object {
[PSCustomObject]@{
Hash = $_.Groups['Hash'].Value
Author = $_.Groups['Author'].Value
# DateTime cast will convert to local time.
Date = [DateTime]$_.Groups['Date'].Value
Path = $filename
Line = [int]$_.Groups['Line'].Value
Text = $_.Groups['Text'].Value
}
}
}
}
<#
.SYNOPSIS
"Pretty prints" and displays .gitconfig file.
#>
function Get-GitConfig {
param([switch]$Local)
$params = @('--no-pager', 'config', '--list')
if ($Local) {
$params += '--local'
}
$config = foreach ($line in git.exe @params) {
if ($line -match '(?<section>.+?)(\.(?<subsection>.+))?\.(?<variable>.+?)=(?<value>.+)') {
[PSCustomObject]@{
Section = $matches['section']
SubSection = $matches['subsection']
Variable = $matches['variable']
Value = $matches['value']
}
}
else {
Write-Warning "Invalid line? $line"
}
}
foreach ($item in $config | Group-Object -Property Section, SubSection) {
# read section, subsection from first item in group
$Section, $SubSection = $item.Group[0].Section, $item.Group[0].SubSection
if ($SubSection) {
'[{0} "{1}"]' -f $Section, $SubSection
}
else {
'[{0}]' -f $Section
}
foreach ($entry in $item.Group) {
"{0}{1} = {2}" -f "`t", $entry.Variable, $entry.Value
}
}
}
function Format-GitConfig {
<#
.DESCRIPTION
Formats and outputs the Git configuration in a structured manner.
.EXAMPLE
.\Format-GitConfig.ps1
Outputs the Git configuration grouped by sections and sorted.
#>
$config = foreach ($line in git.exe --no-pager config --global --list | Sort-Object -Unique) {
if ($line -match '^\s*(?<section>\w*)\.(?<subsection>\w*)\.(?<key>\w*)\s*=\s*(?<value>.+)') {
# e.g. difftool.winmerge.name=WinMerge
[PSCustomObject]@{
Section = $matches['section']
Subsection = $matches['subsection']
Key = $matches['key']
Value = $matches['value']
}
}
elseif ($line -match '^\s*(?<section>\w*)\.(?<key>\w*)\s*=\s*(?<value>.+)') {
# e.g. color.ui=auto
[PSCustomObject]@{
Section = $matches['section']
Subsection = $null
Key = $matches['key']
Value = $matches['value']
}
}
else {
Write-Warning "Invalid line? $line"
}
}
# $config | Sort-Object Section, Variable | Format-List; return
foreach ($line in $config | Group-Object -Property Section, SubSection | Sort-Object Name) {
$section, $subsection = $line.Group[0].Section, $line.Group[0].Subsection
if ($subsection) {
"[$section ""$subsection""]"
}
else {
"[$section]"
}
foreach ($item in $line.Group) {
"`t{0} = {1}" -f $item.Key, $item.Value
}
}
}
function Write-BoxedMessage {
Param(
[Parameter(Mandatory)][string]$Message,
[ValidateRange(30, 37)][string]$ColorCode = 34
)
$Color = "`e[0;${ColorCode}m"
$lines = $Message -split "`n"
$maxLength = ($lines | Measure-Object -Maximum -Property Length).Maximum
$separator = '─' * ($maxLength + 2)
Write-Host "$Color┌$separator┐`e[0m"
foreach ($line in $lines) {
$paddedLine = $line.PadRight($maxLength)
Write-Host "$Color│`e[0m $paddedLine $Color│`e[0m"
}
Write-Host "$Color└$separator┘`e[0m`n"
}
<#
.SYNOPSIS
Creates a folder and cds into it.
#>
function mdcd {
Param([Parameter(Mandatory)][string]$Path)
if (New-Item -Path $Path -ItemType Directory -Force) {
Push-Location -LiteralPath $Path
}
}
<#
.SYNOPSIS
Create a PowerShell comment using figlet <https://github.com/lukesampson/figlet>
.DESCRIPTION
Will use "small" figlet font, with maximum 120 character width.
#>
function Write-Figlet {
[CmdletBinding()]
Param(
# Message(s) to print, can be from pipeline.
[Parameter(Mandatory, Position = 0, ValueFromPipeline)][ValidateNotNullOrEmpty()]
[string[]]$Message,
# Language comment indicator, '' for none.
[string]$CommentIndicator = '# ',
# Maximum width of message
[uint]$OutputWidth = 80,
# Font to use, see "figlet.exe -list", e.g. 'slant'
[string]$FontName = 'small'
)
begin {}
process {
foreach ($line in $Message) {
figlet.exe -w $OutputWidth -f $FontName $line | ForEach-Object {
"$CommentIndicator $_"
}
$CommentIndicator
}
}
end {}
}
<#
.SYNOPSIS
Write text in a bold unicode font, e.g. 𝐇𝐞𝐥𝐥𝐨 𝐖𝐨𝐫𝐥𝐝!
#>
function Write-Bold {
[CmdletBinding()]
Param(
# Message(s) to print, can be from pipeline.
[Parameter(Mandatory, Position = 0, ValueFromPipeline)][ValidateNotNullOrEmpty()]
[string[]]$Message
)
begin {
# Mathematical Alphanumeric Symbols Block https://unicodeplus.com/block/1D400
# Mathematical Operators Block https://unicodeplus.com/block/2200
# Miscellaneous Mathematical Symbols-A Block https://unicodeplus.com/block/27C0
# Miscellaneous Mathematical Symbols-B Block https://unicodeplus.com/block/2980
# Supplemental Mathematical Operators Block https://unicodeplus.com/block/2A00
# Can't use dictionary, as it is case insensitive by default
$hash = New-Object System.Collections.Hashtable ([StringComparer]::Ordinal)
$hash[[char]'-'] = "`u{22EF}"
$hash[[char]'('] = "`u{27EE}"
$hash[[char]')'] = "`u{27EF}"
$hash[[char]'['] = "`u{27E6}"
$hash[[char]']'] = "`u{27E7}"
$hash[[char]'|'] = "`u{2999}"
$hash[[char]'0'] = "`u{1D7EC}"
$hash[[char]'1'] = "`u{1D7ED}"
$hash[[char]'2'] = "`u{1D7EE}"
$hash[[char]'3'] = "`u{1D7EF}"
$hash[[char]'4'] = "`u{1D7F0}"
$hash[[char]'5'] = "`u{1D7F1}"
$hash[[char]'6'] = "`u{1D7F2}"
$hash[[char]'7'] = "`u{1D7F3}"
$hash[[char]'8'] = "`u{1D7F4}"
$hash[[char]'9'] = "`u{1D7F5}"
$hash[[char]'A'] = "`u{1D400}"
$hash[[char]'a'] = "`u{1D41A}"
$hash[[char]'B'] = "`u{1D401}"
$hash[[char]'b'] = "`u{1D41B}"
$hash[[char]'C'] = "`u{1D402}"
$hash[[char]'c'] = "`u{1D41C}"
$hash[[char]'D'] = "`u{1D403}"
$hash[[char]'d'] = "`u{1D41D}"
$hash[[char]'E'] = "`u{1D404}"
$hash[[char]'e'] = "`u{1D41E}"
$hash[[char]'F'] = "`u{1D405}"
$hash[[char]'f'] = "`u{1D41F}"
$hash[[char]'G'] = "`u{1D406}"
$hash[[char]'g'] = "`u{1D420}"
$hash[[char]'H'] = "`u{1D407}"
$hash[[char]'h'] = "`u{1D421}"
$hash[[char]'I'] = "`u{1D408}"
$hash[[char]'i'] = "`u{1D422}"
$hash[[char]'J'] = "`u{1D409}"
$hash[[char]'j'] = "`u{1D423}"
$hash[[char]'K'] = "`u{1D40A}"
$hash[[char]'k'] = "`u{1D424}"
$hash[[char]'L'] = "`u{1D40B}"
$hash[[char]'l'] = "`u{1D425}"
$hash[[char]'M'] = "`u{1D40C}"
$hash[[char]'m'] = "`u{1D426}"
$hash[[char]'N'] = "`u{1D40D}"
$hash[[char]'n'] = "`u{1D427}"
$hash[[char]'O'] = "`u{1D40E}"
$hash[[char]'o'] = "`u{1D428}"
$hash[[char]'P'] = "`u{1D40F}"
$hash[[char]'p'] = "`u{1D429}"
$hash[[char]'Q'] = "`u{1D410}"
$hash[[char]'q'] = "`u{1D42A}"
$hash[[char]'R'] = "`u{1D411}"
$hash[[char]'r'] = "`u{1D42B}"
$hash[[char]'S'] = "`u{1D412}"
$hash[[char]'s'] = "`u{1D42C}"
$hash[[char]'T'] = "`u{1D413}"
$hash[[char]'t'] = "`u{1D42D}"
$hash[[char]'U'] = "`u{1D414}"
$hash[[char]'u'] = "`u{1D42E}"
$hash[[char]'V'] = "`u{1D415}"
$hash[[char]'v'] = "`u{1D42F}"
$hash[[char]'W'] = "`u{1D416}"
$hash[[char]'w'] = "`u{1D430}"
$hash[[char]'X'] = "`u{1D417}"
$hash[[char]'x'] = "`u{1D431}"
$hash[[char]'Y'] = "`u{1D418}"
$hash[[char]'y'] = "`u{1D432}"
$hash[[char]'Z'] = "`u{1D419}"
$hash[[char]'z'] = "`u{1D433}"
}
process {
foreach ($line in $Message) {
# StringBuilder is much faster than string concatenation
$sb = [Text.StringBuilder]::new()
foreach ($char in $line.ToCharArray()) {
$u = $hash[$char]
$sb.Append(@($char, $u)[$null -ne $u ]) | Out-Null
}
$sb.ToString()
}
}
end {}
}
if ($env:VSCODE_INJECTION) {
Write-Host -ForegroundColor Yellow 'Running inside VSCode Terminal.'
return
}
if ($host.Name -ne 'ConsoleHost') {
Write-Host -ForegroundColor Yellow 'Not running in consolehost.'
return
}
# ___ _ _
# | __| _ _ _ __| |_(_)___ _ _ ___
# | _| || | ' \/ _| _| / _ \ ' \(_-<
# |_| \_,_|_||_\__|\__|_\___/_||_/__/
#
#
function Show-ColorTable {
Param([switch]$ShowAll)
if (-not $ShowAll) {
# Inspired by https://windowsterminalthemes.dev
$fgColors = @('Black', 'DarkGray', 'DarkRed', 'Red', 'DarkGreen', 'Green', `
'DarkYellow', 'Yellow', 'DarkBlue', 'Blue', 'DarkMagenta', 'Magenta', `
'DarkCyan', 'Cyan', 'Gray', 'White')
$bgColors = @('Black', 'DarkGray', 'DarkRed', 'DarkGreen', 'DarkYellow', `
'DarkBlue', 'DarkMagenta', 'DarkCyan', 'Gray', 'Black' )
foreach ($fgcolor in $fgColors) {
foreach ($bgcolor in $bgColors) {
Write-Host -NoNewLine -ForegroundColor $fgcolor -BackgroundColor $bgcolor 'gYw'
Write-Host -NoNewline ' '
}
Write-Host ''
}
}
else {
$colors = [enum]::GetValues([ConsoleColor])
foreach ($bgcolor in $colors) {
foreach ($fgcolor in $colors) {
$fg, $bg = ($fgcolor -replace 'Dark', 'Dk'), ($bgcolor -replace 'Dark', 'Dk')
Write-Host -ForegroundColor $fgcolor -BackgroundColor $bgcolor -NoNewLine "|$fg"
}
Write-Host " on $bg"
}
}
}
<#
.SYNOPSIS
Download latest PowerShell.
.DESCRIPTION
Will locate and download latest version of PowerShell.
#>
function DownloadLatestPS {
param([Parameter(Mandatory = $true)] [string]$Folder)
# Speed up Invoke-WebRequest calls
$oldpp = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
$json = (Invoke-WebRequest -uri 'https://api.github.com/repos/PowerShell/PowerShell/releases/latest').Content | ConvertFrom-Json
$psUri = [uri]($json.assets | Where-Object name -Like 'PowerShell-*-win-x64.msi').browser_download_url
"Downloading: $($psUri.AbsoluteUri)"
Invoke-WebRequest $psUri.AbsoluteUri -OutFile (Join-Path $Folder $psuri.Segments[-1])
$ProgressPreference = $oldpp
}
<#
.SYNOPSIS
Start notepad.
.DESCRIPTION
Will use notepad4.
#>
function Invoke-notepad {
Start-Process -NoNewWindow -FilePath 'notepad4.exe' -ArgumentList $args
}
function Get-UninstallPath {
param(
[Parameter(Mandatory = $true)] [string]$ProductId
)
$regPath = "/SOFTWARE/Microsoft/Windows/CurrentVersion/Uninstall/$ProductId"
# Check user location first
$path = Get-ItemProperty -Path "HKCU:$regPath" -Name 'InstallLocation' -ErrorAction SilentlyContinue
if (-not $path) {
# Check system location next
$path = Get-ItemProperty -Path "HKLM:$regPath" -Name 'InstallLocation' -ErrorAction SilentlyContinue
}
$path.InstallLocation
}
<#
.SYNOPSIS
Kill a process.
.DESCRIPTION
Will try nicely first. Specify -Force to force.
#>
function Invoke-Kill {
param(
[Parameter(Mandatory = $true)] [string]$Name,
[switch]$Force
)
$process = Get-Process -Name $Name -ErrorAction SilentlyContinue
if ($process.Count -eq 0) {
'Process not found'
}
elseif ($process.Count -eq 1) {
$description = $process.Description
$processId = $process.Id
$mainWindowTitle = $process.MainWindowTitle
# try gracefully first
$process.CloseMainWindow() | Out-Null
if ($Force -and -not $process.HasExited) {
Stop-Process -Force -Name $Name
}
if ($process.HasExited) {
'process {0} ({1}) - ''{2}'' killed' -f $description, $processId, $mainWindowTitle
}
}
else {
'Multiple instances running: {0}' -f ($process.Id -join ' ')
}
}
<#
.SYNOPSIS
Path tools.
.DESCRIPTION
Doesn't persist changes.
#>
function Path {
param(
[Parameter(ParameterSetName = 'List')]
[switch]$List,
[Parameter(ParameterSetName = 'Add')]
[switch]$Add,
[Parameter(ParameterSetName = 'Add')]
[switch]$Top = $False,
[Parameter(ParameterSetName = 'Remove')]
[switch]$Remove,
[Parameter(ParameterSetName = 'Add', Mandatory = $True, Position = 0)]
[Parameter(ParameterSetName = 'Remove', Mandatory = $True, Position = 0)]
[string]$Path
)
if ($List) {
(Get-ChildItem env:PATH).Value -split ';'
return
}
if ($Add) {
$Path = $Path.TrimEnd('\')
$paths = (Get-ChildItem env:PATH).Value -split ';'
if ($paths -notcontains $Path -and $paths -notcontains "$Path\") {
if ($Top) {
$env:PATH = "$Path;$env:PATH"
}
else {
$env:PATH = "$env:PATH;$Path"
}
}
return
}
if ($Remove) {
$Path = $Path.TrimEnd('\')
$newPath = @()
(Get-ChildItem env:PATH).Value -split ';' | ForEach-Object {
if ($_ -notlike $Path -and $_ -notlike "$Path\") {
$newPath += $_
}
}
$env:PATH = $newPath -join ';'
return
}
}
<#
.SYNOPSIS
rsync wrapper.
.DESCRIPTION
Requires cwrsync to be installed.
.EXAMPLE
rsync -Source 'C:\Users\jeffm\Downloads\Raise' -Hostname '192.168.10.11' -User 'jeff' -Destination '/media/usb/Music/Earth, Wind & Fire'
#>
function rsync {
param(
[Parameter(Mandatory = $True)] [string]$Source,
[Parameter(Mandatory = $True)] [string]$Destination,
[Parameter(Mandatory = $True)] [string]$User,
[Parameter(Mandatory = $True)] $Hostname
)
# Use cwrsync
$cwrsync_path = scoop.ps1 prefix cwrsync
if (-not $cwrsync_path) {
'cwrsync not found'
return
}
$cwrsync_path += '\bin'
# https://www.itefix.net/content/rsync-does-not-recognize-windows-paths-correct-manner
$Source = $Source -replace '\\', '/'
if ($Source -match '^[A-Za-z]:') {
$Source = '/cygdrive/{0}{1}' -f $Source[0], $Source.Substring(2)
}
$Destination = "$User@${Hostname}:$Destination"
"Source: $Source"
"Destination: $Destination"
& "$cwrsync_path\rsync.exe" --rsh="$cwrsync_path/ssh.exe" --progress --human-readable --verbose --recursive --dirs $Source $Destination
}
<#
.SYNOPSIS
Get metadata from all JJazzLab '*.sng' files in current directory.
#>
function Get-JJazzLabMeta {
foreach ($file in (Get-ChildItem -File '*.sng')) { ([xml](Get-Content $file)).Song | Select-Object @{Name = 'BaseName'; Expression = { $file.BaseName } }, spName, spTempo, spComments }
}
<#
.SYNOPSIS
Simulate output of UNIX "sha256hash".
#>
function Invoke-Sha256Hash {
[CmdletBinding()]
param(
[Parameter(Mandatory, Position = 0, ValueFromPipeline)]
[string[]]$Files
)
begin {}
process {
foreach ($file in $Files) {
$target = (Resolve-Path $file).Path
Write-Verbose "Hashing $file"
# Get-ChildItem -Recurse -File | ForEach-Object {
'{0} *{1}' -f (Get-FileHash -Algorithm SHA256 -LiteralPath $target).Hash.ToLower(), (Resolve-Path -Relative $target).Substring(2) -replace '\\', '/'
}
}
end {}
clean {}
}
function Invoke-7zBackup {
param (
# Root of path to back up recursively, e.g. 'F:\'
[Parameter(Mandatory)][string]$SourcePath,
# Backup path, e.g. 'D:\BACKUP'
[string]$DestinationPath = '.'
)
Get-Command '7z.exe' -CommandType Application -ErrorAction Stop | Out-Null
$filename = 'Backup {0}' -f (Get-Date -Format '(yyyy-dd-mm)')
$arguments = @(
'a',
# "-mx=1" reduces compression to the minimum (faster)
'-mx=1',
# "-ms=off" turns off solid mode (faster)
'-ms=off',
# "-mf=off" turns off special compression for exe files (faster)
'-mf=off',
# "-v4g" uses 4GB volumes
'-v4g',
# -x[r[-|0]][m[-|2]][w[-]]{@listfile|!wildcard} : eXclude filenames
'-x!"System Volume Information"',
'-x!"$RECYCLE.BIN"',
'-x!"$RECYCLER"',
'-x!"$WINDOWS.~BT"',
# -r[-|0] : Recurse subdirectories for name search
'-r',
# <archive_name>
('"{ 0 }"' -f (Join-Path $DestinationPath $filename)),
# <file_names>
(Join-Path $SourcePath '*')
)
Start-Process -FilePath '7z.exe' -ArgumentList $arguments -NoNewWindow -Wait
}
function Invoke-IpFilterUpdate {
[CmdletBinding()]
param ([switch]$Force)
[uri]$BlockListUri = 'http://list.iblocklist.com/?list=ydxerpxkpcfqjaybcssw&fileformat=dat&archiveformat=zip'
# try default install location
$BlockListPathResolved = Resolve-Path "$env:AppData\qBittorrent" -ErrorAction SilentlyContinue
if (-not $BlockListPathResolved) {
# try scoop install path
$BlockListPathResolved = Resolve-Path '~\scoop\apps\qbittorrent\current\profile\qBittorrent' -ErrorAction SilentlyContinue
}
if ($BlockListPathResolved) {
$ipFilterPath = Join-Path -Path $BlockListPathResolved -ChildPath 'ipfilter.dat'
Write-Host " `e[1;34m*`e[0m Blocklist path: `e[1;35m$ipFilterPath`e[0m"
if (-not $Force) {
Write-Host " `e[1;34m*`e[0m Checking blocklist date on server."
$response = Invoke-WebRequest -Uri $BlockListUri -Method Head -UserAgent 'curl/8.7.1'
$lmServer = [datetime]($response.Headers['Last-Modified'][0])
$lmLastWriteTime = (Get-ItemProperty -LiteralPath $ipFilterPath -Name LastWriteTime -ErrorAction SilentlyContinue).LastWriteTime
$updateNeeded = $true
if ($lmLastWriteTime) {
$lmLocal = [datetime]$lmLastWriteTime
Write-Host (" `e[1;33m*`e[0m Server: `e[1;36m{0:yyyy-MM-dd}`e[0m; Local: `e[1;36m{1:yyyy-MM-dd}`e[0m" -f $lmServer, $lmLocal)
# Only update if local is at least a day older than server
if (($lmLocal - $lmServer).Days -eq 0) {
$updateNeeded = $false
}
}
}
else {
# -Force specified
Write-Host " `e[1;33m!`e[0m Force specified, forcing update."
$updateNeeded = $true
}
if ($updateNeeded) {
# ZIP file contains a single file, "ydxerpxkpcfqjaybcssw.txt"
$tempIpFilterPath = Join-Path -Path $BlockListPathResolved -ChildPath 'ipfilter.dat.zip'
Write-Host " `e[1;34m*`e[0m Downloading new filter to `e[1;35m$tempIpFilterPath`e[0m"
$response = Invoke-WebRequest -Uri $BlockListUri -UserAgent 'curl/8.7.1' -OutFile $tempIpFilterPath
Write-Host " `e[1;34m*`e[0m Expanding to `e[1;35m$BlockListPathResolved`e[0m"
Expand-Archive -DestinationPath $BlockListPathResolved -LiteralPath $tempIpFilterPath
Remove-Item -LiteralPath $ipFilterPath -ErrorAction SilentlyContinue
Write-Host " `e[1;34m*`e[0m Renaming to `e[1;35mipfilter.dat`e[0m"
Rename-Item -NewName 'ipfilter.dat' -LiteralPath (Join-Path -Path $BlockListPathResolved 'ydxerpxkpcfqjaybcssw.txt')
Remove-Item -LiteralPath $tempIpFilterPath
Write-Host " `e[1;32m✓`e[0m Blocklist updated."
}
else {
Write-Host " `e[1;32m✓`e[0m Everything is up to date!"
}
}
else {
Write-Warning " `e[1;31m!`e[0m Cant determine qBittorrent path."
}
}
# Convert a file to MP3 Using FFMPEG
function ConvertTo-Mp3 {
[CmdletBinding()]
param (
# "transparent results":
# 0 = 245 kbit/sec avg.
# 1 = 225 kbit/sec avg..
# 2 = 190 kbit/sec avg. (170-210 kbit/sec). See https://trac.ffmpeg.org/wiki/Encode/MP3
# 3 = 175 kbit/sec avg.
[ValidateRange(0, 9)][int]$Quality = 2,
# Input file name. Can also use "-FullName" for piping from "Get-ChildItem -File"
[Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
[Alias('FullName')]
[string] $InputFile,
# Output file name. If not specified, uses <name>.mp3 in current directory.
[Parameter(Position = 1)]
[string] $OutputFile,
# Force overwriting files that exist.
[switch]$Force
)
begin {
$count = 0
}
process {
# Don't modify $OutputFile, as we will check it against null with each new file.
$Destination = $OutputFile
if (-not $OutputFile) {
$Destination = ('{0}.mp3' -f (Split-Path -Path $InputFile -LeafBase))
}
if ((Split-Path -Path $Destination -Extension) -ne '.mp3') {
throw 'Extension must be .mp3'
}
'Converting "{0}" to "{1}".' -f (Split-Path -Path $InputFile -Leaf), (Split-Path -Path $Destination -Leaf)
# -hide_banner Suppress printing banner.
# -loglevel Set logging level and flags used by the library.
# -stats Log encoding progress/statistics as "info"-level log
# -i input file url
# -vn blocks all video streams of a file from being filtered or being automatically selected or mapped for any output.
# -codec:a Select an encoder. "a" is stream_specifier, followed by codec name.
# -qscale:a Specify codec-dependent fixed quality scale (VBR). "a" is stream_specifier, followed by q value.
$ffmpeg_args = `
'-hide_banner', `
'-loglevel', 'error', `
'-stats', `
'-i', """$InputFile""", `
'-vn', `
'-codec:a', 'libmp3lame', `
'-qscale:a', $Quality
if ($Force) {
$ffmpeg_args += '-y'
}
# Output filename must be last argument.
$ffmpeg_args += """$Destination"""
# ffmpeg always seems to return 0 in ExitCode, so no way to check for failures, even with "-PassThru".
Start-Process -FilePath 'ffmpeg.exe' -ArgumentList $ffmpeg_args -NoNewWindow -Wait
$count++
}
end {
"Processed $count files."
}
}
function Format-PowerShell {
# .DESCRIPTION
# format powershell script
Param([Parameter(Mandatory, Position = 0)][string]$Path)
Install-Module -Name PowerShell-Beautifier
Edit-DTWBeautifyScript -SourcePath (Resolve-Path -LiteralPath $Path).Path -IndentType FourSpaces
}
<#
.SYNOPSIS
Decode an ATP safelink
#>
function Convert-SafeLink {
[CmdletBinding()]
Param([Parameter(Mandatory, Position = 0, ValueFromPipeline)][Uri]$Uri)
if ($Uri.Query) {
$query = @{}
# Split query into Name, Unescaped Value pairs.
$Uri.Query.Substring(1) -split '&' | ForEach-Object {
$key, $value = $_.Split('=')
$query[$key] = [URI]::UnescapeDataString($value)
}
[PSCustomObject]@{
'Host' = $Uri.Host
'Uri' = $query['url']
'Data' = $query['data']
}
}
}
function Get-SystemInfo {
# Derived from WinProdKeyFinder: https://github.com/mrpeardotnet/WinProdKeyFinder (DecodeProductKeyWin8AndUp)
$digitalProductId = (Get-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion').DigitalProductId
# First byte appears to be length
if ($digitalProductId[0] -ne $digitalProductId.Length) {
throw 'Invalid length.'
}
$productId = [Text.Encoding]::UTF8.GetString($digitalProductId[8..30])
$sku = [Text.Encoding]::UTF8.GetString($digitalProductId[36..48])
$keyOffset = 52
# decrypt base24 encoded binary data from $digitalProductId[52..66] $key
$key = $null
$digits = 'BCDFGHJKMPQRTVWXY2346789'
For ($i = 24; $i -ge 0; $i--) {
$index = 0
For ($j = 14; $j -ge 0; $j--) {
$index = $index * 256
$index += $digitalProductId[$keyOffset + $j]
$digitalProductId[$keyOffset + $j] = [math]::truncate($index / 24)
$index = $index % 24
}
$key = $digits[$index] + $key
}
# Replace first character with 'N', split every 5 chars with '-'
$key = ('N' + $key.Substring(1, $key.Length - 1)) -split '(.{5})' -ne '' -join '-'
$currentVersion = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$win32os = Get-WmiObject Win32_OperatingSystem
[PSCustomObject]@{
Edition = $win32os.Caption
Version = $currentVersion.DisplayVersion
OSBuild = '{0}.{1}' -f $currentVersion.CurrentBuild, $currentVersion.UBR
OSArch = $win32os.OSArchitecture
RegisteredOwner = $currentVersion.RegisteredOwner
ProductID = $productId
Sku = $sku
ProductKey = $key
}
}
#######################################################################################################################
# _
# _ __ __ _(_)_ _
# | ' \/ _` | | ' \
# |_|_|_\__,_|_|_||_|
#
'download', 'filesystem', 'git', 'messages', 'musescore', 'normalize', 'prompt', 'transcribe', 'update', 'vscode' `
| ForEach-Object {
"Loading functions from $_.ps1"
. "$PSScriptRoot\$_.ps1"
}
# See also $env:USERPROFILE\OneDrive\Documents\PowerShell\profile.ps1
Write-BoxedMessage -Message "Profile loaded from $PSScriptRoot`nPowerShell $((Get-Host).Version)"
# Use emacs key bindings.
# Set-PSReadLineOption -EditMode Emacs
# Make TAB completion more like bash - Use up, down arrow to complete command.
Set-PSReadlineKeyHandler -Key UpArrow -Function HistorySearchBackward
Set-PSReadlineKeyHandler -Key DownArrow -Function HistorySearchForward
if ((Get-Command -Name 'fzf.exe' -CommandType Application -ErrorAction SilentlyContinue)) {
# PSFzf: https://github.com/kelleyma49/PSFzf
'PSFzf' | ForEach-Object {
if (-not (Get-Module -Name $_ -ListAvailable -ErrorAction SilentlyContinue)) {
"Installing $_"
Install-Module -Name $_ -Force
}
}
'Enabling Ctrl+T, Ctrl+R completion'
# Reverse Search Through PSReadline History (default chord: Ctrl+r)
Set-PsFzfOption -PSReadlineChordProvider 'Ctrl+t' -PSReadlineChordReverseHistory 'Ctrl+r'
# Set-Location Based on Selected Directory (default chord: Alt+c)
$commandOverride = [ScriptBlock] { param($Location) Write-Host $Location }
Set-PsFzfOption -AltCCommand $commandOverride
# Tab Expansion
# Set-PSReadLineKeyHandler -Key Tab -ScriptBlock { Invoke-FzfTabCompletion }
}
Invoke-WingetUpdate
# Add Winget package paths to PATH
$wingetPackagesPath = "$env:LocalAppData\Microsoft\Winget\Packages"
"Adding Winget paths from $wingetPackagesPath"
Get-ChildItem -LiteralPath $wingetPackagesPath -Recurse -Filter '*.exe' -ErrorAction SilentlyContinue `
| Group-Object DirectoryName `
| ForEach-Object {
$exes = $_.Group | ForEach-Object { "`e[1m{0}`e[22m" -f (Split-Path -Leaf $_) }
' {0}: {1}' -f [IO.Path]::GetRelativePath("$env:LocalAppData\Microsoft\Winget\Packages", $_.Name), ($exes -join ', ')
$env:Path += ";$($_.Name)"
}
# Invoke-ScoopUpdate
# -----------------------------------------------------------------------------
# zoxide, needs to come AFTER setting prompt!
if ((Get-Command -Name 'zoxide.exe' -CommandType Application -ErrorAction SilentlyContinue)) {
'Adding zoxide completion'
Invoke-Expression -Command $(zoxide.exe init powershell | Out-String)
}
<#
.SYNOPSIS
Convert MuseScore file to PDF. Files are output to current folder.
.EXAMPLE
mkdir PDF; cd PDF
Get-ChildItem -File -LiteralPath '..' -Filter '*.mscz' | ForEach-Object { ConvertFrom-MuseScore -Extract Parts -File $_.FullName }
#>
function ConvertFrom-MuseScore {
param(
# Path to .mscz file
[Parameter(Mandatory)][string]$File,
# What to extract
[ValidateSet('Score', 'ScoreAndParts', 'ScoreAudio', 'Parts', IgnoreCase)]
[string[]]$Extract = @('Score', 'Parts'),
# Path to MuseScore. Defaults to standard install location.
[string]$MuseScorePath = (Join-Path "$env:ProgramFiles" 'MuseScore 4\bin\MuseScore4.exe')
)
# https://github.com/musescore/MuseScore/issues/22887
# --export-score-parts / -P CLI options no longer work in 4.x #22887
$path = Resolve-Path -LiteralPath $File -ErrorAction Stop
Write-Host ('Processing "{0}"...' -f (Split-Path -Path $path -Leaf))
Write-Host '* Extracting JSON'
$musescoreJsonFile = 'musescore-score-parts.json'
$process = Start-Process -Wait -NoNewWindow -PassThru `
-WorkingDirectory (Get-Location).Path `
-FilePath """$MuseScorePath""" `
-ArgumentList '--score-parts-pdf', """$path""", '--export-to', """$musescoreJsonFile"""
if ($process.ExitCode -ne 0) {
Write-Warning "ExitCode=$($process.ExitCode)"
}
$musescoreJson = Get-Content $musescoreJsonFile | ConvertFrom-Json
$name = Split-Path -LeafBase $musescoreJson.score
if ('ScoreAndParts' -in $Extract) {
$filename = $name + ' [Score and Parts].pdf'
Write-Host "* Extracting score and parts ""$filename"""
# Set-Content -LiteralPath $filename -AsByteStream -Value ([Convert]::FromBase64String($musescoreJson.scoreFullBin))
# WORKAROUND:
# --score-parts-pdf CLI option outputs double base64-encoded JSON string for .scoreFullBin field #28436
# https://github.com/musescore/MuseScore/issues/28436 (Fixed in MuseScore 4.6)
$base64string = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($musescoreJson.scoreFullBin))
Set-Content -LiteralPath $filename -AsByteStream -Value ([Convert]::FromBase64String($base64string))
}
if ('Score' -in $Extract) {
$filename = $name + ' [Score].pdf'
Write-Host "* Extracting score ""$filename"""
Set-Content -LiteralPath $filename -AsByteStream -Value ([Convert]::FromBase64String($musescoreJson.scoreBin))
}
if ('Parts' -in $Extract) {
for ($i = 0; $i -lt $musescoreJson.parts.Count; $i++) {
$partname = $musescoreJson.parts[$i]
# Create filename using "score_name [part].pdf", ensuring valid filename.
$filename = ('{0} [{1}].pdf' -f (Split-Path -LeafBase $name), $partname) -replace '[<>:"/\\|?*]', '_'
Write-Host "* Extracting part ""$filename"""
Set-Content -LiteralPath $filename -AsByteStream -Value ([Convert]::FromBase64String($musescoreJson.partsBin[$i]))
}
}
Remove-Item -LiteralPath $musescoreJsonFile
if ('ScoreAudio' -in $Extract) {
$filename = $name + '.mp3'
Write-Host "* Extracting audio ""$filename""..."
$process = Start-Process -Wait -NoNewWindow -PassThru `
-WorkingDirectory (Get-Location).Path `
-FilePath """$MuseScorePath""" `
-ArgumentList """$path""", '--export-to', """$filename"""
if ($process.ExitCode -ne 0) {
Write-Warning "ExitCode=$($process.ExitCode)"
}
}
}
# Normalize a file using FFMPEG
# Normalized file will be named {filename}-normalized.{extension}
function Invoke-Normalize {
# Support -Confirm (ShouldProcess), -WhatIf
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]]$Path,
[switch]$InPlace
)
begin {
Get-Command -Name ffmpeg.exe -CommandType Application -ErrorAction Stop | Out-Null
}
process {
foreach ($item in $Path) {
Write-Verbose ('-' * 79)
$resolveditem = Resolve-Path -LiteralPath $item
$itemname = Split-Path -Path $item -Leaf | Split-Path -LeafBase
$itemextension = Split-Path -Path $item -Leaf | Split-Path -Extension
Write-Verbose "Resolved item: $resolveditem"
Write-Verbose "Name: $itemname; Extension: $itemextension"
if (-not $PSCmdlet.ShouldProcess($resolveditem, 'Normalize')) {
continue
}
$ffmpeg_args = `
# Overwrite output files.
'-y', `
# Don't show banner
'-hide_banner', `
# disable console itneraction
'-nostdin', `
# Input file
'-i', """$resolveditem""", `
# set audio filters
'-af', '"volumedetect"', `
# disable video
'-vn', `
# disable subtitle
'-sn', `
# disable data
'-dn', `
# force format
'-f', 'null', `
# No output file
'-'
$startinfo = New-Object System.Diagnostics.ProcessStartInfo
$startinfo.FileName = 'ffmpeg.exe'
# FFMPEG writes to stderr!
$startinfo.RedirectStandardError = $true
$startinfo.RedirectStandardOutput = $false
$startinfo.UseShellExecute = $false
$startinfo.Arguments = $ffmpeg_args
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $startinfo
Write-Verbose "Running ffmpeg with arguments: $($ffmpeg_args -join ' ')"
$proc.Start() | Out-Null
Write-Verbose 'ReadToEnd...'
$out = $proc.StandardError.ReadToEnd()
Write-Verbose 'WaitForExit...'
$proc.WaitForExit()
Write-Verbose "ffmpeg exited with code $($proc.ExitCode)"
if ($proc.ExitCode -ne 0) {
Write-Warning "ffmpeg failed with code $($proc.ExitCode)"
$out
continue
}
# Write-verbose $out
# e.g. "[Parsed_volumedetect_0 @ 0000021cc88cbc00] max_volume: 0.0 dB"
if ($out -notmatch 'max_volume: (?<max_volume>-?[\d\.]+) dB') {
Write-Warning "Unable to determine max_volume of $resolveditem"
continue
}
# Gain is inverse of reported max_volume
$gain = - [decimal]$matches['max_volume']
if ($gain -le 1.0) {
Write-Output "No gain adjustment required for $resolveditem"
continue
}
$tempfile = Join-Path -Path (Get-Location) -ChildPath "$itemname-normalized$($itemextension)"
Write-Output "Applying gain of $gain dB to $resolveditem"
$ffmpeg_args = `
# Overwrite output files.
'-y', `
# Don't show banner
'-hide_banner', `
# disable console itneraction
'-nostdin', `
# Input file
'-i', """$resolveditem""", `
# set audio filters
'-af', """volume=$($gain)dB""", `
# Output file
"""$tempfile"""
$startinfo = New-Object System.Diagnostics.ProcessStartInfo
$startinfo.FileName = 'ffmpeg.exe'
# FFMPEG writes to stderror!
$startinfo.RedirectStandardError = $true
$startinfo.RedirectStandardOutput = $false
$startinfo.UseShellExecute = $false
$startinfo.Arguments = $ffmpeg_args
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo = $startinfo
Write-Verbose "Running ffmpeg with arguments: $($ffmpeg_args -join ' ')"
$proc.Start() | Out-Null
Write-Verbose 'ReadToEnd...'
$out = $proc.StandardError.ReadToEnd() -split "`r`n"
Write-Verbose 'WaitForExit...'
$proc.WaitForExit()
Write-Verbose "ffmpeg exited with code $($proc.ExitCode)"
if ($proc.ExitCode -ne 0) {
Write-Warning "ffmpeg failed with code $($proc.ExitCode)"
$out
Remove-Item $tempfile -ErrorAction SilentlyContinue
continue
}
if ($InPlace) {
$backupItem = "$itemname-backup$itemextension"
Rename-Item -LiteralPath $resolveditem -NewName $backupItem -ErrorAction Stop
Rename-Item -LiteralPath $tempfile -NewName $resolveditem -ErrorAction Stop
Remove-Item -LiteralPath $backupItem -ErrorAction SilentlyContinue
}
}
}
end {}
}
# Profile for Windows PowerShell (PowerShell 5)
Write-Host ("Profile loaded from {0}" -f $PSCommandPath)
if ($env:VSCODE_INJECTION) {
Write-Host -ForegroundColor Yellow 'Running inside VSCode Terminal'
}
function TruncatePath {
param([string]$Path, [int]$MaxChars = 80)
$truncated = $Path
if ($Path.Length -gt $MaxChars) {
if (Split-Path $Path -IsAbsolute) {
# truncated is the minimal string that will be shown, e.g. 'C:\...'
$minPath = (Split-Path $Path -Qualifier) + '\...'
# Fit as many subpaths as possible
$fit = $null
while ($true) {
$leaf = Split-Path $Path -Leaf
if ($minPath.Length + 1 + $fit.Length + $leaf.Length -gt $MaxChars) {
break
}
$fit = '\' + $leaf + $fit
$Path = Split-Path $Path -Parent
}
$truncated = $minPath + $fit
}
else {
# Non-absolute path, just truncate as needed.
$truncated = '...' + $Path.Substring(($Path.Length + 3) - $MaxChars)
}
}
$truncated
}
<#
.SYNOPSIS
Prompt
.DESCRIPTION
Indicates if running as admin, and git branch.
#>
function global:Prompt {
$cwd = (Get-Location).Path
if ($cwd.StartsWith($HOME)) { $cwd = $cwd.Replace($HOME, '~') }
$cwd = TruncatePath -Path $cwd -MaxChars 60
$isAdmin = [bool](([Security.Principal.WindowsIdentity]::GetCurrent()).groups -match 'S-1-5-32-544')
Write-Host @(' PS ', ' ADMIN ')[$isAdmin] -NoNewline -BackgroundColor @('DarkMagenta', 'DarkRed')[$isAdmin] -ForegroundColor White
Write-Host " $cwd " -NoNewline -BackgroundColor DarkYellow -ForegroundColor Black
try {
$gitBranch = (git.exe symbolic-ref HEAD 2>&1)
if ($gitBranch.ToString() -notmatch 'fatal:') {
$gitBranch = Split-Path -Leaf $gitBranch
$isDirty = (git.exe status --porcelain).Count -ne 0
Write-Host " $gitBranch " -NoNewline -Background @('DarkBlue', 'DarkCyan')[$isDirty] -ForegroundColor White
}
}
catch {}
Write-Host ''
'> '
}
<#
.SYNOPSIS
Fix a Transcribe XSC file to my desired default settings.
.DESCRIPTION
This script reads a Transcribe XSC file, modifies some settings to my preferred defaults,
and writes the changes back to the file.
.PARAMETER Path
The path to the XSC file to fix.
.NOTES
The XSC file format is meh, so Read-Xsc is a bit weird.
.EXAMPLE
. c:\Users\jeffm\.local\bin\FixXsc.ps1
Get-ChildItem -File '*.xsc' | Process-Xsc
#>
function Read-Xsc {
[CmdletBinding()]
param ([Parameter(Mandatory)][string]$Path)
$data = [ordered]@{}
$version = $null
$transcribe = $null
$section = $null
Get-Content -LiteralPath $Path -ErrorAction Stop | ForEach-Object {
switch -regex ($_) {
# [Signature] Document Version
'^XSC Transcribe.Document Version (\d+\.\d+)' {
$version = $matches[1]
Write-Host "XSC Version: $version"
continue
}
# [Signature] System Info (transcribe platform,v_major,v_minor,?,?,?
'^Transcribe!,(.+)$' {
$transcribe = $matches[1]
Write-Host "Transcribe Info: $transcribe"
continue
}
# [Header] SectionStart
'^SectionStart,(.+)$' {
if ($section) { throw "Unclosed section: $section" }
$section = $matches[1]
$data[$section] = [ordered]@{}
continue
}
# [Header] SectionEnd
'^SectionEnd,(.+)$' {
if ($matches[1] -ne $section) { throw "Unmatched SectionEnd: $($matches[1])" }
$section = $matches[1]
$section = $null
continue
}
# [Values] key,value
'^(.+?),(.+)$' {
if (-not $section) { throw "key,value not in section!: $_" }
$key, $value = $matches[1, 2]
if ($section -eq 'Loops' -and $key -eq 'L') {
# e.g. "L,15,0,0,0,,White,,0:00:00.000000,0:00:00.000000"
# use "L,15" as key, rest as value.
$i = $value.IndexOf(',')
$data[$section]['L,' + $value.Substring(0, $i)] = $value.Substring($i + 1)
}
else {
$data[$section][$key] = $value
}
continue
}
}
}
[PSCustomObject]@{
Version = $version
Transcribe = $transcribe
Data = $data
}
}
function Write-Xsc {
param ([Parameter(Mandatory)]$XSC)
# Write header
"XSC Transcribe.Document Version $($XSC.Version)"
"Transcribe!,$($XSC.Transcribe)"
# Each section is in format:
# SectionStart,<section_name>
# Key,Value
# SectionEnd,<section_name>
$data = $XSC.Data
Foreach ($section in $data.get_keys()) {
''
"SectionStart,$section"
foreach ($key in $data[$section].get_keys()) {
'{0},{1}' -f $key, $data[$section][$key]
}
"SectionEnd,$section"
}
}
## Main
function Process-Xsc {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string[]]$FullName
)
begin {
Write-Host "Starting XSC processing..."
Write-Host "Files to process: $($Paths.Count)"
}
process {
foreach ($file in $FullName) {
try {
Write-Host "Processing file: $file"
$xsc = Read-Xsc -Path $file
# The values I prefer
$xsc.Data['Main']['SaveDate'] = (Get-Date -Format 'yyyy-MM-dd HH:mm')
$xsc.Data['Main']['WindowSize'] = '1706|979|858|410,0'
$xsc.Data['Main']['ViewList'] = '1,0,0'
$xsc.Data['View0']['ShowSpectrum'] = '0'
$xsc.Data['View0']['ShowGuessNotes'] = '0'
$xsc.Data['View0']['ShowGuessChords'] = '0'
$xsc.Data['View0']['ShowAsMono'] = '1'
$xsc.Data['View0']['ShowDB'] = '0'
$xsc.Data['View0']['ViewSplitterPos'] = '0.792035'
$xsc.Data['View0']['HorizProfileZoom'] = '0.015929'
Write-Host "Writing fixed XSC file to: $file"
Write-Xsc -XSC $xsc | Out-File -LiteralPath $file
}
catch {
Write-Warning "Failed to read XSC file at path '$file': $_"
}
}
}
end {
Write-Host "Finished XSC processing."
}
}
function Invoke-WingetUpdate {
Param([switch]$Force)
if (-not (Get-Command -Name 'Get-WinGetPackageUpdate' -ErrorAction SilentlyContinue)) {
# Install-Module -Name Microsoft.WinGet.Client -RequiredVersion 0.2.1
Write-Warning 'WinGet module not installed. Skipping.'
return
}
if (-not $Force) {
if (((Get-Date) - [DateTime](Get-Content "~/.winget-lastcheck" -ErrorAction SilentlyContinue)).TotalDays -lt 7) {
'Winget update check was run recently. Skipping.'
return
}
Set-Content "~/.winget-lastcheck" -ErrorAction SilentlyContinue (Get-Date -Format 'O')
}
'Checking for winget updates'
''
$updates = Get-WinGetPackageUpdate | Where-Object { $_.Source -eq 'winget' }
if ($updates.Count -eq 0) {
return
}
'The following Winget packages have updates available:'
$updates | ForEach-Object {
"• {0} ({1} -> {2})" -f $_.ID, $_.Version, $_.Available
}
''
if (-not $Force) {
for ($i = 0; $i -lt 3; $i++) {
Write-Host -NoNewline "WinGet updates are available. Waiting for $(3-$i) second$(@('','s')[$i -le 1]), press a key to skip ...`r"
if ($Host.UI.RawUI.KeyAvailable -and $host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp,IncludeKeyDown').KeyDown) {
Write-Warning 'Updating cancelled.'
return
}
Start-Sleep -Seconds 1
}
''
}
'Found updates. Updating now.'
''
# $wingetArgs = 'update', '--all', '--source', 'winget', '--accept-package-agreements', '--accept-source-agreements', '--include-unknown'
# & sudo.exe winget.exe @wingetArgs
$updates | ForEach-Object {
"• Updating {0} ..." -f $_.ID
Update-WinGetPackage -ID $_.ID -Exact
}
}
function Invoke-ScoopUpdate {
Param([switch]$Force)
if (-not (Get-Command -Name 'scoop.ps1' -CommandType ExternalScript -ErrorAction SilentlyContinue)) {
return
}
if (-not $Force) {
if (((Get-Date) - [DateTime](Get-Content "~/.scoop-lastcheck" -ErrorAction SilentlyContinue)).TotalDays -lt 7) {
'scoop update check was run recently. Skipping.'
return
}
Set-Content "~/.scoop-lastcheck" -ErrorAction SilentlyContinue (Get-Date -Format 'O')
}
'Checking for scoop updates'
scoop update *
scoop cleanup *
# Enable completion in current shell.
# scoop install extras/scoop-completion
$modulePath = "$env:USERPROFILE\scoop\modules\scoop-completion"
if (Test-Path -LiteralPath $modulePath -PathType Container) {
'Installing scoop-completion'
Import-Module $modulePath
}
}
<#
.SYNOPSIS
Launch vscode
.DESCRIPTION
Unlike code.cmd, this supports wildcards.
.EXAMPLE
Get-ChildItem *.txt | Invoke-Code -Confirm
#>
function Invoke-Code {
# Support -Confirm, -WhatIf
[CmdletBinding(SupportsShouldProcess)]
param(
# Files to edit. Wildcards supported.
[Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]]$Path,
# Whether to create a new VSCode isntance.
[switch]$NewWindow,
# Command to launch code. Typically 'code.cmd'
[string]$CodeCommand = 'code.cmd'
)
begin {
Write-Verbose 'Invoke-Code: begin'
# Fail fast if $CodeCommand can't be located.
Get-Command -Name $CodeCommand -CommandType Application -ErrorAction Stop | Out-Null
$codeArgs = @()
}
process {
Write-Verbose 'Invoke-Code: process'
foreach ($item in $Path) {
if ($item -notmatch '[\*\?]' -and -not (Test-Path $item -PathType Leaf)) {
# Add directory names or files that don't exist as-is
if ($PSCmdlet.ShouldProcess($item, 'Edit with code')) {
Write-Verbose "Adding non-existent file: $item"
$codeArgs += $item
}
}
else {
# Support wildcards
Get-ChildItem -Path $item | Select-Object -ExpandProperty FullName | ForEach-Object {
if ($PSCmdlet.ShouldProcess($_, 'Edit with code')) {
Write-Verbose "Adding file: $_"
$codeArgs += $_
}
}
}
}
}
end {
Write-Verbose 'Invoke-Code: end'
if ($PSCmdlet.MyInvocation.BoundParameters.ContainsKey('WhatIf')) {
# -WhatIf requires no additional processing.
return
}
if ($codeArgs.Count -eq 0) {
Write-Warning 'No matching files found.'
return
}
if ($NewWindow) {
$codeArgs += '--new-window'
}
Write-Verbose "Launching $CodeCommand $codeArgs"
& $CodeCommand @codeArgs
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment