Skip to content

Instantly share code, notes, and snippets.

@jongio
Created March 3, 2021 15:34
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save jongio/a40ea198ca5ebd85d711a7779289cc89 to your computer and use it in GitHub Desktop.
Save jongio/a40ea198ca5ebd85d711a7779289cc89 to your computer and use it in GitHub Desktop.
A PowerShell script to organize photos by date taken
Param(
[string]$source,
[string]$dest,
[string]$format = "yyyy/yyyy_MM/yyyy_MM_dd"
)
$shell = New-Object -ComObject Shell.Application
function Get-File-Date {
[CmdletBinding()]
Param (
$object
)
$dir = $shell.NameSpace( $object.Directory.FullName )
$file = $dir.ParseName( $object.Name )
# First see if we have Date Taken, which is at index 12
$date = Get-Date-Property-Value $dir $file 12
if ($null -eq $date) {
# If we don't have Date Taken, then find the oldest date from all date properties
0..287 | ForEach-Object {
$name = $dir.GetDetailsof($dir.items, $_)
if ( $name -match '(date)|(created)') {
# Only get value if date field because the GetDetailsOf call is expensive
$tmp = Get-Date-Property-Value $dir $file $_
if ( ($null -ne $tmp) -and (($null -eq $date) -or ($tmp -lt $date))) {
$date = $tmp
}
}
}
}
return $date
}
function Get-Date-Property-Value {
[CmdletBinding()]
Param (
$dir,
$file,
$index
)
$value = ($dir.GetDetailsof($file, $index) -replace "`u{200e}") -replace "`u{200f}"
if ($value -and $value -ne '') {
return [DateTime]::ParseExact($value, "g", $null)
}
return $null
}
Get-ChildItem -Attributes !Directory $source -Recurse |
Foreach-Object {
Write-Host "Processing $_"
$date = Get-File-Date $_
if ($date) {
$destinationFolder = Get-Date -Date $date -Format $format
$destinationPath = Join-Path -Path $dest -ChildPath $destinationFolder
# See if the destination file exists and rename until we get a unique name
$newFullName = Join-Path -Path $destinationPath -ChildPath $_.Name
if ($_.FullName -eq $newFullName) {
Write-Host "Skipping: Source file and destination files are at the same location. $_"
return
}
$newNameIndex = 1
$newName = $_.Name
while (Test-Path -Path $newFullName) {
$newName = ($_.BaseName + "_$newNameIndex" + $_.Extension)
$newFullName = Join-Path -Path $destinationPath -ChildPath $newName
$newNameIndex += 1
}
# If we have a new name, then we need to rename in current location before moving it.
if ($newNameIndex -gt 1) {
Rename-Item -Path $_.FullName -NewName $newName
}
Write-Host "Moving $_ to $newFullName"
# Create the destination directory if it doesn't exist
if (!(Test-Path $destinationPath)) {
New-Item -ItemType Directory -Force -Path $destinationPath
}
robocopy $_.DirectoryName $destinationPath $newName /mov
}
}
@monahancj
Copy link

Worked great. Thanks for posting this.

@jongio
Copy link
Author

jongio commented May 18, 2021

Nice!

@nightskyguy
Copy link

Really appreciate this. It works, but I'm seeing a lot of exceptions being thrown: may be because I've got a source full of all sorts of things: raw files, video, etc.

Indeed I've made a few additions to the code to put specific file types in specific subdirectories... and it works... but I haven't found a clean way to deal with the exceptions.

I'll create a fork/pull request for your consideration.

@tamelander
Copy link

tamelander commented Nov 25, 2021

Hm, I am only getting a load of red errors.

Exception calling "ParseExact" with "3" argument(s): "The string was not identified as a valid DateTime string."
At Q:\PhotoOrganizer.ps1:50 char:16
+         return [DateTime]::ParseExact($value, "g", $null)
+                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

I downloaded and ran the script as it was. Thoughts?

Could be something with delimiter. Sweden uses komma for decimals, 3,1415. And semi-colon for string delimiter. value1;value2

@ArtjomsNi
Copy link

Thank You! You are genius! It worked, needed to work around a little to allow poweshell to allow scrips running.
My family says thank you, because we had problems with photos transferred from android memory to SD card. All the dates was the same day we moved them.

@0xMLNK
Copy link

0xMLNK commented Mar 3, 2022

Hm, I am only getting a load of red errors.

Exception calling "ParseExact" with "3" argument(s): "The string was not identified as a valid DateTime string."
At Q:\PhotoOrganizer.ps1:50 char:16
+         return [DateTime]::ParseExact($value, "g", $null)
+                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

I downloaded and ran the script as it was. Thoughts?

Could be something with delimiter. Sweden uses komma for decimals, 3,1415. And semi-colon for string delimiter. value1;value2

