functions/Set-TrackMetadata.ps1
|
<# .SYNOPSIS Updates metadata tags on an audio track file. .DESCRIPTION Modifies metadata properties on an audio file using the TagLib# library. Supports standard ID3v2 properties like artist, album, title, track number, and year. Changes are saved directly to the file. You can specify commonly used tags as individual parameters or provide them in a hashtable. .PARAMETER FilePath The full path to the audio file to update. Supports MP3, AIFF, FLAC, OGG, and other TagLib#-compatible formats. .PARAMETER Metadata A hashtable containing metadata properties to update. Supported keys include: - Artist, AlbumArtist, Album, Title, Subtitle - Genre, Composer, Publisher - TrackNumber (format: "5" or "5/12" for track/total) - Year (numeric value) - Comments, ISRC - Any other TagLib tag property .PARAMETER CustomFields A hashtable of custom text fields to set as ID3v2 TXXX frames. Keys are field names (descriptions), and values are the text to store. .PARAMETER Title The title of the track. .PARAMETER Artist The artist name. .PARAMETER Album The album name. .PARAMETER AlbumArtist The album artist name. .PARAMETER Genre The genre of the track. .PARAMETER Composer The composer name. .PARAMETER Publisher The publisher name/record label. .PARAMETER TrackNumber The track number (format: "5" or "5/12" for track/total). .PARAMETER Year The year of release. .PARAMETER Comments Comments for the track. .PARAMETER ISRC The ISRC code. .PARAMETER Subtitle The subtitle or mix/version. .PARAMETER Artwork One or more TagLib.IPicture objects to embed as artwork. These can be created using Import-TrackArtwork. .PARAMETER FrontCoverPath Path to an image file to use as the front cover artwork. .PARAMETER BackCoverPath Path to an image file to use as the back cover artwork. .PARAMETER LeafletPagePath Path to an image file to use as a leaflet page. .PARAMETER BandLogoPath Path to an image file to use as the band logo. .PARAMETER PublisherLogoPath Path to an image file to use as the publisher logo. .EXAMPLE Set-TrackMetadata -FilePath 'C:\Music\Track.mp3' -Title 'New Title' -Artist 'New Artist' -Album 'New Album' -Year 2026 .EXAMPLE Set-TrackMetadata -FilePath '/Users/username/Music/Track.aiff' -Metadata @{ Title = 'Song Title' TrackNumber = '5/12' Genre = 'Techno' Comments = 'Updated' } -Verbose .EXAMPLE Set-TrackMetadata -FilePath 'C:\Music\Track.mp3' -CustomFields @{ 'CATALOG_NUMBER' = '12345-XYZ' 'BARCODE' = '0123456789012' } -Verbose .INPUTS System.String (FilePath via pipeline) .OUTPUTS None. Writes success/error messages to host. .NOTES - Requires TagLib# assembly to be loaded before calling this function. - Changes are saved immediately to the file. - TrackNumber format: use "5" for track only, or "5/12" for track/total count. - Arrays like Performers and Genres accept single values and convert automatically. - Properties not in the mapping are attempted as direct property assignments. - Individual tag parameters override values in the Metadata hashtable if both are provided. #> function Set-TrackMetadata { [CmdletBinding()] param( [Parameter(Mandatory)][string]$FilePath, [hashtable]$Metadata = @{}, [hashtable]$CustomFields = @{}, [string]$Title, [string]$Artist, [string]$Album, [string]$AlbumArtist, [string]$Genre, [string]$Composer, [string]$Publisher, [string]$TrackNumber, [string]$Year, [string]$Comments, [string]$ISRC, [string]$Subtitle, [TagLib.IPicture[]]$Artwork, [string]$FrontCoverPath, [string]$BackCoverPath, [string]$LeafletPagePath, [string]$BandLogoPath, [string]$PublisherLogoPath ) # Merge individual parameters into Metadata hashtable if provided $mergedMetadata = @{} if ($Metadata) { $mergedMetadata = $Metadata.Clone() } if ($Title) { $mergedMetadata['Title'] = $Title } if ($Artist) { $mergedMetadata['Artist'] = $Artist } if ($Album) { $mergedMetadata['Album'] = $Album } if ($AlbumArtist) { $mergedMetadata['AlbumArtist'] = $AlbumArtist } if ($Genre) { $mergedMetadata['Genre'] = $Genre } if ($Composer) { $mergedMetadata['Composer'] = $Composer } if ($Publisher) { $mergedMetadata['Publisher'] = $Publisher } if ($TrackNumber) { $mergedMetadata['TrackNumber'] = $TrackNumber } if ($Year) { $mergedMetadata['Year'] = $Year } if ($Comments) { $mergedMetadata['Comments'] = $Comments } if ($ISRC) { $mergedMetadata['ISRC'] = $ISRC } if ($Subtitle) { $mergedMetadata['Subtitle'] = $Subtitle } $tf = $null try { $tf = [TagLib.File]::Create($FilePath) $tag = $tf.Tag $id3 = $tf.GetTag([TagLib.TagTypes]::Id3v2, $true) Write-Verbose "Updating metadata for $FilePath" $propertyMap = @{ 'Artist' = { param($tag, $value) $tag.Performers = @($value) } 'AlbumArtist' = { param($tag, $value) $tag.AlbumArtists = @($value) } 'Album' = { param($tag, $value) $tag.Album = $value } 'Title' = { param($tag, $value) $tag.Title = $value } 'Subtitle' = { param($tag, $value) $tag.Subtitle = $value } 'Genre' = { param($tag, $value) $tag.Genres = @($value) } 'Composer' = { param($tag, $value) $tag.Composers = @($value) } 'Publisher' = { param($tag, $value) $tag.Publisher = $value } 'TrackNumber' = { param($tag, $value) $parts = $value -split '/' $tag.Track = [uint]$parts[0] if ($parts.Count -gt 1) { $tag.TrackCount = [uint]$parts[1] } } 'Year' = { param($tag, $value) $tag.Year = [uint]$value } 'Comments' = { param($tag, $value) $tag.Comment = $value } 'ISRC' = { param($tag, $value) $tag.ISRC = $value } } # Update standard properties if ($mergedMetadata.Count -gt 0) { Write-Verbose "Setting standard metadata properties" $mergedMetadata.GetEnumerator() | ForEach-Object { $propName = $_.Key $propValue = $_.Value if ($propertyMap.ContainsKey($propName)) { Write-Verbose "Setting $propName = $propValue" & $propertyMap[$propName] $tag $propValue } else { try { Write-Verbose "Setting $propName = $propValue (direct)" $tag.$propName = $propValue } catch { Write-Warning "Could not set property $propName : $_" } } } } # Update custom fields (TXXX frames) if ($CustomFields.Count -gt 0 -and $id3) { Write-Verbose "Setting custom fields" $CustomFields.GetEnumerator() | ForEach-Object { Set-Id3CustomText -Id3 $id3 -Description $_.Key -Text $_.Value } } # Handle artwork $newPictures = [System.Collections.Generic.List[TagLib.IPicture]]::new() # Add any existing artwork passed directly if ($Artwork) { Write-Verbose "Adding $($Artwork.Count) artwork picture(s) from Artwork parameter" $newPictures.AddRange($Artwork) } # Import artwork from file paths for specific picture types if ($FrontCoverPath) { Write-Verbose "Importing front cover from $FrontCoverPath" $newPictures.Add((Import-TrackArtwork -FilePath $FrontCoverPath -PictureType FrontCover)) } if ($BackCoverPath) { Write-Verbose "Importing back cover from $BackCoverPath" $newPictures.Add((Import-TrackArtwork -FilePath $BackCoverPath -PictureType BackCover)) } if ($LeafletPagePath) { Write-Verbose "Importing leaflet page from $LeafletPagePath" $newPictures.Add((Import-TrackArtwork -FilePath $LeafletPagePath -PictureType LeafletPage)) } if ($BandLogoPath) { Write-Verbose "Importing band logo from $BandLogoPath" $newPictures.Add((Import-TrackArtwork -FilePath $BandLogoPath -PictureType BandLogo)) } if ($PublisherLogoPath) { Write-Verbose "Importing publisher logo from $PublisherLogoPath" $newPictures.Add((Import-TrackArtwork -FilePath $PublisherLogoPath -PictureType PublisherLogo)) } # Apply artwork if any were provided, preserving existing artwork of different types if ($newPictures.Count -gt 0) { # Get the picture types we're adding/replacing $newPictureTypes = $newPictures | ForEach-Object { $_.Type } # Keep existing pictures that are NOT of the same type as new pictures $existingPictures = $tag.Pictures $preservedPictures = @() if ($existingPictures -and $existingPictures.Count -gt 0) { $preservedPictures = @($existingPictures | Where-Object { $newPictureTypes -notcontains $_.Type }) if ($preservedPictures.Count -gt 0) { Write-Verbose "Preserving $($preservedPictures.Count) existing picture(s) of other types" } } # Combine preserved existing pictures with new pictures $finalPictures = [System.Collections.Generic.List[TagLib.IPicture]]::new() foreach ($preserved in $preservedPictures) { $finalPictures.Add($preserved) } foreach ($newPic in $newPictures) { $finalPictures.Add($newPic) } Write-Verbose "Setting $($finalPictures.Count) picture(s) on track ($($newPictures.Count) new, $($preservedPictures.Count) preserved)" $tag.Pictures = $finalPictures.ToArray() } Write-Verbose "Saving metadata to $FilePath" $tf.Save() Write-Host "Successfully updated metadata for $FilePath" } catch { Write-Error "Failed to update metadata: $FilePath — $($_.Exception.Message)" } finally { if ($tf) { $tf.Dispose() } } } |