2

I'm pretty new working with Powershell and i have some working code but I'm not sure how to get it into an efficient routine to return all of the extended file properties of some video files i have.

I have:

# The basic setup for the next steps
$path = 'C:\test\videotocheck.mp4'
$shell = New-Object -COMObject Shell.Application
$folder = Split-Path $path
$file = Split-Path $path -Leaf
$shellfolder = $shell.Namespace($folder)
$shellfile = $shellfolder.ParseName($file)

# This command gets a list of all the extended attributes available for this file
0..500 | Foreach-Object { '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_) }

# These commands get the individual attributes picked out of the list above
$shellfolder.GetDetailsOf($shellfile, 314)
$shellfolder.GetDetailsOf($shellfile, 316)

All i want to do is provide a filename and have it give me a list back of all of the attributes and their values (if they have one.)

Example: Example Output

I intend to use this in a SQL stored procedure. I can work with different types of output if that's easier.

I'm mostly interested in dimensions

Any guidance would be appreciated.

jimerb
  • 67
  • 9

2 Answers2

2

To get all of this extended metadate you could use the function below. You can give it the path to a single file, or the path to a folder where the files are.

function Get-MetaData {
    [CmdletBinding()]
    [OutputType([Psobject[]])]
    Param (
        # Path can be the path to a folder or the full path and filename of a single file
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [string]$Path,

        # Pattern is unused if Path is pointing to a single file
        [Alias('Filter')]
        [string]$Pattern = '*.*',

        [Alias('Indices')]
        [int[]]$Properties = 1..500,

        # Recurse is unused if Path is pointing to a single file
        [switch]$Recurse,
        [switch]$IncludeEmptyProperties
    )
    $item = Get-Item -Path $Path -ErrorAction SilentlyContinue
    if (!$item) { Write-Error "$Path could not be found."; return }
    if (!$item.PSIsContainer) {
        # it's a file
        $files = @($item)
        $Path  = $item.DirectoryName
    }
    else {
        # it's a folder
        $files = Get-ChildItem -Path $Path -Filter $Pattern -File -Recurse:$Recurse
    }
    $shell  = New-Object -ComObject "Shell.Application"
    $objDir = $shell.NameSpace($Path)

    foreach($file in $files) {
        $objFile    = $objDir.ParseName($file.Name)
        $mediaFile  = $objDir.Items()

        foreach($index in $Properties) {
            $name  = $objDir.GetDetailsOf($mediaFile, $index)
            if (![string]::IsNullOrWhiteSpace($name)) { 
                $value = $objDir.GetDetailsOf($objFile, $index)
                if (![string]::IsNullOrWhiteSpace($value) -or $IncludeEmptyProperties) {
                    [PsCustomObject]@{
                        Path     = $file.FullName
                        Index    = $index
                        Property = $name
                        Value    = $value
                    }
                }
            }
        }
    }
    # clean-up Com objects
    $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objFile)
    $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objDir)
    $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell)
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()
}

You can of course play around with the different parameters the function can take like -Pattern '*.mp4' to only list properties for mp4 files or add switch -IncludeEmptyProperties to also list properties that exist for that file type, but have no value for the specified file.

With parameter Properties you can give the function an array of int32 values of the property indices to return. If you leave that open, the function tries to get all properties from index 1 to index 500 (if available).

Most of the interesting property indices can be found at:

  • audio/video files: 0,1,2,3,4,5,9,11,12,13,14,15,16,17,18,19,20,21,22,26,27,28,36,164,165,194,213,220,223,237,243
  • font files:
    0,1,2,3,4,5,20,21,25,33,34,164,165,166,196,310
  • image files:
    0,1,2,3,4,5,9,11,31,164,165,174,175,176,177,178,194,196

Use it like this:

$result = Get-MetaData -Path '<pathToTheFile_OR_pathToTheFolder>'

# output to GridView
$result | Out-GridView

# output to CSV file
$result | Export-Csv -Path '<pathToTheOutput.csv>' -NoTypeInformation
Theo
  • 57,719
  • 8
  • 24
  • 41
  • Wow! So cool? I'm pretty new to using these scripts. Can i call this from a "exec xp_cmdshell "powershell.... " in SQL? Maybe with Import-Module? – jimerb Dec 22 '20 at 21:17
  • @jimerb I cannot test this with`xp_cmdshell "powershell.... `, so I guess you just have to try this out yourself. Please let me know. – Theo Dec 22 '20 at 22:11
1

Here's a simplified version similar to Theo's. It will accept file/folder paths or objects via pipeline or as parameter.

Function Get-FileMetaData {
    [cmdletbinding()]
    Param
    (
        [parameter(valuefrompipeline,ValueFromPipelineByPropertyName,Position=1,Mandatory)]
        $InputObject
    )

    begin
    {
        $shell = New-Object -ComObject Shell.Application
    }

    process
    {
        foreach($object in $InputObject)
        {
            if($object -is [string])
            {
                try
                {
                    $object = Get-Item $object -ErrorAction Stop
                }
                catch
                {
                    Write-Warning "Error while processing $object : $($_.exception.message)"
                    break
                }
            }

            try
            {
                Test-Path $object -ErrorAction Stop
            }
            catch
            {
                Write-Warning "Error while processing $($object.fullname) : $($_.exception.message)"
                break
            }

            switch ($object)
            {
                {$_ -is [System.IO.DirectoryInfo]}{
                    write-host Processing folder $object.FullName -ForegroundColor Cyan
                    $currentfolder = $shell.namespace($object.FullName)
                    $items = $currentfolder.items()
                }
                {$_ -is [System.IO.FileInfo]}{
                    write-host Processing file $object.FullName -ForegroundColor Cyan
                    $parent = Split-Path $object
                    $currentfolder = $shell.namespace($parent)
                    $items = $currentfolder.ParseName((Split-Path $object -Leaf))
                }
            }

            try
            {
                foreach($item in $items)
                {
                    0..512 | ForEach-Object -Begin {$ht = [ordered]@{}}{
                        if($value = $currentfolder.GetDetailsOf($item,$_))
                        {
                            if($propname = $currentfolder.GetDetailsOf($null,$_))
                            {
                                $ht.Add($propname,$value)
                            }
                        
                        }    
                    } -End {[PSCustomObject]$ht}
                }
            }
            catch
            {
                Write-Warning "Error while processing $($item.fullname) : $($_.exception.message)"
            }
        }
    }

    end
    {
        $shell = $null
    }
}
Doug Maurer
  • 8,090
  • 3
  • 12
  • 13
  • I've got this working!! I'm pretty new to using these scripts. Can i call this from a "exec xp_cmdshell "powershell.... " in SQL? Maybe with Import-Module? – jimerb Dec 22 '20 at 21:16