functions/helpers/Read-TrackMetadataSingle.ps1


# ------------------------------
# Single-file reader (returns PSCustomObject)
# ------------------------------
<#
.SYNOPSIS
    Reads metadata from a single audio track file.

.DESCRIPTION
    Reads metadata from an audio file using TagLib# library. Extracts standard ID3v2 tags,
    Xiph comments, and Apple freeform tags. Returns a PSCustomObject with all metadata properties.

.PARAMETER FilePath
    The full path to the audio file to read metadata from.

.EXAMPLE
    $metadata = Read-TrackMetadataSingle -FilePath 'C:\Music\Track.mp3'
    
.EXAMPLE
    $metadata = Read-TrackMetadataSingle -FilePath '/Users/username/Music/Track.aiff' -Verbose

.OUTPUTS
    PSCustomObject with properties:
    - FilePath, Container, TagTypes
    - Artist, AlbumArtist, Album, Title, Subtitle
    - Genre, Composer, Lyricist, OriginalArtist, Publisher
    - TrackNumber, Year, Comments, ISRC
    - CoverArt, Remixers, CatalogNumber, Barcode, ASIN
    - PurchaseDate, ReleaseCountry, ReleaseStatus, ReleaseType
    - DiscogsReleaseUrl, DiscogsArtistUrl
    - Plus all other tag properties found in the file

.NOTES
    Requires TagLib# assembly to be loaded before calling this function.
    Supports MP3, AIFF, FLAC, OGG, and other TagLib#-compatible formats.

#>

