In Part 1 of trying to tame my photos, I tried to wrangle my horribly organized photo library. My organization skills were lacking, like many, when it comes to photos. We take them, we import them or save them and then we… forget about them. That’s a problem when I’m trying to handle large amounts of them (22.8 GB in 13,316 files) and organize them in ways that seem sensible. To use this we leverage the metadata tags that describe the date and time the photo was taken.
Now it’s time to work on the actual mechanics of the organization process.
The Pseudocode
- Cycle through each image file
- Extract the Tag information
- Extract out the year and month of the date the photo was taken
- See if there’s a folder matching that year and month, if not create it
- Check to see if a picture with the same file name already exists in the destination
- If the source and destination are the same then we’ve already moved this one, so skip it
- If it does, then it’s a duplicate and we should log and skip it
- If it does not, then we should move the existing one
- If an error is encountered when trying to build the tag, then the image probably wasn’t taken with a camera (a scan or a screenshot), log it, but don’t move it.
The Actual Code
Let’s start by adding the library we used to extract tags in Part 1.
$TagLibrary = "$env:UserProfile\OneDrive\Scripts\_includes\taglib-sharp.dll"
[System.Reflection.Assembly]::LoadFile($TagLibrary) | Out-Null
Now we need to know on which files to operate. For me, that’s stored in my OneDrive, so I use the Get-ChildItem to get a list of the files on which we want to operate.
$PhotoPath = Get-Item -Path "$env:UserProfile\OneDrive\Pictures"
$Photos = Get-ChildItem -Path $PhotoPath -Recurse -File -Include *.jpg
Time to setup some empty collections for the Duplicates and Errors that are found.
$Duplicates = @()
$ErrorFiles = @()
Let’s setup the loop. I constantly go back and forth between using ForEach and For loops. When I drafted this up I was in a “For” mood, so that’s how I did it.
For ( $i = 0; $i -lt $Photos.Count; $i++ )
{
Now I setup a try..catch..finally block for trying to extract the tag data. I’m fulling expecting errors, so that’s why I wrapped this is the try block.
try
{
$Media = [Taglib.file]::Create( ( $Photos[$i].FullName ) )
Write-Host "Extracting data from $( $Photos[$i].FullName ) was successful!"
<#
# More stuff here to come
#>
}
catch
{
Write-Host "Error extracting tag information from $( $Photos[$i].FullName )" -ForegroundColor Red
$ErrorFiles += $Photos[$i]
}
I’m also closing out the for loop here and we can get a quick check on the logic.
This makes the full script to this point something like this:
$TagLibrary = "$env:UserProfile\OneDrive\Scripts\_includes\taglib-sharp.dll"
[System.Reflection.Assembly]::LoadFile($TagLibrary) | Out-Null
$PhotoPath = Get-Item -Path "$env:UserProfile\OneDrive\Pictures"
$Photos = Get-ChildItem -Path $PhotoPath -Recurse -File -Include *.jpg
$Duplicates = @()
$ErrorFiles = @()
For ( $i = 0; $i -lt $Photos.Count; $i++ )
{
try
{
$Media = [Taglib.file]::Create( ( $Photos[$i].FullName ) )
Write-Host "Extracting data from $( $Photos[$i].FullName ) was successful!"
}
catch
{
Write-Host "Error extracting tag information from $( $Photos[$i].FullName )" -ForegroundColor Red
$ErrorFiles += $Photos[$i]
}
}
Now that we can confirm that we can get some data from the files, let’s move onto the other stuff.
Let’s extract out the date and time from the metadata. Inside the try block we’re going to work with this metadata.
$DateString = $Media.ImageTag.DateTime.ToString("yyyy/MM")
$NewDirectory = ( Join-Path -Path $PhotoPath -ChildPath $DateString )
Now we have a path for the new directory, but that directory may (yet) not exist. So next we’ll check for existence and create if it’s not there.
if ( -not ( Test-Path -Path $NewDirectory -ErrorAction SilentlyContinue ) )
{
$NewDirectory = New-Item -Type Directory -Path $NewDirectory -Force
}
Next, I check to see if the new directory is the same as the existing (meaning that I’ve already run this script). If it is, then I skip it.
if ( $NewDirectory -ne $Photos[$i].DirectoryName )
{
# Explained in the next section
}
else
{
Write-Host "$( $Photos[$i].Name ) already in $NewDirectory Folder" -ForegroundColor Yellow
}
What do we do for the files we need to move? Well, we move them of course. This portion is within the if block above.
Write-Host "Moving $( $Photos[$i].FullName ) to $NewDirectory" -ForegroundColor Green
if ( -not ( Test-Path -Path ( Join-Path -Path $NewDirectory -ChildPath $Photos[$i].Name ) -ErrorAction SilentlyContinue ) )
{
Move-Item -Path $Photos[$i] -Destination $NewDirectory
}
else
{
Write-Host "File already exists!" -ForegroundColor Red
$Duplicates += $Photos[$i]
}
At the end of the day, this does the moves and keeps a list of duplicates and errors for me to review later.
So my photos folders go from this:
To this:
For my complete script, I also added some progress bars because I like the way they look.
$TagLibrary = "$env:UserProfile\OneDrive\Scripts\_includes\taglib-sharp.dll"
[System.Reflection.Assembly]::LoadFile($TagLibrary) | Out-Null
$PhotoPath = Get-Item -Path "$env:UserProfile\OneDrive\Pictures"
$Photos = Get-ChildItem -Path $PhotoPath -Recurse -File -Include *.jpg
$Duplicates = @()
$ErrorFiles = @()
For ( $i = 0; $i -lt $Photos.Count; $i++ )
{
Write-Progress -Activity "Organizing Photos" -CurrentOperation "Analyzing $( $Photos[$i].Name )" -PercentComplete ( ( $i / $Photos.Count ) * 100 ) -Status "$( ( $i * 100 / $Photos.Count ).ToString("0.00") )% Complete"
try
{
$Media = [Taglib.file]::Create( ( $Photos[$i].FullName ) )
$DateString = $Media.ImageTag.DateTime.ToString("yyyy/MM")
$NewDirectory = ( Join-Path -Path $PhotoPath -ChildPath $DateString )
if ( -not ( Test-Path -Path $NewDirectory -ErrorAction SilentlyContinue ) )
{
$NewDirectory = New-Item -Type Directory -Path $NewDirectory -Force
}
if ( $NewDirectory -ne $Photos[$i].DirectoryName )
{
Write-Host "Moving $( $Photos[$i].FullName ) to $NewDirectory" -ForegroundColor Green
if ( -not ( Test-Path -Path ( Join-Path -Path $NewDirectory -ChildPath $Photos[$i].Name ) -ErrorAction SilentlyContinue ) )
{
Move-Item -Path $Photos[$i] -Destination $NewDirectory
}
else
{
Write-Host "File already exists!" -ForegroundColor Red
$Duplicates += $Photos[$i]
}
}
else
{
Write-Host "$( $Photos[$i].Name ) already in $NewDirectory Folder" -ForegroundColor Yellow
}
}
catch
{
Write-Host "Error extracting tag information from $( $Photos[$i].FullName )" -ForegroundColor Red
$ErrorFiles += $Photos[$i]
}
}
Write-Progress -Activity "Organizing Photos" -Completed
#region Dealing with Duplicate Photos
#
# This is yet to be done because I don't have duplicate photos in my library.
# Most likely I would do an MD5 Hash against the duplicates to see if they are actually duplicates.
# If they are duplicates, then delete one
# If they are not, come up with a clever naming convention to indicate duplicates
# Because calculating file hashes is an expensive (CPU) process, this is a last resort.
#
#endregion Dealing with Duplicate Photos
#region Dealing with Errors reading tags
<#
This is most likely due to the images being scanned/downloaded or something else that isn't a camera
#>
if ( $ErrorFiles )
{
Write-Host "Moving Error Files to Error Directory"
if ( -not ( Test-Path -Path ( Join-Path -Path $PhotoPath -ChildPath "ErrorFiles" ) -ErrorAction SilentlyContinue ) )
{
$ErrorDirectory = New-Item -ItemType Directory -Path ( Join-Path -Path $PhotoPath -ChildPath "ErrorFiles" )
}
$ErrorFiles | Move-Item -Destination $ErrorDirectory -Force
}
#endregion Dealing with Errors reading tags
Well, that’s enough for today. If someone has a clever way to handle duplicates, I’d love to gain a better understanding. Until next time ramblers!
I found this page today while searching for a certain solution…… I would like to scan a folder path contain photos and put the folder name into the photo tags. My folders are organized / named by subject, so adding that folder name into the metadata will help take the organization of photos much better.
Pretty sure your can add an additional tag by using
$Media.Tags.Add($Photos[$i].DirectoryName)
$Media.Save($Photos[$i].FullName)
The first line adds the current directory name as a tag.
The second line saves the file with that new tag.