DonationCoder.com forum
 
Read-only archive. This is a static copy of the DonationCoder.com forum. Reading and searching work as before; logging in, posting and private messages do not.
Archive of 2026-10-21 · About this archive · Main site

Author Topic: IDEA: Automagic Digital Photo Manager  (Read 13,684 times)

oblivion

  • Supporting Member
  • Joined in 2010
  • **
  • Posts: 499
    • Participated in NANY Challenge Once
IDEA: Automagic Digital Photo Manager
« on: October 21, 2018, 08:28 PM »
My father-in-law wants to be able to clear the photos off the SD card from his camera onto his PC so he can (a) free up space on the card, and (b) review and delete any photos he doesn't want to keep.

He has next to no idea of file management. The computer is for eBay and a few games.

So what I thought was some sort of semi-automated process. Identify a base target folder (possibly stored for later use in a config file), make a subfolder of the current year (if it doesn't already exist) and a subfolder of that for the current month.

Identify the source drive; assume \DCIM as the base folder for the photos.

Then copy all the JPGs from \dcim\ and any subfolders it might have into the newly-created yyyy/mmmm target folder, but ignore source target structure. (His camera makes subfolders for month or day but trying to create and then navigate the destination if it's too granular seems overkill, and dating the destination for now won't prevent later organisation based on the file data or EXIF stuff, I figure.)

Then fire up Explorer, or Photos, or Windows Photo Viewer, pointed at the destination.

Then reviewing the photos should at least mean accidental deletions wind up in the recycle bin...

This sounds straightforward but I keep getting tied in knots trying to work out how to solve it myself -- I guess I'm getting old :(
-- bests, Tim

...this space unintentionally left blank.

skwire

  • I eat Coding Snacks for breakfast...
  • Global Moderator
  • Joined in 2005
  • *****
  • Posts: 5,297
    • Participated in NANY Challenge Seventeen Times
    • Coding Snacks
    • Superboy Coder
    • Cody Award
    • Volunteers/Staff
    • Gathering Badge
Re: IDEA: Automagic Digital Photo Manager
« Reply #1 on: October 21, 2018, 08:43 PM »
To review:

  1. Source is an SD card full of photos in the standard DCIM folder.  The folder structure of this folder is of no concern, i.e., we're not looking to recreate it in the destination.
  2. Choose a destination folder on the computer.  In this folder, create a new folder with the year and a subfolder of the month.
  3. Move all photos in the DCIM folder, and its subfolders, into this destination folder.
  4. Start Windows Explorer at the current month subfolder of the destination.

Does the SD card show up as a drive letter?

oblivion

  • Supporting Member
  • Joined in 2010
  • **
  • Posts: 499
    • Participated in NANY Challenge Once
Re: IDEA: Automagic Digital Photo Manager
« Reply #2 on: October 22, 2018, 07:09 AM »
The review: yes to all.

Does the SD card show up as a drive letter?
Yes, again.

I can probably ensure that the drive letter always comes up the same, but I guess you could also look for available drives with \dcim folders in the root and present them (or, hopefully always!) it as a confirmation for the source.
-- bests, Tim

...this space unintentionally left blank.

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #3 on: October 23, 2018, 10:36 PM »
Something simple in PowerShell while skwire whips up a masterpiece :P : DCIMover.ps1

Updated version:
  • Lists all removable drives that have a DCIM folder,
  • Saves the destination folder when you click Start,
  • Defaults to Copy,
  • Creates destination folders as per Year\Month, eg. 2017\03_Mar,
  • Will not overwrite any existing file,
  • Works on JPEG, DNG, AVI, MOV, MP4 files, (uses Date Created for video formats),
  • Can easily add more image/video formats, (just edit the .ini file),
  • Will open your default file manager at the last folder created, (works for DOpus, File Explorer), for both images and videos (if it's the same folder then normally the already open file manager window is brought to the front),
  • Multi-threaded so the GUI doesn't become unresponsive,
  • Progressbar updates as it goes and a window shows the current operation and any errors,
  • Can cancel the operation by just closing the GUI, (might implement a Stop button later),
  • Sets the creation date of the destination file to the Date Taken of the image, (or Date Created if it's video),
  • You can set the initial folder to select a destination folder in the .ini file,
  • Interface controls are disabled while it's running, (so you don't go swapping between Copy/Move),
  • If any errors occurred the log file will be opened at the end of the operation.

There's a small video, (1:16), in the archive that shows it working, (original files shown on the left, folder tree being created with files on the right).

It requires Powershell v3+, (TBH I'm not sure what minimum version is required for some cmdlets - I can only test against v5).

Installation:

Extract the archive and copy the contents somewhere.
If you need to move the shortcut somewhere else, open the properties of the shortcut and edit the Target field to include the path to the script.

Edit the DCIMover.ini file for some basic preferences.

DCIMover.ini
Code: (text)
[General]
dest=K:\a_folder\test place
initfolder=c:\users\fred\pictures\photos
images=.dng,.jpg
videos=.3gp,.avi,.mov,.mp4

  • dest - The last destination folder used, saved whenever the Start button is pressed.
  • initfolder - The "top level" folder used for the folder browser when selecting a destination, if it doesn't exist the folder browser will default to My Computer.
  • images - The extensions of image files, comma separated.  These are files that contain Date Taken metadata so can include any other file type that has that data, (eg. maybe some video formats)
  • videos - The extensions of video files, comma separated.  The Date Created metadata will be used for these file types.

Running:

  • Run it from the shortcut, a Powershell console will open for a second then close, the script GUI will then open.
  • Select your source drive from the drop-down list, (any removable drive with a folder named DCIM in the root will be in the list).
  • Select your destination folder, the files will be copied here in the appropriate year\month sub-folders.
  • Select Copy or Move, (I suggest Copy until you're are sure it's working correctly).
  • Hit Start.

DCIMover.png

Code: ( powershell)
$Global:SyncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("SyncHash",$SyncHash)

# Load WPF assembly if necessary
[void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')

$psCmd = [PowerShell]::Create().AddScript({
  [xml]$xaml = @"
<Window x:Class="DCIMover.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DCIMover" Height="350" Width="522" ResizeMode="CanMinimize">
    <Grid>
        <TextBox x:Name="TextBox1" HorizontalAlignment="Left" Height="23" Margin="102,72,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="325" ToolTip="Destination folder for photos/videos"/>
        <Label x:Name="Label1" Content="Destination" HorizontalAlignment="Left" Margin="25,66,0,0" VerticalAlignment="Top" Width="72"/>
        <Button x:Name="Button1" Content="..." HorizontalAlignment="Left" Margin="460,72,0,0" VerticalAlignment="Top" Width="32" ToolTip="Select destination folder"/>
        <Label x:Name="Label2" Content="Source" HorizontalAlignment="Left" Margin="25,21,0,0" VerticalAlignment="Top"/>
        <ComboBox x:Name="ComboBox1" HorizontalAlignment="Left" Margin="102,25,0,0" VerticalAlignment="Top" Width="62" ToolTip="Select source card from list"/>
        <ProgressBar x:Name="ProgressBar1" HorizontalAlignment="Left" Height="26" Margin="25,118,0,0" VerticalAlignment="Top" Width="467" IsTabStop="False"/>
        <TextBox x:Name="TextBox2" HorizontalAlignment="Left" Height="136" Margin="25,164,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="467" ToolTip="Status messages" AllowDrop="False" Focusable="False" IsTabStop="False" VerticalScrollBarVisibility="Auto" IsUndoEnabled="False" IsReadOnly="True" HorizontalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True"/>
        <Button x:Name="Button2" Content="Start" HorizontalAlignment="Left" Margin="417,25,0,0" VerticalAlignment="Top" Width="75"/>
        <RadioButton x:Name="RadioButton1" Content="Copy" HorizontalAlignment="Left" Margin="331,19,0,0" VerticalAlignment="Top" IsChecked="True" ToolTip="Copy files from card"/>
        <RadioButton x:Name="RadioButton2" Content="Move" HorizontalAlignment="Left" Margin="331,39,0,0" VerticalAlignment="Top" ToolTip="Move files from card"/>

    </Grid>
</Window>
"@

  # Remove XML attributes that break a couple things.
  #   Without this, you must manually remove the attributes
  #   after pasting from Visual Studio. If more attributes
  #   need to be removed automatically, add them below.
  $AttributesToRemove = @(
    'x:Class',
    'mc:Ignorable'
  )

  Foreach ($Attrib in $AttributesToRemove) {
    if ($xaml.Window.GetAttribute($Attrib)) {
      $xaml.Window.RemoveAttribute($Attrib)
    }
  }
    
  $reader = (New-Object System.Xml.XmlNodeReader $xaml)
    
  $SyncHash.Window = [Windows.Markup.XamlReader]::Load( $reader )

  [xml]$XAML = $xaml
  $xaml.SelectNodes("//*[@*[contains(translate(name(.),'n','N'),'Name')]]") | %{
  #Find all of the form types and add them as members to the synchash
    $SyncHash.Add($_.Name, $SyncHash.Window.FindName($_.Name) )
  }

  $Script:JobCleanup = [hashtable]::Synchronized(@{})
  $Script:Jobs = [system.collections.arraylist]::Synchronized((New-Object System.Collections.ArrayList))

  #region Background runspace to clean up jobs
  $jobCleanup.Flag = $True
  $newRunspace = [runspacefactory]::CreateRunspace()
  $newRunspace.ApartmentState = "STA"
  $newRunspace.ThreadOptions = "ReuseThread"          
  $newRunspace.Open()        
  $newRunspace.SessionStateProxy.SetVariable("jobCleanup", $jobCleanup)     
  $newRunspace.SessionStateProxy.SetVariable("jobs", $jobs)
  $jobCleanup.PowerShell = [PowerShell]::Create().AddScript({
  #Routine to handle completed runspaces
    Do {    
      Foreach($runspace in $jobs) {            
        If ($runspace.Runspace.isCompleted) {
          [void]$runspace.powershell.EndInvoke($runspace.Runspace)
          $runspace.powershell.dispose()
          $runspace.Runspace = $null
          $runspace.powershell = $null               
        } 
      }
  #Clean out unused runspace jobs
      $temphash = $jobs.clone()
      $temphash | Where {$_.runspace -eq $Null} | ForEach {$jobs.remove($_)}        
      Start-Sleep -Seconds 1     
    } while ($jobCleanup.Flag)
  })
  $jobCleanup.PowerShell.Runspace = $newRunspace
  $jobCleanup.Thread = $jobCleanup.PowerShell.BeginInvoke()  
  #endregion Background runspace to clean up jobs


  $SyncHash.Button1.Add_Click({
    $objShell = new-object -com shell.application
    # Special Folders: https://docs.microsoft.com/en-us/windows/desktop/api/shldisp/ne-shldisp-shellspecialfolderconstants
    # eg. Pictures = 0x27
    $objFolder = $objShell.BrowseForFolder(0, "Select destination folder", 0x00000074, $SyncHash.InitFolder)
    if ($objfolder.self.path -ne $null) {
      $SyncHash.TextBox1.Text = $objfolder.self.path
    }
  })

  $SyncHash.Button2.Add_Click({
# Start-Job -Name Sleeping -ScriptBlock {start-sleep 5}
# while ((Get-Job Sleeping).State -eq 'Running'){
# region Boe's Additions
    $newRunspace =[runspacefactory]::CreateRunspace()
    $newRunspace.ApartmentState = "STA"
    $newRunspace.ThreadOptions = "ReuseThread"          
    $newRunspace.Open()
    $newRunspace.SessionStateProxy.SetVariable("SyncHash", $SyncHash) 
    $PowerShell = [PowerShell]::Create().AddScript({
      Function Update-Window {
        Param (
          $Control,
          $Property,
          $Value,
          [switch]$AppendContent
        )
# This is kind of a hack, there may be a better way to do this
        If ($Property -eq "Close") {
          $SyncHash.Window.Dispatcher.invoke([action]{$SyncHash.Window.Close()}, "Normal")
          Return
        }
# This updates the control based on the parameters passed to the function
        $SyncHash.$Control.Dispatcher.Invoke([action]{
# This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
          If ($PSBoundParameters['AppendContent']) {
            $SyncHash.$Control.AppendText($Value)
          } Else {
            $SyncHash.$Control.$Property = $Value
          }
        }, "Normal")
      }

      Function Get-MetaData {
        Param (
          [PARAMETER(Mandatory=$true)]
          [string]$path = '',
#          [PARAMETER( Mandatory = $true, HelpMessage = 'extension eg. .mp3, .txt or .mov')]
          [boolean]$images,
#          [string]$type = '', # 'mp3'
          [switch]$recurse
        )

        if ($images) {
          $types = $SyncHash.imageTypes
        } else {
          $types = $SyncHash.videoTypes
        }

        $path += 'DCIM'
        if ($recurse) {
          $LPath = Get-ChildItem -Path $path -Directory -Recurse
        } else {
          $LPath = $path
        }

#        $DirectoryCount = 1
        $RetrievedMetadata = $true
        $OutputList = New-Object 'System.Collections.generic.List[psobject]'

        Foreach ($pa in $LPath) {
          $shell = New-Object -ComObject shell.application

          if ($recurse) {
            $objshell = $shell.NameSpace($pa.FullName)
          } else {
            $objshell = $shell.NameSpace($pa)
          }
          # Build data list
          $count = 0

          # Filter on filetype
          for ($i = 0; $i -lt $types.Count; $i++) {
            $filter = $null
            Update-Window -Control ProgressBar1 -Property Value -Value 0
            Start-Sleep -Milliseconds 500
            $filter = $objshell.Items() | where {$_.Name -match $types[$i]}
            Write-Message  "Collecting Metadata for '$($types[$i])' files ..."
<#
$filter.Count > 0 if more than one matching file
$filter.Count = 0 if no matching files
$filter.Count = $null if one matching file
#>
            Foreach ($file in $filter) {
              if ($RetrievedMetadata) {
# Build metanumbers
                $Metanumbers = New-Object -TypeName 'System.Collections.Generic.List[int]'
                for ($a = 0; $a -le 400; $a++) {
                  if ($objshell.GetDetailsOf($file, $a)) {
                    $Metanumbers.Add([int]$a)
                  }    
                }
                $RetrievedMetadata = $false
              }

              $count++
              $CurrentDirectory = Get-ChildItem -Path $file.path

              if ($filter.Count -gt 0) {
                try {
                  Update-Window -Control ProgressBar1 -Property Value -Value ([math]::Round((($count / $filter.count) * 100)))
                } catch {}
              } else {
                try {
                  Update-Window -Control ProgressBar1 -Property Value -Value 100
                } catch {}
              }
                
# Build Hashtable for each file
              $hash = $null
              $Hash = @{}
              foreach ($nr in $Metanumbers) {
                $PropertyName = $($objshell.GetDetailsOf($objshell.Items, $nr))
                $PropertyValue = $($objshell.GetDetailsOf($File, $nr))
                $Hash[$PropertyName] = $PropertyValue
              }
            
              $Hash.Remove("")
              $FileMetaData = New-Object -TypeName PSobject -Property $hash
              $OutputList.Add($FileMetaData)
            }
          }
        }
        Process-OutputList $OutputList $images
      }

      Function Process-OutputList {
        Param (
          [object]$files,
          [string]$images
        )

        $dest = $SyncHash.Window.Dispatcher.invoke([System.Func[String]] {$SyncHash.TextBox1.Text})

        if ($files.Count -eq $null) {
          Write-Message  "Copy/Move one file ..."
          switch ($images) {
            $true { $takenDate = [datetime]((Select-Object -InputObject $files[$j] -ExpandProperty 'Date Taken') -replace '[^\d:/ -_.,]', '') }
            $false { $takenDate = [datetime](Select-Object -InputObject $files[$j] -ExpandProperty 'Date Created') }
          }
          if ($takenDate -ne $null) {
            $outPath = ($dest + '\' + $takenDate.Year + '\' + ($takenDate.Month).ToString('00') + '_' + ((Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($takenDate.Month)) + '\' + $files.Name) -replace '\\', '\'
            Move-File $files.'Path' $outPath $takenDate
            Update-Window -Control ProgressBar1 -Property Value -Value 100
          } else {
            Write-Message "[Warn]: $($files.Name): Date Taken not found, Copy/Move not performed" $true
          }
        } else {
          if ($files.Count -gt 0) {
            Write-Message  "Copy/Move $($files.Count) files ..."
            for ($j = 0; $j -lt $files.Count; $j++) {
              $outPath = $null
              $takenDate = $null
                switch ($images) {
                  $true {
                      if (((Select-Object -InputObject $files[$j] -ExpandProperty 'Date Taken')).Length -gt 1 ) {
                        $takenDate = [datetime]((Select-Object -InputObject $files[$j] -ExpandProperty 'Date Taken') -replace '[^\d:/ -_.,]', '')
                      } else {
                        Write-Message "[Warn]: $($files[$j].'Name'): Date Taken not found, Copy/Move not performed" $true
                      }
                    }
                  $false {
                      $takenDate = [datetime](Select-Object -InputObject $files[$j] -ExpandProperty 'Date Created')
                    }
                }
              if ($takenDate -ne $null) {
                $outPath = ($dest + '\' + $takenDate.Year + '\' + ($takenDate.Month).ToString('00') + '_' + ((Get-Culture).DateTimeFormat.GetAbbreviatedMonthName($takenDate.Month)) + '\' + $files[$j].'Name') -replace '\\', '\'
                Move-File $files[$j].'Path' $outPath $takenDate
              }
              Update-Window -Control ProgressBar1 -Property Value -Value ([math]::Round((($j + 1) / $files.Count) * 100))
            }
          } else {
            Write-Message  "No matching files found ..."
          }
        }
        Invoke-Item (Split-Path -Path $outPath)
      }

      Function Move-File {
        param (
          [string]$source,
          [string]$dest,
          [datetime]$date
        )
        if (!(Test-Path -Path (Split-Path -Path $dest))) {
          New-Item (Split-Path -Path $dest) -ItemType Directory
          if (!$?) {
            Write-Message "[Error] Failed to create folder: $(Split-Path -Path $dest)" $true
          }
        }

        if (!(Test-Path $dest)) {
          if ($SyncHash.Window.Dispatcher.invoke([System.Func[string]] {$SyncHash.RadioButton1.IsChecked}) -eq 'True') {
            Copy-Item $source $dest
            if (!$?) {
              Write-Message "[Error] $(Split-Path $source -Leaf): Copy failed" $true
            }
          } else {
            Move-Item $source $dest
            if (!$?) {
              Write-Message "[Error] $(Split-Path $source -Leaf): Move failed" $true
            }
          }
          Set-ItemProperty -Path $dest -Name CreationTime -Value $date
        } else {
          Write-Message "[Warn] $(Split-Path $source -Leaf): Exists in destination" $true
        }
      }

      Function Write-Message {
        Param (
          [string]$message,
          [boolean]$err = $false
        )
        Update-Window -Control TextBox2 -Property Text -Value "$($message)`r`n" -Append
        if ($err) {
          $SyncHash.hadError = $true
          $message | Out-File -FilePath $SyncHash.LogFile -Append
        }
      }

# Button2 Main
      $srceFol = $SyncHash.Window.Dispatcher.invoke([System.Func[String]] {$SyncHash.ComboBox1.SelectedItem})
      $destFol = $SyncHash.Window.Dispatcher.invoke([System.Func[String]] {$SyncHash.TextBox1.Text})
      $SyncHash.hadError = $false

      if (($destFol -ne $null) -and ($destFol -ne '')) {
        if (($srceFol -ne $null) -and ($srceFol -ne '')) {
          $ini = "[General]`r`ndest=$($destFol)`r`ninitfolder=$($SyncHash.InitFolder)`r`nimages=$($SyncHash.h.Get_Item('images'))`r`nvideos=$($SyncHash.h.Get_Item('videos'))"
          Out-File -FilePath $SyncHash.iniFile -InputObject $ini
          $SyncHash.LogFile = ("$($env:TEMP)\$(Get-Date -Format 'yyyyMMdd_HHmmss').txt").Replace('\\', '\')
          Update-Window -Control Button2 -Property IsEnabled -Value $false
          Update-Window -Control RadioButton1 -Property IsEnabled -Value $false
          Update-Window -Control RadioButton2 -Property IsEnabled -Value $false
          Update-Window -Control TextBox1 -Property IsEnabled -Value $false
          Update-Window -Control ComboBox1 -Property IsEnabled -Value $false
          Update-Window -Control Button1 -Property IsEnabled -Value $false
          $SyncHash.hadError = $false
          Update-Window -Control TextBox2 -Property Text -Value ''
          Write-Message "---- START ----`r`nLogfile: $($SyncHash.LogFile)`r`nProcessing image files ..."
          Get-MetaData -Path $srceFol $true -Recurse
          Write-Message "Finished image files`r`nProcessing video files ..."
          Get-MetaData -Path $srceFol $false -Recurse
          Write-Message  "Finished video files`r`n---- END ----"
          Update-Window -Control RadioButton1 -Property IsEnabled -Value $true
          Update-Window -Control RadioButton2 -Property IsEnabled -Value $true
          Update-Window -Control TextBox1 -Property IsEnabled -Value $true
          Update-Window -Control ComboBox1 -Property IsEnabled -Value $true
          Update-Window -Control Button1 -Property IsEnabled -Value $true
          Update-Window -Control Button2 -Property IsEnabled -Value $true
          if ($SyncHash.hadError) {
            "--- SAVE THIS FILE ---`r`n" + (Get-Content $SyncHash.LogFile -Raw) | Set-Content $SyncHash.LogFile
            Write-Message  "*** There were errors, refer to log file ***"
            Invoke-Item $SyncHash.LogFile
          }
        }
      }
# End Button2 Main

    })
    $PowerShell.Runspace = $newRunspace
    [void]$Jobs.Add((
    [pscustomobject]@{
      PowerShell = $PowerShell
      Runspace = $PowerShell.BeginInvoke()
    }
    ))
  })

    #region Window Close 
    $SyncHash.Window.Add_Closed({
      Write-Verbose 'Halt runspace cleanup job processing'
      $jobCleanup.Flag = $False

      #Stop all runspaces
      $jobCleanup.PowerShell.Dispose()      
    })
    #endregion Window Close 
    #endregion Boe's Additions


# GUI Main + Functions
  Function Add-Drives {
    $usb = ([System.IO.DriveInfo]::GetDrives() | Where DriveType -match 'Removable')
    for ($i = 0; $i -lt $usb.Count; $i++) {
      $dcimFol = ($usb[$i].Name) + 'DCIM'
      if (Test-Path -Path $dcimFol) {
        [void] $SyncHash.ComboBox1.Items.Add($usb[$i])
      }
    }
  }

  Function Read-Ini {
    if (Test-Path -Path $SyncHash.iniFile) {
      Get-Content $SyncHash.iniFile | foreach-object -begin {$SyncHash.h=@{}} -process { $k = [regex]::split($_,'='); `
        if (($k[0].CompareTo("") -ne 0) -and ($k[0].StartsWith("[") -ne $True)) { $SyncHash.h.Add($k[0], $k[1]) } }
      $SyncHash.TextBox1.Text = $SyncHash.h.Get_Item('dest')
      $SyncHash.InitFolder = $SyncHash.h.Get_Item('initfolder')
      $SyncHash.imageTypes = ($SyncHash.h.Get_Item('images')).Split(',')
      $SyncHash.videoTypes = ($SyncHash.h.Get_Item('videos')).Split(',')
    }
  }

  if ($PSVersionTable.PSVersion.Major -lt 3) {
    [System.Windows.MessageBox]::Show("This script requires Powershell v3+`r`n`r`nClick OK to exit")
    Exit
  }
  $SyncHash.iniFile = '.\DCIMover.ini'
  Read-Ini
  Add-Drives
# End GUI Main + Functions


  $SyncHash.Window.ShowDialog() | Out-Null
  $SyncHash.Error = $Error
})
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Other things I might look at doing:
  • If Date Taken doesn't exist, default to Date Created, (usually happens with photos that are manipulated in the phone/camera, eg. panoramas).
  • For video files look for Media Created first before using Date Created, (one problem is Media Created uses UTC as defined by the camera setting, which can be different from Date Created).
  • Find some way of outputting Powershell errors, (the red ones), to a log file.
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903
« Last Edit: November 10, 2018, 12:45 PM by 4wd »

skwire

  • I eat Coding Snacks for breakfast...
  • Global Moderator
  • Joined in 2005
  • *****
  • Posts: 5,297
    • Participated in NANY Challenge Seventeen Times
    • Coding Snacks
    • Superboy Coder
    • Cody Award
    • Volunteers/Staff
    • Gathering Badge
Re: IDEA: Automagic Digital Photo Manager
« Reply #4 on: October 24, 2018, 06:57 PM »
Something simple in PowerShell while skwire whips up a masterpiece

Hahaha.  :D  I've been traveling for work a lot lately, so I won't be able to get to this for at least a few days.  Thank you for coming up with your rendition of the original ask!

oblivion

  • Supporting Member
  • Joined in 2010
  • **
  • Posts: 499
    • Participated in NANY Challenge Once
Re: IDEA: Automagic Digital Photo Manager
« Reply #5 on: October 27, 2018, 05:53 AM »
Something simple in PowerShell while skwire whips up a masterpiece :P : DCIMover.ps1
That's awesome, thanks!

I won't be able to get access to their PC again for a few days but I gave them a USBsafelyRemove license I no longer need to handle the autoplay side of things and between the two, it should be perfect -- thanks again!
-- bests, Tim

...this space unintentionally left blank.

oblivion

  • Supporting Member
  • Joined in 2010
  • **
  • Posts: 499
    • Participated in NANY Challenge Once
Re: IDEA: Automagic Digital Photo Manager
« Reply #6 on: October 27, 2018, 09:23 PM »
Code
#region gui events {
$Button1.Add_Click({
  $objForm = New-Object System.Windows.Forms.FolderBrowserDialog
  $objForm.Description = "Destination folder"
  $objForm.SelectedPath = [System.Environment+SpecialFolder]'MyComputer'
  $objForm.ShowNewFolderButton = $false
  $result = $objForm.ShowDialog()
  if ($result -eq "OK") {
    $TextBox1.Text = $objForm.SelectedPath
  } else {
    $TextBox1.Text = ""
  }
})

Just one quick question: if I wanted to modify this bit to a specific root destination folder (eg c:\users\fred\pictures\photos) would I just change this line

Code
$objForm.SelectedPath = [System.Environment+SpecialFolder]'MyComputer'

or is it more complicated than that? (I really want to make it as simple as I can for him!)
-- bests, Tim

...this space unintentionally left blank.

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #7 on: October 28, 2018, 04:02 AM »
Just one quick question: if I wanted to modify this bit to a specific root destination folder (eg c:\users\fred\pictures\photos) would I just change this line

Code
$objForm.SelectedPath = [System.Environment+SpecialFolder]'MyComputer'

or is it more complicated than that? (I really want to make it as simple as I can for him!)

If you want an absolute location change it to:
Code
$objForm.SelectedPath = 'c:\users\fred\pictures\photos'

The [System.Environment+SpecialFolder] is due to My Computer being classed as a special folder, (like 'Documents', 'Pictures', etc).

I've just updated it using a different file properties function and added the ability to handle the following filetypes:
.jpg, .dng (Canon Raw), .mov, .mp4, .avi

For JPEG and DNG it'll use the Date Taken value, for the video files it'll use Date Created.

Easy enough to add other filetypes if you want something specific to a camera.

Also changed the month folder names to xx_ABC, eg. 05_May, so it lists the months in calendar order rather than alphabetical.

Added a progress bar, still can't interrupt it as it works but at least it shows it's doing something ... maybe.

Now to work out how runspaces work.

Updated
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903
« Last Edit: October 28, 2018, 04:21 AM by 4wd »

oblivion

  • Supporting Member
  • Joined in 2010
  • **
  • Posts: 499
    • Participated in NANY Challenge Once
Re: IDEA: Automagic Digital Photo Manager
« Reply #8 on: October 28, 2018, 11:14 PM »
I've just updated it
Well, I took it over tonight; put it and the shortcuts in c:\utilities\dcimover, modified the relevant line with the absolute path, set up the relevant bits to autorun the shortcut on insertion, and I just got a Powershell window with nothing in it.

Some more experimentation later and I managed to start the GUI -- by running the script directly, I think -- but it threw some scary-looking errors when I hit the Move button.

It did something -- the test pictures I'd put on the card disappeared -- but I couldn't work out what.

And, of course, as I'd spent several hours trying to solve a completely unrelated problem on the computer that I was completely unprepared for, by the time I got to this point I didn't have time to do all the testing I'd have liked. :(

Now I'm away from it, it occurs to me to wonder if I'd just left a terminating backslash off the path, or included one when I shouldn't have, or something else obvious, but I probably won't be able to look for another week...
-- bests, Tim

...this space unintentionally left blank.

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #9 on: October 29, 2018, 12:34 AM »
Well, I took it over tonight; put it and the shortcuts in c:\utilities\dcimover, modified the relevant line with the absolute path, set up the relevant bits to autorun the shortcut on insertion, and I just got a Powershell window with nothing in it.

Forgot to mention, the shortcut needs to be in the same place as the script otherwise you need to add the full path to it in the shortcut Target field.

What OS are we looking at?

Quote
Some more experimentation later and I managed to start the GUI -- by running the script directly, I think -- but it threw some scary-looking errors when I hit the Move button.

Did you run it with ExecutionPolicy set to Bypass?

Because it was downloaded it will be marked by Windows as coming from another computer which can block it from running or lower it's access to a folder.

Run it with the following command from a PowerShell console:

Code: (text)
%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -sta -noprofile -executionpolicy bypass -File "DCIMover-GUI.ps1"

You can then copy/paste the error messages in the console.

Also, what version of PowerShell, do you know?
Next time you're there type $PSVersionTable in the PowerShell console which will give the version number, I don't think there's anything version specific in the script.

Quote
It did something -- the test pictures I'd put on the card disappeared -- but I couldn't work out what.

Well, it moved them somewhere otherwise they'd still be there ;)

The error messages will help a lot.

Quote
Now I'm away from it, it occurs to me to wonder if I'd just left a terminating backslash off the path, or included one when I shouldn't have, or something else obvious, but I probably won't be able to look for another week...

No terminating backslash required on the path.

You can't try it on your computer?

Thanks for giving it a go.
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #10 on: November 05, 2018, 11:59 AM »
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903
« Last Edit: November 09, 2018, 04:22 AM by 4wd »

magician62

  • Supporting Member
  • Joined in 2011
  • **
  • Posts: 180
Re: IDEA: Automagic Digital Photo Manager
« Reply #11 on: November 05, 2018, 01:30 PM »
A little observation, as I need to look further at this util. I work always in GMT for photograph times, but I suspect not all cameras do. :)
Why an I Magician62? Because Magician1 thru 61 were gone. :)

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #12 on: November 05, 2018, 01:55 PM »
Date/time on photos/videos is set by the camera, the operator sets the date/time on the camera.

The script just grabs the date/time in the file metadata: Date Taken for JPEG and DNG; Date Created for AVI, MP4, and MOV.

No date/time manipulation is done.
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903

oblivion

  • Supporting Member
  • Joined in 2010
  • **
  • Posts: 499
    • Participated in NANY Challenge Once
Re: IDEA: Automagic Digital Photo Manager
« Reply #13 on: November 06, 2018, 10:27 PM »
I've attached a new version here, differences to the one above:
Thanks!

I haven't had a chance to try again, yet. The father-in-law's PC is acting weird and I spent several hours at the weekend trying to work out the problem rather than taking another shot at this. (I am beginning to wonder if the problems I had with the earlier version of this and the odd general problems are perhaps related... every so often the system goes unresponsive, for no obvious reason, and the only evidence of anything is some incomprehensible DCOM-related errors in the system log. I may have to reset Windows. :( )
-- bests, Tim

...this space unintentionally left blank.

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #14 on: November 07, 2018, 08:47 AM »
I'm updating the script a fair bit to display progress/errors, (and an error log), and make it easier to add support for other photo/video extensions.

EDIT: Whoops, I misinterpreted the Copy-Item/Move-Item cmdlets, they do overwrite by default - which is kind of dumb.

I'll put in a Test-Path in beforehand to check for file existence first.
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903
« Last Edit: November 07, 2018, 10:55 AM by 4wd »

4wd

  • Pedantic Old Bastard
  • Supporting Member
  • Joined in 2006
  • **
  • Posts: 5,652
    • Coding Snacks
Re: IDEA: Automagic Digital Photo Manager
« Reply #15 on: November 09, 2018, 04:30 AM »
New version

Other things I might look at doing:
  • If Date Taken doesn't exist, default to Date Created, (usually happens with photos that are manipulated in the phone/camera, eg. panoramas).
  • For video files look for Media Created first before using Date Created, (one problem is Media Created uses UTC as defined by the camera setting, which can be different from Date Created).
  • Find some way of outputting Powershell errors, (the red ones), to a log file.
"I will not follow where the path may lead, but I will go where there is no path, and I will leave a trail." - Muriel Strode, 1903

Original address: https://www.donationcoder.com/forum/index.php?topic=46553.0