function Read-TrackMetadataSingle {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$FilePath
        #[string]$TagLibPath
    )


    $tf = $null
    try {
        $tf = [TagLib.File]::Create($FilePath)
        $tag = $tf.Tag
        $id3 = $tf.GetTag([TagLib.TagTypes]::Id3v2, $false)
        $xiph  = $tf.GetTag([TagLib.TagTypes]::Xiph,  $false)
        $apple = $tf.GetTag([TagLib.TagTypes]::Apple, $false)

        # Get all tag properties and their values
        Write-Verbose "Reading common tag properties for $FilePath"
        $tagProperties = @{}
        Write-Verbose "Enumerating tag properties"
        [TagLib.Tag].GetProperties() | Where-Object { $_.CanRead } | ForEach-Object {
            $propName = $_.Name
            
            try {
                $value = $tag.$propName
                Write-Verbose "$($propName) :`t $value"
                # Only include non-empty values
                if ($value -and ($value -isnot [string] -or $value.Trim() -ne '')) {
                    $tagProperties[$propName] = $value
                }
            } catch {
                # Skip properties that throw errors
            }
        }
        Write-Verbose "Completed reading common tag properties for $FilePath"
        Write-Verbose "Tag properties found: $($tagProperties.Keys -join ',`r`n ')"

        # Fill gaps from Xiph
        if ($xiph) {
            Write-Verbose "Filling missing Catalog/Commerce fields from Xiph for $FilePath"
            if (-not $catalogNumber)  { $catalogNumber  = Get-XiphField -Xiph $xiph -Keys $keysCatalog }
            if (-not $barcode)        { $barcode        = Get-XiphField -Xiph $xiph -Keys $keysBarcode }
            if (-not $asin)           { $asin           = Get-XiphField -Xiph $xiph -Keys $keysASIN }
            if (-not $purchaseDate)   { $purchaseDate   = Get-XiphField -Xiph $xiph -Keys $keysPurchase }
            if (-not $releaseCountry) { $releaseCountry = Get-XiphField -Xiph $xiph -Keys $keysRelCtry }
            if (-not $releaseStatus)  { $releaseStatus  = Get-XiphField -Xiph $xiph -Keys $keysRelStat }
            if (-not $releaseType)    { $releaseType    = Get-XiphField -Xiph $xiph -Keys $keysRelType }
            if (-not $discogsRelease) { $discogsRelease = Get-XiphField -Xiph $xiph -Keys @('DISCOGS_RELEASE','URL_DISCOGS_RELEASE_SITE') }
            if (-not $discogsArtist)  { $discogsArtist  = Get-XiphField -Xiph $xiph -Keys @('DISCOGS_ARTIST','URL_DISCOGS_ARTIST_SITE') }
        }

        # Fill gaps from Apple freeform
        if ($apple) {
            Write-Verbose "Filling missing Catalog/Commerce fields from Apple freeform for $FilePath"
            if (-not $catalogNumber)  { $catalogNumber  = Get-AppleFreeForm -Apple $apple -Keys $keysCatalog }
            if (-not $barcode)        { $barcode        = Get-AppleFreeForm -Apple $apple -Keys $keysBarcode }
            if (-not $asin)           { $asin           = Get-AppleFreeForm -Apple $apple -Keys $keysASIN }
            if (-not $purchaseDate)   { $purchaseDate   = Get-AppleFreeForm -Apple $apple -Keys $keysPurchase }
            if (-not $releaseCountry) { $releaseCountry = Get-AppleFreeForm -Apple $apple -Keys $keysRelCtry }
            if (-not $releaseStatus)  { $releaseStatus  = Get-AppleFreeForm -Apple $apple -Keys $keysRelStat }
            if (-not $releaseType)    { $releaseType    = Get-AppleFreeForm -Apple $apple -Keys $keysRelType }
            if (-not $discogsRelease) { $discogsRelease = Get-AppleFreeForm -Apple $apple -Keys @('DISCOGS_RELEASE','URL_DISCOGS_RELEASE_SITE') }
            if (-not $discogsArtist)  { $discogsArtist  = Get-AppleFreeForm -Apple $apple -Keys @('DISCOGS_ARTIST','URL_DISCOGS_ARTIST_SITE') }
        }

        Write-Verbose "Completed reading metadata for $FilePath"

        # Build result object with all tag properties
        $result = [ordered]@{
            FilePath          = $FilePath
            Container         = $tf.Properties.MediaTypes.ToString()
            TagTypes          = $tf.TagTypes.ToString()
            
            # Standard ID3v2 tags
            Artist            = $tag.FirstArtist                    # TPE1
            AlbumArtist       = $tag.FirstAlbumArtist               # TPE2
            Album             = $tag.Album                          # TALB
            Title             = $tag.Title                          # TIT2
            Subtitle          = $tag.Subtitle                       # TIT3 (Mix/Version)
            Genre             = $tag.FirstGenre                     # TCON
            Composer          = $tag.FirstComposer                  # TCOM
            Lyricist          = $null                               # TEXT (not standard property)
            OriginalArtist    = $null                               # TOPE (not standard property)
            Publisher         = $tag.Publisher                      # TPUB
            TrackNumber       = "$($tag.Track)/$($tag.TrackCount)"  # TRCK
            Year              = $tag.Year                           # TYER/TDRC
            Comments          = $tag.Comment                        # COMM
            
            # Additional metadata
            ISRC              = $tag.ISRC                           # TSRC
            CoverArt          = $null                               # APIC (embedded image)
            Remixers          = $null
            CatalogNumber     = $null
            Barcode           = $null
            ASIN              = $null
            PurchaseDate      = $null
            ReleaseCountry    = $null
            ReleaseStatus     = $null
            ReleaseType       = $null
            DiscogsReleaseUrl = $null
            DiscogsArtistUrl  = $null
        }

        # Get Lyricist (TEXT) and Original Artist (TOPE) from ID3v2 frames
        if ($id3) {
            $lyricist = Get-Id3Text -Id3 $id3 -FrameId 'TEXT'
            if ($lyricist) { $result.Lyricist = $lyricist }
            
            $originalArtist = Get-Id3Text -Id3 $id3 -FrameId 'TOPE'
            if ($originalArtist) { $result.OriginalArtist = $originalArtist }
            
            # Read custom text fields (TXXX frames)
            Write-Verbose "Reading custom text fields from ID3v2"
            $customTextFrames = $id3.GetFrames('TXXX')
            if ($customTextFrames) {
                $customTextFrames | ForEach-Object {
                    $description = $_.Description
                    $value = $_.Text[0]
                    if ($description -and $value) {
                        Write-Verbose "Found custom field: $description = $value"
                        $result[$description] = $value
                    }
                }
            }
        }

        # Get cover art
        if ($tag.Pictures -and $tag.Pictures.Count -gt 0) {
            $result.CoverArt = $tag.Pictures[0]
        }


        # Add all tag properties dynamically
        $tagProperties.GetEnumerator() | ForEach-Object {
            $result[$_.Key] = $_.Value
        }

        return [pscustomobject]$result
    }
    catch {
        Write-Warning "Failed to read metadata: $FilePath — $($_.Exception.Message)"
    }
    finally {
        if ($tf) { $tf.Dispose() }
    }
}