|
# ============================================================================= |
|
# |
|
# 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 |
|
# ============================================================================= |