Skip to content

Instantly share code, notes, and snippets.

@MagicAndi
Created July 21, 2021 21:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MagicAndi/cdc3c9b292f81d41054983f92e8dbc42 to your computer and use it in GitHub Desktop.
Save MagicAndi/cdc3c9b292f81d41054983f92e8dbc42 to your computer and use it in GitHub Desktop.
A PowerShell script to organise my photo backups.
# =============================================================================
#
# Script Name: Organise-Photos
#
# Author: Andy Parkhill
#
# Date Created: 09/07/2021
#
# Description: A script to organize my photos (and backups) into a coherent
# file structure, and remove duplicates, etc.
#
# Usage: .\Organise-Photos.ps1 -sourceDirectory C:\Temp -targetDirectory D:\Photos
#
# =============================================================================
# =============================================================================
# Parameters
# =============================================================================
Param
(
[Parameter(Position=0,Mandatory=$true, HelpMessage='The source directory path.')]
[string]$sourceDirectory,
[Parameter(Position=1,Mandatory=$false, HelpMessage='The target directory path.')]
[string]$targetDirectory=''
)
# =============================================================================
# Constants
# =============================================================================
Set-Variable ScriptName -option Constant -value "Organise-Photos"
# =============================================================================
# Script Variables
# =============================================================================
$ErrorActionPreference = "Stop"
# =============================================================================
# Functions
# =============================================================================
# Taken from https://gist.github.com/woehrl01/5f50cb311f3ec711f6c776b2cb09c34e
function Get-FileMetaData
{
param
(
[Parameter(Mandatory=$True)]
[System.IO.FileInfo] $file = $(throw "Parameter -file is required.")
)
if(!(Test-Path -Path $file.Fullname))
{
throw "File does not exist: $($file.Fullname)"
Exit 1
}
$pathname = $file.DirectoryName
$filename = $file.Name
$hash = @{}
try{
$shellobj = New-Object -ComObject Shell.Application
$folderobj = $shellobj.namespace($pathname)
$fileobj = $folderobj.parsename($filename)
for($i=0; $i -le 294; $i++)
{
$name = $folderobj.getDetailsOf($null, $i);
if($name){
$value = $folderobj.getDetailsOf($fileobj, $i);
if($value){
$hash[$($name)] = $($value)
}
}
}
}finally{
if($shellObject){
[System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$shellObject) | out-null
}
}
return New-Object PSObject -Property $hash
}
function Get-DateTaken
{
param
(
[Parameter(Mandatory=$True)]
[PSCustomObject] $metadata = $(throw "Parameter -metadata is required.")
)
$dateTaken = $metadata.'Date taken'
if(! $dateTaken)
{
$dateTaken = $metadata.'Date created'
}
$dateTaken = $dateTaken -replace '[^a-zA-z0-9/: ]', ''
$dateTaken = $dateTaken -replace '/', '-'
$dateTaken = $dateTaken -replace ':', ''
$dateTaken = $dateTaken -replace ' ', '-'
return $dateTaken
}
function Get-ImageSize
{
param
(
[Parameter(Mandatory=$True)]
[PSCustomObject] $metadata = $(throw "Parameter -metadata is required.")
)
$dimensions = ''
if ($metadata.Dimensions -ne $null) {
$dimensions = $metadata.Dimensions -replace '[^a-zA-z0-9]', ''
}
return $dimensions
}
function Get-HashesForImagesInTargetDirectory
{
Write-Host "Entering Get-HashesForImagesInTargetDirectory()"
$existingImages = Get-ChildItem -Path $targetDirectory -File -Recurse
$hashes = @{}
foreach($image in $existingImages)
{
$hashes.Add($image.FullName, (Get-FileHash $image.FullName).Hash)
}
Write-Host "Exiting Get-HashesForImagesInTargetDirectory()"
return $hashes
}
function Main
{
if(! (Test-Path -Path $sourceDirectory))
{
Write-Host "The source directory does not exist at '$sourceDirectory'." -ForegroundColor Red
return
}
if(! (Test-Path -Path $targetDirectory))
{
Write-Host "The target directory does not exist at '$targetDirectory'." -ForegroundColor Red
return;
}
$extensions = @('.mp4', '.jpg', '.mov', '.jpeg', '.m4v', '.png', '.bmp', '.gif')
$properties = @('Attributes', 'Bit depth', 'Computer', 'Date accessed', 'Date created', 'Date modified', 'Dimensions', 'EXIF version', 'File extension', 'Filename', 'Folder', 'Folder name', 'Folder path', 'Height', 'Horizontal resolution', 'Item type', 'Kind', 'Link status', 'Name', 'Owner', 'Path', 'Perceived type', 'Rating', 'Shared', 'Size', 'Space free', 'Space used', 'Total size', 'Type', 'Vertical resolution', 'Width', 'Bit rate', 'Length', 'Media created', 'Protected', '35mm focal length', 'Camera maker', 'Camera model', 'Date taken', 'Exposure bias', 'Exposure program', 'Exposure time', 'F-stop', 'Flash mode', 'Focal length', 'ISO speed', 'Metering mode', 'Orientation', 'Program mode', 'Program name', 'White balance', 'Light source', 'Max aperture', 'Saturation', 'Subject', 'Title')
$imageHashes = Get-HashesForImagesInTargetDirectory
$images = Get-ChildItem -Path $sourceDirectory -File -Recurse | Sort-Object -Property LastWriteTime -Descending
$toSortFolder = [System.IO.Path]::Combine($targetDirectory, "ToSort");
New-Item -ItemType Directory -Force -Path $toSortFolder | Out-Null
foreach($image in $images)
{
Write-Host "Processing image: $($image.Fullname)"
if($extensions.Contains($image.Extension.ToLower()))
{
$metadata = Get-FileMetaData -file $image
$dateTaken = Get-DateTaken -metadata $metadata
$imageSize = Get-ImageSize -metadata $metadata
$extension = $image.Extension.ToLower()
$imageHash = (Get-FileHash $image.FullName).Hash
if(! $imageHashes.ContainsValue($imageHash))
{
$split = $dateTaken.split("-")
$year = $split[2]
$month = (Get-Culture).DateTimeFormat.GetMonthName($split[1])
$yearFolder = [System.IO.Path]::Combine($targetDirectory, $year);
New-Item -ItemType Directory -Force -Path $yearFolder | Out-Null
$monthFolder = [System.IO.Path]::Combine($yearFolder, $month);
New-Item -ItemType Directory -Force -Path $monthFolder | Out-Null
$filename = [string]::Format("{0}_{1}{2}", $dateTaken, $imageSize, $extension)
$filePath = [System.IO.Path]::Combine($monthFolder, $filename)
if (! (Test-Path -Path $filePath))
{
Copy-Item $image.FullName -Destination $filePath
}
else
{
for($index = 1; $index -lt 10; $index++)
{
$filename = [string]::Format("{0}_{1}_{2}{3}", $dateTaken, $imageSize, $index, $extension)
$filePath = [System.IO.Path]::Combine($monthFolder, $filename)
if (! (Test-Path -Path $filePath))
{
Copy-Item $image.FullName -Destination $filePath
break;
}
}
}
if(! $imageHashes.ContainsKey($filePath))
{
$imageHashes.Add($filePath, $imageHash)
}
}
}
elseif($image.Extension.ToLower() -eq '.aae')
{
# Ignore file
}
else
{
Copy-Item $image.FullName -Destination $toSortFolder
}
}
Write-Host ""
Write-Host "Total images processed: $($images.Count)" -ForegroundColor Green
}
# =============================================================================
# Start of Script Body
# =============================================================================
$timeStamp= (Get-Date).ToString("HH:mm dd/MM/yyyy")
Write-Host "Starting $ScriptName at $timeStamp"
$scriptTimer = [System.Diagnostics.Stopwatch]::StartNew()
Main
$elapsedTime = $scriptTimer.Elapsed
$message = [string]::Format("Script total execution time: {0} seconds", $elapsedTime.TotalSeconds.ToString("#"))
Write-Host $message
Write-Host
Write-Host "Exiting $ScriptName"
# =============================================================================
# End of Script Body
# =============================================================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment