I noticed my wife going through some photos manually tracking which tags were used to classify different subjects so I wondered if I could write a little PowerShell that could return all the file metadata on a set of files (or the contents of a folder). That would enable me to list out all the tags we’ve used on our photo collection, filter them and generally get an idea about the organisation.
After a quick Google I found a great script here from Microsoft but there were a few additional things I wanted it to do;
- Return the information in a stream of objects so I can use the pipeline.
- Accept input from the pipeline.
- Use either a path to a file or a path to a folder; if the latter is provided recurse through all the sub-directories.
- Get more than 266 file properties. In fact, assume we don’t know how many file properties are going to be listed on the file and get them all.
Updated script and explanation follows!
[CmdletBinding()] param ( [parameter(Mandatory=$True,ValueFromPipeline=$True)] [string[]]$Path ) BEGIN { function Get-MetadataFromFolder { param ( [parameter(Mandatory=$True)] [string]$FolderPath, [string]$FileName="" ) Write-Verbose "Processing $FolderPath" Write-Verbose "FileName : $FileName" $Shell=New-Object -ComObject Shell.Application $Folder=$Shell.namespace($FolderPath) #Build the list of files to process; if the $FileName parameter is provided #only include any files with a matching filename if ($FileName -ne "") { $FileItems=$Folder.Items() | ? {$_.Path -eq $FileName} }else { $FileItems=$Folder.Items() } foreach ($FileItem in $FileItems) { #Only process files (not folders) if (Test-Path -PathType Leaf $FileItem.Path) { $Count=0 $Object=New-Object PSObject $Object | Add-Member NoteProperty FullName $FileItem.Path #Get all the file detail items of the current file and add them to an object. while($Folder.getDetailsOf($Folder.Items, $Count) -ne "") { $Object | Add-Member -Force NoteProperty ($Folder.getDetailsOf($Folder.Items, $Count)) ($Folder.getDetailsOf($FileItem, $Count)) $Count+=1 } $Object } } } # Get the metadata for all the files in a folder then recursively call itself for every child folder. function Process-SingleFolder { param ( [parameter(Mandatory=$True)] [string]$FolderPath ) Write-Verbose "Processing Folder $FolderPath" Get-MetadataFromFolder $FolderPath GCI $FolderPath -Directory | % {Process-SingleFolder $_.FullName} } } PROCESS { #Take all the paths passed from the pipeline. foreach ($SinglePath in $Path) { if (Test-Path -PathType Container $SinglePath) { Process-SingleFolder $SinglePath }elseif (Test-Path -PathType Leaf $SinglePath) { Write-Verbose "Processing File $SinglePath" $CurrentFile=Get-Item $SinglePath Get-MetadataFromFolder $CurrentFile.DirectoryName $CurrentFile.FullName }else { Write-Error "Invalid Path : $SinglePath" } } }
The script accepts a path (either from a parameter or from the pipeline) and then either gets the metadata (if it’s a file) or gets all the metadata for each child file (if the path is to a folder). Sub folders are then processed in the same way. If the input is from the pipeline, these steps are repeated for every item.
Here’s a more detailed breakdown of the code;
[CmdletBinding()] param ( [parameter(Mandatory=$True,ValueFromPipeline=$True)] [string[]]$Path ) BEGIN { function Get-MetadataFromFolder { param ( [parameter(Mandatory=$True)] [string]$FolderPath, [string]$FileName="" )
[CmdletBinding()] is first, to allow use of Write-Verbose (so there’s only output to the screen if we run it with -Verbose).
Next the script defines what parameter we want to accept. It’s the path to the file or folder we want to process so it’s marked as mandatory. Additionally, we want to accept input from the pipeline so that’s defined too.
BEGIN is used to define code that’s only run once when the script is executed instead of for every item in the pipeline. All the functions that the script will use are defined here.
Last is the definition of the Get-MetadataFromFolder function which will do all the actual work of the script. Again, we want a path to the folder we want to process but additionally I want to be able to filter the output so we only process files of a particular name (if necessary).
$Shell=New-Object -ComObject Shell.Application $Folder=$Shell.namespace($FolderPath)
This is the crux of the script. In order to get the metadata for a file (or set of files) you need to use a property of the parent folder and use the shell to get the data you need. Once you’ve got the com object of the parent folder you enumerate the items within it and get the metadata from each.
if ($FileName -ne "") { $FileItems=$Folder.Items() | ? {$_.Path -eq $FileName} }else { $FileItems=$Folder.Items() }
Because we’re forced to get all the files from a parent folder we can’t directly get the metadata from a single file. To work around this we can filter all the items from the parent folder to only get the ones matching the desired filename. If we don’t want a single file, we just get everything.
foreach ($FileItem in $FileItems) { #Only process files (not folders) if (Test-Path -PathType Leaf $FileItem.Path) {
We process every item in the collection of items we’ve created. We’re only interested in getting the metadata for files so test the current item is a leaf object in Test-Path (rather than a container object).
$Count=0 $Object=New-Object PSObject $Object | Add-Member NoteProperty FullName $FileItem.Path #Get all the file detail items of the current file and add them to an object. while($Folder.getDetailsOf($Folder.Items, $Count) -ne "") { $Object | Add-Member -Force NoteProperty ($Folder.getDetailsOf($Folder.Items, $Count)) ($Folder.getDetailsOf($FileItem, $Count)) $Count+=1 } $Object
For each item in the parent folder we enumerate through all the metadata on it. Strangely, you get the name of the metadata from the parent folder and the value from the file. The script loops through all the metadata until it gets to an empty one then stops.
For each file a new object is created and all the metadata is added to it as properties. The script also adds the path as the FullName property on the new object so we can use it if we want to.
function Process-SingleFolder { param ( [parameter(Mandatory=$True)] [string]$FolderPath ) Write-Verbose "Processing Folder $FolderPath" Get-MetadataFromFolder $FolderPath GCI $FolderPath -Directory | % {Process-SingleFolder $_.FullName} }
This function is used to enumerate all the sub folders in a passed folder path and outputting all the files within them. It calls itself with recursion.
PROCESS { #Take all the paths passed from the pipeline. foreach ($SinglePath in $Path) {
PROCESS marks the part of the script that runs for every item that is passed via the pipeline. It enumerates every string that’s passed from the pipeline or passed as an array.
if (Test-Path -PathType Container $SinglePath) { Process-SingleFolder $SinglePath }elseif (Test-Path -PathType Leaf $SinglePath) { Write-Verbose "Processing File $SinglePath" $CurrentFile=Get-Item $SinglePath Get-MetadataFromFolder $CurrentFile.DirectoryName $CurrentFile.FullName }else { Write-Error "Invalid Path : $SinglePath" }
This checks if the current item we’re processing is a path to a file, a path to a folder or an invalid path. The script then proceeds to call the correct function or error.
As this outputs all the metadata information as objects you can use it as part of a pipeline to filter.
i.e.
Get-Metadata c:\Photos | ? {$_.Tags like "*Holiday*"} | Select-Object -Property FullName, Tags
Which would list the full filenames and tags for every file in c:\Photos which is tagged with the work “Holiday”.
Hey this is fantastic. Thank you!
You’re very welcome, glad it helped!
Thanks for the script, it is great. I am wanting to modify it for use with listings of Movie files, and their extended Metadata, but not seeing the enumeration for the field named “Frame Height”. Is there a Master list of the attributes and their corresponding number? Not sure if I am wording that correctly. Thanks again.
Glad it’s useful! This is the best I could find;
https://docs.microsoft.com/en-us/windows/win32/medfound/metadata-properties-for-media-files