Skip to content

Instantly share code, notes, and snippets.

@joshooaj
Created September 4, 2025 19:20
Show Gist options
  • Select an option

  • Save joshooaj/5be5bd624d70167a1e10696ccbb01726 to your computer and use it in GitHub Desktop.

Select an option

Save joshooaj/5be5bd624d70167a1e10696ccbb01726 to your computer and use it in GitHub Desktop.
A mix of PowerShell functions for working with my music library. Work in progress...
$script:itemFields = @'
acoustid_fingerprint
acoustid_id
added
album
album_id
albumartist
albumartist_credit
albumartist_sort
albumdisambig
albumstatus
albumtype
albumtypes
arranger
artist
artist_credit
artist_sort
asin
bitdepth
bitrate
bpm
catalognum
channels
comments
comp
composer
composer_sort
country
day
disc
discogs_albumid
discogs_artistid
discogs_labelid
disctitle
disctotal
encoder
filesize
format
genre
grouping
id
initial_key
isrc
label
language
length
lyricist
lyrics
mb_albumartistid
mb_albumid
mb_artistid
mb_releasegroupid
mb_releasetrackid
mb_trackid
mb_workid
media
month
mtime
original_day
original_month
original_year
path
r128_album_gain
r128_track_gain
releasegroupdisambig
rg_album_gain
rg_album_peak
rg_track_gain
rg_track_peak
samplerate
script
singleton
style
title
track
trackdisambig
tracktotal
work
work_disambig
year
'@ -split "`n"
$script:albumFields = @'
added
album
albumartist
albumartist_credit
albumartist_sort
albumdisambig
albumstatus
albumtotal
albumtype
albumtypes
artpath
asin
catalognum
comp
country
day
discogs_albumid
discogs_artistid
discogs_labelid
disctotal
genre
id
label
language
mb_albumartistid
mb_albumid
mb_releasegroupid
month
original_day
original_month
original_year
path
r128_album_gain
releasegroupdisambig
rg_album_gain
rg_album_peak
script
style
year
'@ -split "`n"
function Get-BeetsTrackInfo {
[CmdletBinding()]
param (
[Parameter()]
[string[]]
$Field = @('albumartist', 'album', 'title', 'length', 'genre', 'path'),
[Parameter()]
[string]
$Query,
[Parameter(DontShow)]
[string]
$FieldSeparator = ';;'
)
process {
# Example: Field = albumartist, album, title, length, genre, path
# Format: $albumartist;;$album;;$title;;$length;;$genre;;$path
$format = ($Field | ForEach-Object { "`$$($_.ToLower())" } ) -join ';;'
beet ls '-f' $format $Query | ForEach-Object {
$parts = $_ -split $FieldSeparator
if ($parts.Count -ne $Field.Count) {
throw "The number of fields parsed ($($parts.Count)) doesn't match the supplied number of fields ($($Field.Count)) when splitting fields with '$FieldSeparator'. Try providing an alternative FieldSeparator value."
}
$trackInfo = [ordered]@{}
for ($i = 0; $i -lt $Field.Count; $i++) {
if ($Field[$i] -eq 'length') {
$trackInfo[$Field[$i]] = New-TimeSpan -Seconds ([math]::Round($parts[$i]))
} else {
$trackInfo[$Field[$i]] = $parts[$i]
}
}
[pscustomobject]$trackInfo
}
}
}
function Get-BeetsLyrics {
[CmdletBinding()]
param (
[Parameter(ValueFromPipelineByPropertyName)]
[Alias('AlbumArtist')]
[string]
$Artist,
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Album,
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Title,
[Parameter(ValueFromPipelineByPropertyName)]
[timespan]
$Length,
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Path,
[Parameter()]
[switch]
$Save,
[Parameter()]
[switch]
$Embed
)
process {
$uriBuilder = [uribuilder]'https://lrclib.net/api/get'
$query = [System.Web.HttpUtility]::ParseQueryString('')
$query.Add('artist_name', $Artist)
$query.Add('album_name', $Album)
$query.Add('track_name', $Title)
$query.Add('duration', [int]$Length.TotalSeconds)
$uriBuilder.Query = $query.ToString()
$response = Invoke-RestMethod -Uri $uriBuilder.Uri -UserAgent $script:USERAGENT -ErrorAction Stop
if ($Save) {
$saved = $false
if ([string]::IsNullOrWhiteSpace($Path) -or !(Test-Path -LiteralPath $Path)) {
Write-Error "Unable to save lyrics - the provided path does not exist: $Path"
} else {
$item = Get-Item -LiteralPath $Path
$fileName = Join-Path $item.DirectoryName "$($item.BaseName).lrc"
if (Test-Path -LiteralPath $fileName) {
Write-Error "Lyrics already available for $Artist/$Album/$Title"
} else {
if (![string]::IsNullOrWhiteSpace($response.syncedLyrics)) {
$response.syncedLyrics | Set-Content -LiteralPath $fileName
$saved = $true
} elseif (![string]::IsNullOrWhiteSpace($response.plainLyrics)) {
$response.plainLyrics | Set-Content -LiteralPath $fileName
$saved = $ture
} else {
Write-Error "Lyrics unavailable for the provided track."
}
}
if ($saved -and $Embed) {
metaflac --remove-tag=LYRICS $Path
metaflac --set-tag-from-file="LYRICS=$fileName" $Path
}
}
}
$response
}
}
function Get-LrcLyrics {
[CmdletBinding()]
param (
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Artist,
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Album,
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Title,
[Parameter(ValueFromPipelineByPropertyName)]
[timespan]
$Length,
[Parameter(ValueFromPipelineByPropertyName)]
[string]
$Path,
[Parameter()]
[switch]
$Save,
[Parameter()]
[switch]
$Embed
)
process {
#?artist_name=Borislav+Slavov&track_name=I+Want+to+Live&album_name=Baldur%27s+Gate+3+(Original+Game+Soundtrack)&duration=233
$uriBuilder = [uribuilder]'https://lrclib.net/api/get'
$query = [System.Web.HttpUtility]::ParseQueryString('')
$query.Add('artist_name', $Artist)
$query.Add('album_name', $Album)
$query.Add('track_name', $Title)
$query.Add('duration', [int]$Length.TotalSeconds)
$uriBuilder.Query = $query.ToString()
$response = Invoke-RestMethod -Uri $uriBuilder.Uri -ErrorAction Stop
if ($Save) {
$saved = $false
if ([string]::IsNullOrWhiteSpace($Path) -or !(Test-Path -LiteralPath $Path)) {
Write-Error "Unable to save lyrics - the provided path does not exist: $Path"
} else {
$item = Get-Item -LiteralPath $Path
$fileName = Join-Path $item.DirectoryName "$($item.BaseName).lrc"
if (Test-Path -LiteralPath $fileName) {
Write-Error "Lyrics already available for $Artist/$Album/$Title"
} else {
if (![string]::IsNullOrWhiteSpace($response.syncedLyrics)) {
$response.syncedLyrics | Set-Content -LiteralPath $fileName
$saved = $true
} elseif (![string]::IsNullOrWhiteSpace($response.plainLyrics)) {
$response.plainLyrics | Set-Content -LiteralPath $fileName
$saved = $ture
} else {
Write-Error "Lyrics unavailable for the provided track."
}
}
if ($saved -and $Embed) {
metaflac --remove-tag=LYRICS $Path
metaflac --set-tag-from-file="LYRICS=$fileName" $Path
}
}
}
$response
}
}
function EmbedLyrics {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string]
$Path
)
process {
$ErrorActionPreference = 'Stop'
foreach ($flacFile in Get-ChildItem -Path $Path -Recurse -File -Filter '*.flac') {
$lrcFile = Join-Path $flacFile.DirectoryName "$($flacFile.BaseName).lrc"
if (!(Test-Path -LiteralPath $lrcFile)) {
Write-Verbose "No .lrc file found for $flacFile"
continue
}
Write-Verbose "Embedding lyrics: $flacFile"
metaflac --remove-tag=LYRICS $flacFile.FullName
metaflac --set-tag-from-file="LYRICS=$lrcFile" $flacFile.FullName
}
}
}
function Get-MBRating {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]
$Path,
[Parameter()]
[switch]
$Force
)
begin {
$lastResponseTime = [datetime]::MinValue
}
process {
$tags = Get-FlacTags -Path $Path
if (!$Force -and $tags.'MUSICBRAINZ_RATING') {
Write-Verbose "Rating already available for $Path"
return
}
$trackId = ($tags).MUSICBRAINZ_TRACKID
if ([string]::IsNullOrWhiteSpace($trackId)) {
Write-Warning "No MUSICBRAINZ_TRACKID value found for $Path"
return
}
if ($lastResponseTime -gt (Get-Date).AddMilliseconds(-200)) {
Start-Sleep -Milliseconds 200
}
$response = Invoke-RestMethod "https://musicbrainz.org/ws/2/recording/$($trackId)?fmt=json&inc=ratings" -UserAgent $script:USERAGENT -Verbose:$false
$lastResponseTime = Get-Date
$rating = $response.rating
if ($rating.'votes-count' -lt 2) {
Write-Verbose "Ignoring rating with low vote count."
return
}
Set-FlacTag -Path $Path -Name MUSICBRAINZ_RATING -Value $rating.Value
[pscustomobject]@{
Artist = $tags.'Album_Artist'
Album = $tags.Album
Title = $tags.Title
Rating = $rating.Value
}
}
end {
}
}
function Set-FlacTag {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]
$Path,
[Parameter(Mandatory)]
[string]
$Name,
[Parameter(Mandatory)]
[string]
$Value
)
process {
metaflac "--remove-tag=$Name" $Path
metaflac "--set-tag=$Name=$Value" $Path
}
}
function Get-FlacTags {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
[string]
$Path,
[Parameter()]
[switch]
$IncludeEmptyTags
)
process {
foreach ($filePath in (Resolve-Path -Path $Path).Path) {
$tags = [ordered]@{}
metaflac --show-all-tags $filePath | ForEach-Object {
$key, $value = $_ -split '=', 2
if ([string]::isnullorempty($value) -and !$IncludeEmptyTags) { return }
if ($tags.Contains($key)) {
$tags[$key] = $tags[$key] + " " + $value
} else {
$tags[$key] = $value
}
}
$tags
}
}
}
Register-ArgumentCompleter -CommandName Get-BeetsTrackInfo -ParameterName Field -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
# Trim single, or double quotes from the start/end of the word to complete.
if ($wordToComplete -match '^[''"]') {
$wordToComplete = $wordToComplete.Trim($Matches.Values[0])
}
# Get all unique process names starting with the characters provided, if any.
$script:itemFields | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
# Wrap the completion in single quotes if it contains any whitespace.
if ($_ -match '\s') {
"'{0}'" -f $_
} else {
$_
}
}
}
$script:USERAGENT = "PowerShell/$($PSVersionTable.PSVersion) ( joshooaj )"
Export-ModuleMember -Function Get-BeetsTrackInfo, Get-BeetsLyrics, Get-FlacTags, Get-MBRating
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment