Simply copy the script into a PowerShell ISE script window and execute! You will be prompted for a Source and Target folder.
Example:
Source:
- unsorted\DSC_0236.jpg
- unsorted\DSC_0258.jpg
- unsorted\IMG_0258.jpg
Target:
- sorted\2014\05_May\2014-05-01_19-08-26_258.jpg
- sorted\2014\05_May\thumbs\2014-05-01_19-08-26_258_thumb_400x400.jpg
# ============================================================================================== # # Microsoft PowerShell Source File # # This script will organize photo and video files by renaming the file based on the date the # file was created and moving them into folders based on the year and month. It will also append # an index number to the end of the file name just to avoid name collisions. The script will # look in the SourceRootPath (recursing through all subdirectories) for any files matching # the extensions in FileTypesToOrganize. It will rename the files and move them to folders under # DestinationRootPath, e.g. DestinationRootPath\2011\02_February\2011-02-09_21-41-47_680.jpg # # JPG files contain EXIF data which has a DateTaken value. Other media files have a MediaCreated # date. # # The code for extracting the EXIF DateTaken is based on a script by Kim Oppalfens: # http://blogcastrepository.com/blogs/kim_oppalfenss_systems_management_ideas/archive/2007/12/02/organize-your-digital-photos-into-folders-using-powershell-and-exif-data.aspx # ============================================================================================== # Modified by Brock Hensley brock@brockhensley.com # - Prompts for many options (source/destination folders, label, thumbnail sizes (multiple at once even)) # - Resize thumbnail rotates orientation appropriately # - Named by incremental number instead of random number # - Ability to apply time fix for multiple cameras with out of sync clocks # ============================================================================================== [Reflection.Assembly]::LoadFile("C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll") function Select-Folder($message='Select folder', $path = 0) { $object = New-Object -comObject Shell.Application $folder = $object.BrowseForFolder(0, $message, 0, $path) if ($folder -ne $null) { return $folder.self.Path } } # Defaults $FileTypesToOrganize = @("*.jpg","*.jpeg") # Prompts to override defaults $Label = "" $prompt = Read-Host "What to name files (default is date taken timestamp + index#: YYYY-MM-DD_HH-MM-SS_x#)" if ($prompt -ne $null -and $prompt -ne "") {$Label = $prompt} $Index = 10000 $prompt = Read-Host "Index starting number (default: $Index)" if ($prompt -ne $null -and $prompt -ne "") { $Index = [Convert]::ToInt32($prompt) } $SourceRootPath = "C:\UnsortedPhotos" $prompt = Select-Folder "Select source folder (default: $SourceRootPath )" if ($prompt -ne $null -and $prompt -ne "") {$SourceRootPath = $prompt} $DestinationRootPath = "C:\SortedPhotos" $prompt = Select-Folder "Select destination folder (default: $DestinationRootPath )" if ($prompt -ne $null -and $prompt -ne "") {$DestinationRootPath = $prompt} $CopyRenameOrig = $true $prompt = Read-Host "Copy and Rename originals by date? ([y]/n)" if ($prompt -ne $null -and $prompt -ne "" -and $prompt.ToLower() -eq "n") { $CopyRenameOrig = $false } $CreateThumb = $true $prompt = Read-Host "Create thumbnails? ([y]/n)" if ($prompt -ne $null -and $prompt -ne "" -and $prompt.ToLower() -eq "n") { $CreateThumb = $false } if ($CreateThumb -eq $true) { $ThumbSizes = "128, 256, 512, 1024" Write-Host "Enter thumbnail sizes to create in comma seperated list. Default:" $ThumbSizes Write-Host "or enter N to skip creating thumbnails" $prompt = Read-Host "Thumbnail sizes" if ($prompt -ne $null -and $prompt -ne "") { if ($prompt.ToLower() -eq "n") { $CreateThumb = $false } else { $ThumbSizes = $prompt } } } # Functions function GetMediaCreatedDate($File) { $Shell = New-Object -ComObject Shell.Application $Folder = $Shell.Namespace($File.DirectoryName) $CreatedDate = $Folder.GetDetailsOf($Folder.Parsename($File.Name), 191).Replace([char]8206, ' ').Replace([char]8207, ' ') if (($CreatedDate -as [DateTime]) -ne $null) { return [DateTime]::Parse($CreatedDate) } else { return $null } } function ConvertAsciiArrayToString($CharArray) { $ReturnVal = "" foreach ($Char in $CharArray) { $ReturnVal += [char]$Char } return $ReturnVal } function GetExifDateTaken($File) { $FileDetail = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $File.Fullname if ($FileDetail -ne $null) { $DateTimePropertyItem = $FileDetail.GetPropertyItem(36867) $FileDetail.Dispose() } if ($DateTimePropertyItem -eq $null) { return $null } $Year = ConvertAsciiArrayToString $DateTimePropertyItem.value[0..3] $Month = ConvertAsciiArrayToString $DateTimePropertyItem.value[5..6] $Day = ConvertAsciiArrayToString $DateTimePropertyItem.value[8..9] $Hour = ConvertAsciiArrayToString $DateTimePropertyItem.value[11..12] $Minute = ConvertAsciiArrayToString $DateTimePropertyItem.value[14..15] $Second = ConvertAsciiArrayToString $DateTimePropertyItem.value[17..18] # CameraA and CameraB taking pictures of same event # ...CameraB timestamps don't match CameraA due to DST etc. # Set $timefix to $true to make adustments here # otherwise by default this should be set to $false $timefix = $false if ($timefix -eq $true) { $Hour = ([Convert]::ToInt32($Hour) + 1).ToString() $Minute = ([Convert]::ToInt32($Minute) - 18).ToString() } $DateString = [String]::Format("{0}-{1}-{2} {3}:{4}:{5}", $Year, $Month, $Day, $Hour, $Minute, $Second).Trim() if (($DateString -as [DateTime]) -ne $null) { return [DateTime]::Parse($DateString) } else { return $null } } function GetCreationDate($File) { if ($File.Extension.ToLower() -eq ".jpg" -or $File.Extension.ToLower() -eq ".jpeg") { $CreationDate = GetExifDateTaken($File) } else { $CreationDate = GetMediaCreatedDate($File) } return $CreationDate } function BuildDesinationPath($Path, $Date) { return [String]::Format("{0}\{1}\{2}_{3}", $Path, $Date.Year, $Date.ToString("MM"), $Date.ToString("MMMM")) } function BuildThumbDesinationPath($Path, $Date, $ThumbSizeLabel) { return [String]::Format("{0}\{1}\{2}_{3}\thumbs\{4}", $Path, $Date.Year, $Date.ToString("MM"), $Date.ToString("MMMM"), $ThumbSizeLabel) } function BuildNewFilePath($Path, $Date, $Extension, $Index, $Label) { if ($Label -eq "") {$Label = $Date.ToString("yyyy-MM-dd_HH-mm-ss")} return [String]::Format("{0}\{1}_x{2}{3}", $Path, $Label, $Index, $Extension.ToLower()) if ($Label -ne "") { return [String]::Format("{0}\{1}_x{2}{3}", $Path, $Label, $Index, $Extension.ToLower()) } return [String]::Format("{0}\{1}_x{2}{3}", $Path, $Date.ToString("yyyy-MM-dd_HH-mm-ss"), $Index, $Extension.ToLower()) } function BuildThumbFilePath($Path, $Date, $Extension, $Size, $Index, $Label) { if ($Label -eq "") {$Label = $Date.ToString("yyyy-MM-dd_HH-mm-ss")} return [String]::Format("{0}\thumbs\{5}\{1}_x{2}_{3}{4}", $Path, $Label, $Index, $Size, $Extension.ToLower(), $Size) } function ScaleImage($image, $maxWidth, $maxHeight) { try { $prop = $image.GetPropertyItem(274) } catch {} if ($prop -ne $null) { $orientation = [int]$prop.Value[0]; } else { $orientation = 1 } switch ($orientation) { 1 { # No rotation required. break; } 2 { $image.RotateFlip("RotateNoneFlipX"); break; } 3 { $image.RotateFlip("Rotate180FlipNone"); break; } 4 { $image.RotateFlip("Rotate180FlipX"); break; } 5 { $image.RotateFlip("Rotate90FlipX"); break; } 6 { $image.RotateFlip("Rotate90FlipNone"); break; } 7 { $image.RotateFlip("Rotate270FlipX"); break; } 8 { $image.RotateFlip("Rotate270FlipNone"); break; } } [Double]$ratioX = $maxWidth / $image.Width; [Double]$ratioY = $maxHeight / $image.Height; $ratio = [System.Math]::Min($ratioX, $ratioY); [int]$newWidth = $image.Width * $ratio; [int]$newHeight = $image.Height * $ratio; # Now calculate the X,Y position of the upper-left corner (one of these will always be zero) [int]$posX = [Convert]::ToInt32(($maxWidth - ($image.Height * $ratio)) / 2); [int]$posY = [Convert]::ToInt32(($maxHeight - ($image.Width * $ratio)) / 2); Write-Host -NoNewline " "$newWidth"x"$newHeight $newImage = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $newWidth,$newHeight $g=[System.Drawing.Graphics]::FromImage($newImage); $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic # use high quality resize algorythm $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality $g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality $g.Clear([System.Drawing.Color]::White) $g.DrawImage($image, 0, 0, $newWidth, $newHeight) return $newImage; } function CreateDirectory($Path){ if (!(Test-Path $Path)) { New-Item $Path -Type Directory | Out-Null } } function ConfirmContinueProcessing() { $Response = Read-Host "Continue? (Y/N)" if ($Response.Substring(0,1).ToUpper() -ne "Y") { break } } # Begin $StartTime = Get-Date Write-Host "*Begin" $StartTime $Files = Get-ChildItem $SourceRootPath -Recurse -Include $FileTypesToOrganize $Count = 0 $Existing = 0 $ThumbCount = 0 $ExistingThumb = 0 $Unknown = 0 $LastCreationDate = $null foreach ($File in $Files) { $CreationDate = GetCreationDate($File) if ($CreationDate -eq $null -or ($CreationDate -as [DateTime]) -eq $null) { $Unknown++ Write-Host " Unable to determine creation date of file. " $File.FullName if ($LastCreationDate -eq $null) { $CreationDate = [DateTime]::Now } else { $CreationDate = $LastCreationDate } Write-Host " (borrowing creation date: " $CreationDate " )" #ConfirmContinueProcessing } $LastCreationDate = $CreationDate $DestinationPath = BuildDesinationPath $DestinationRootPath $CreationDate CreateDirectory $DestinationPath $NewFilePath = BuildNewFilePath $DestinationPath $CreationDate $File.Extension $Index $Label Write-Host "" Write-Host "" Write-Host "[Orig] " $File.FullName Write-Host "--New: " $NewFilePath if ($CopyRenameOrig -eq $true) { if (Test-Path $NewFilePath) { $Existing++ Write-Host " -- **** (skipping) Unable to make file. File already exists: $NewFilePath ****" #ConfirmContinueProcessing } else { Copy-Item $File.FullName $NewFilePath $Count++ } } if ($CreateThumb -eq $true) { Write-Host " -- Creating thumbnails: " -NoNewline $ThumbSizes.Split(",") | ForEach { $ThumbSize = $_.Trim(); $ThumbSizeLabel = $ThumbSize + "x" + $ThumbSize $DestinationThumbPath = BuildThumbDesinationPath $DestinationRootPath $CreationDate $ThumbSizeLabel CreateDirectory $DestinationThumbPath $ThumbFilePath = BuildThumbFilePath $DestinationPath $CreationDate $File.Extension $ThumbSizeLabel $Index $Label if (Test-Path $ThumbFilePath) { $ExistingThumb++; Write-Host " -- **** (skipping thumb: $ThumbFilePath) ****" } else { $full = [System.Drawing.Image]::FromFile($File.FullName); $newImage = ScaleImage $full $ThumbSize $ThumbSize #Encoder parameter for image quality $myEncoder = [System.Drawing.Imaging.Encoder]::Quality $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1) $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($myEncoder, 100) # get codec $myImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders()|where {$_.MimeType -eq 'image/jpeg'} if ($newImage -ne $null){ $newImage.Save($ThumbFilePath, $myImageCodecInfo, $($encoderParams)); } $full.Dispose(); if ($newImage -ne $null){$newImage.Dispose();} } $ThumbCount++ } } $Index++ } $TimeSpan = New-TimeSpan $StartTime (Get-Date) Write-Host "" Write-Host "**** Done! Total:" $Count " Existing (skipped):" $Existing " Unknown dates:" $Unknown " Duration:" $TimeSpan.Minutes"min " $TimeSpan.Seconds "sec"
Very helpful script, just what I was seeking. Thanks so much for sharing.
ReplyDeleteGlad it helped!
DeleteGreat Script. But for my .mts and .mp4 Files it just did not find out the CreateDate. The Function GetMediaCreatedDate($File) doesn't work for those Filetypes. With ExifTool I could see, that there is a CreateDate Tag in the File. Any Suggestions how the Function could be adapted for those types of Files?
ReplyDeleteInteresting, I had not tried mp3 files, only images... If I get some free time I will revisit this with this in mind.
DeleteThat’s an informative post I spend my great timing on reading this post.
ReplyDelete