I have had the same issue, so i pass script a bit. mby this will be helpful. I just pass the Unicode symbols and Format for ParseExact so that it passed for German Date.

function Get-Date-Property-Value {
    [CmdletBinding()]

    Param (
        $dir,
        $file,
        $index
    )

    $value = ($dir.GetDetailsof($file, $index) -replace "\u200E") -replace "\u200F" 
	
    if ($value -and $value -ne '') {
        return [DateTime]::ParseExact($value, 'dd/MM/yyyy HH:mm', $null)
    }
    return $null
}

@martenj77
Copy link

Thank you @jongio! This was exactly what I was looking for! I had the same problems as @0xMLNK and @tamelander did, so I took @0xMLNK 's edit and made a additional minor change in the Get-Date-Property-Value function to make it work with Swedish date format. Posted as a fork here: https://gist.github.com/martenj77/f410d8b71fd4b1eec728f7d600f07fbe

@Dopudop
Copy link

Dopudop commented Jul 14, 2022

Y

Hm, I am only getting a load of red errors.

Exception calling "ParseExact" with "3" argument(s): "The string was not identified as a valid DateTime string."
At Q:\PhotoOrganizer.ps1:50 char:16
+         return [DateTime]::ParseExact($value, "g", $null)
+                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FormatException

I downloaded and ran the script as it was. Thoughts?

Could be something with delimiter. Sweden uses komma for decimals, 3,1415. And semi-colon for string delimiter. value1;value2

for me the solution was to change the Regional settings and to have the ofrmat of the date dd/mm/yyyy and the hour hh:mm

@0xMLNK
Copy link

0xMLNK commented Jul 14, 2022

for me the solution was to change the Regional settings and to have the ofrmat of the date dd/mm/yyyy and the hour hh:mm
kinda wrong way, its simple to pass the script and not the system xD

@cooltig
Copy link

cooltig commented Jan 20, 2023

Thank you very much, really appreciate your work.

I have a question: The script generates in the target new folders like "yyyy.yyyy_mm.yyyy_mm_dd"
How is it possible to change that for every month it creates a subfolder like "yyyy\mm"

Thank you in advance!

@jongio
Copy link
Author

jongio commented Jan 20, 2023

You can pass in a custom format to the script

Param(
[string]$source,
[string]$dest,
[string]$format = "yyyy/yyyy_MM/yyyy_MM_dd"
)

@cooltig
Copy link

cooltig commented Jan 20, 2023

Thanks but I already tried that and in the destination folder it creates a folder called "yyyy.mm" instead of "yyyy/mm"

.\PhotoOrganizer.ps1 -source "E:" -dest "C:\test"
Screenshot_20230120_171231

It seems that the slash "/" doesn't create a subfolder, it will used as . in foldername.

@jongio
Copy link
Author

jongio commented Jan 20, 2023

This:
.\PhotoMover.ps1 -source "C:\temp\phototest\source" -dest "C:\temp\phototest\dest" -format "yyyy/MM"
Created:
image

Can you try with:

`.\PhotoMover.ps1 -source "C:\temp\phototest\source" -dest "C:\temp\phototest\dest" -format "yyyy\/MM"`

To escape the slash?

@cooltig
Copy link

cooltig commented Jan 20, 2023

Awesome, this works!

[string]$format = "yyyy\/MM"

Thank you so much!

@LeoZandvliet
Copy link

For some reason the EXIF Date taken in some of my files are formatted like this:
" 23/ 07/ 2023 15:37"

including some not specificly identified non-printable characters. Sigh....

So I ended in replacing all non ASCII characters with a space, and then preg replaced the result to gat a compliance date format:

function Get-Date-Property-Value {
    [CmdletBinding()]

    Param (
        $dir,
        $file,
        $index
    )

    $value = ($dir.GetDetailsof($file, $index) -replace "`u{200e}") -replace "`u{200f}"
	
    $value = (($value  -replace '[^ -~]+', ' ') -replace '[\s]*(\d{2})\/[\s]*(\d{2})\/[\s]*(\d{4})[\s]+([\d]{2}:[\d]{2})[\s]*', '$1/$2/$3 $4')
    if ($value -and $value -ne '') {
		Write-Host "Found ""$value"""
        return [DateTime]::ParseExact($value, "g", $null)
    }
    return $null
}

@greiginsydney
Copy link

greiginsydney commented Sep 8, 2023

Thanks @LeoZandvliet , that did the trick for me!

EDIT: I also needed to replace the "g" "general date format specifier" with my specific format, which here in AU turned out to be "dd/MM/yyyy hh:mmtt".

This is my final "return" line:

return [DateTime]::ParseExact($value, "dd/MM/yyyy hh:mmtt", $null)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment