Last active
May 1, 2025 18:23
-
-
Save morrisonbrett/4e25bff390fe8ee3e8bdfcef8b98b6cd to your computer and use it in GitHub Desktop.
Powershell script to copy files from a SD card into a directory and combine them
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Camera import script for dual-slot card reader | |
| # Path configurations - CUSTOMIZE THESE | |
| param( | |
| [string]$TargetDriveLetter = "", # Empty means process all removable drives | |
| [string]$TargetCameraType = "" # Empty means detect camera type automatically | |
| ) | |
| $rootTempDir = "e:\temp" | |
| $rootOutputDir = "e:\Pickup Hockey\Media" | |
| $lockFilePath = Join-Path $env:TEMP "camera_import_lock" | |
| $logFile = Join-Path $env:TEMP "camera_import.log" | |
| # Function for logging | |
| function Write-Log { | |
| param( | |
| [string]$message | |
| ) | |
| $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" | |
| $logMessage = "[$timestamp] $message" | |
| # Output to console | |
| Write-Host $logMessage | |
| # Output to log file | |
| Add-Content -Path $logFile -Value $logMessage | |
| } | |
| # Clear previous log at start | |
| if (Test-Path $logFile) { | |
| # Keep previous log but add a separator | |
| Add-Content -Path $logFile -Value "`n`n===== New Session Started $(Get-Date) =====`n" | |
| } else { | |
| # Create log file | |
| New-Item -ItemType File -Path $logFile -Force | Out-Null | |
| Add-Content -Path $logFile -Value "===== Camera Import Log Started $(Get-Date) =====`n" | |
| } | |
| # Function to get physical drive for a logical drive | |
| function Get-PhysicalDrive { | |
| param ( | |
| [string]$driveLetter | |
| ) | |
| try { | |
| # Alternative approach to get physical drive | |
| $volume = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter='$driveLetter`:'" | |
| if ($volume) { | |
| $diskPartition = Get-WmiObject -Class Win32_DiskPartition -Filter "DeviceID='$($volume.DeviceID.Replace("\", "\\"))'" | |
| if ($diskPartition) { | |
| $diskDrive = Get-WmiObject -Class Win32_DiskDrive -Filter "Partitions > 0" | | |
| Where-Object { $_.DeviceID -eq ($diskPartition.DeviceID -replace "Disk #(\d+), Partition #\d+", '\\.\PHYSICALDRIVE$1') } | |
| return $diskDrive.DeviceID | |
| } | |
| } | |
| } catch { | |
| Write-Log "Error getting physical drive: $($_.Exception.Message)" | |
| } | |
| # Final fallback | |
| return "Unknown" | |
| } | |
| # Function to combine MP4 files directly from PowerShell | |
| function Combine-MP4Files { | |
| param ( | |
| [string]$workingDirectory, | |
| [string]$outputFileName | |
| ) | |
| Write-Log "Combining MP4 files in $workingDirectory into $outputFileName" | |
| # Change to the working directory | |
| $currentLocation = Get-Location | |
| Set-Location $workingDirectory | |
| # Create the input.txt file for ffmpeg | |
| Remove-Item "input.txt" -ErrorAction SilentlyContinue | |
| $mp4Files = Get-ChildItem -Path $workingDirectory -Filter "*.MP4" | Sort-Object LastWriteTime | |
| foreach ($file in $mp4Files) { | |
| Write-Log "Adding file to concat list: $($file.Name)" | |
| Add-Content -Path "input.txt" -Value "file '$($file.Name)'" | |
| } | |
| # Remove previous output file if it exists | |
| Remove-Item $outputFileName -ErrorAction SilentlyContinue | |
| # Run ffmpeg to combine the files | |
| Write-Log "Running ffmpeg to combine files..." | |
| $ffmpegArgs = "-f concat -i input.txt -map_metadata 0 -map_chapters 0 -codec copy `"$outputFileName`"" | |
| try { | |
| $process = Start-Process -FilePath "ffmpeg.exe" -ArgumentList $ffmpegArgs -NoNewWindow -Wait -PassThru | |
| if ($process.ExitCode -ne 0) { | |
| Write-Log "ffmpeg process failed with exit code $($process.ExitCode)" | |
| Set-Location $currentLocation | |
| return $false | |
| } | |
| } catch { | |
| Write-Log "Error executing ffmpeg: $($_.Exception.Message)" | |
| Set-Location $currentLocation | |
| return $false | |
| } | |
| # Restore the original location | |
| Set-Location $currentLocation | |
| # Check if the output file was created | |
| if (Test-Path (Join-Path $workingDirectory $outputFileName)) { | |
| Write-Log "MP4 files successfully combined" | |
| return $true | |
| } else { | |
| Write-Log "Failed to create combined file" | |
| return $false | |
| } | |
| } | |
| # Function to create a drive-specific lock file | |
| function Create-DriveLock { | |
| param ( | |
| [string]$driveLetter, | |
| [string]$cameraType | |
| ) | |
| # Create lock directory if it doesn't exist | |
| if (!(Test-Path $lockFilePath)) { | |
| New-Item -ItemType Directory -Path $lockFilePath -Force | Out-Null | |
| } | |
| $lockFile = "$lockFilePath\${driveLetter}_${cameraType}.lock" | |
| Write-Log "Checking lock file: $lockFile" | |
| # Check if lock already exists | |
| if (Test-Path $lockFile) { | |
| $lockAge = (Get-Date) - (Get-Item $lockFile).CreationTime | |
| # If lock is older than 30 minutes, assume it's stale | |
| if ($lockAge.TotalMinutes -gt 30) { | |
| Write-Log "Found stale lock file for drive $driveLetter (age: $($lockAge.TotalMinutes) minutes). Removing." | |
| Remove-Item $lockFile -Force | |
| } else { | |
| Write-Log "Drive $driveLetter is currently being processed by another instance (lock age: $($lockAge.TotalMinutes) minutes). Skipping." | |
| return $false | |
| } | |
| } | |
| # Create lock file | |
| try { | |
| Write-Log "Creating lock file for drive $driveLetter" | |
| New-Item -ItemType File -Path $lockFile -Force | Out-Null | |
| return $true | |
| } catch { | |
| Write-Log "Could not create lock file: $($_.Exception.Message)" | |
| return $false | |
| } | |
| } | |
| # Function to remove a drive-specific lock file | |
| function Remove-DriveLock { | |
| param ( | |
| [string]$driveLetter, | |
| [string]$cameraType | |
| ) | |
| $lockFile = "$lockFilePath\${driveLetter}_${cameraType}.lock" | |
| if (Test-Path $lockFile) { | |
| Write-Log "Removing lock file for drive $driveLetter" | |
| Remove-Item $lockFile -Force | |
| } else { | |
| Write-Log "Lock file for drive $driveLetter not found (already removed)" | |
| } | |
| } | |
| # Function to process a drive based on camera type | |
| function Process-CameraDrive { | |
| param ( | |
| [string]$driveLetter, | |
| [string]$cameraType | |
| ) | |
| Write-Log "Processing $cameraType camera on drive $driveLetter" | |
| # Create a lock for this drive/camera combination | |
| if (!(Create-DriveLock -driveLetter $driveLetter -cameraType $cameraType)) { | |
| return $false | |
| } | |
| try { | |
| # Check if the drive exists | |
| if (!(Test-Path "${driveLetter}:\")) { | |
| Write-Log "Drive $driveLetter not found. Skipping." | |
| return $false | |
| } | |
| # Check if the expected directory exists | |
| $sourceDirs = Get-ChildItem -Path "${driveLetter}:\" -Filter "DCIM" -Directory -ErrorAction SilentlyContinue | |
| if (!$sourceDirs) { | |
| Write-Log "No DCIM directory found on drive $driveLetter. Skipping." | |
| return $false | |
| } | |
| # Find the camera-specific subdirectory | |
| $cameraDir = Get-ChildItem -Path "${driveLetter}:\DCIM" -Directory | Where-Object { | |
| $_.Name -like "DJI_*_${cameraType}*" | |
| } | Select-Object -First 1 | |
| if (!$cameraDir) { | |
| Write-Log "No directory matching ${cameraType} camera pattern found on drive $driveLetter. Skipping." | |
| return $false | |
| } | |
| # Full path to camera directory | |
| $cameraDirPath = Join-Path "${driveLetter}:\DCIM" $cameraDir.Name | |
| Write-Log "Found camera directory: $cameraDirPath" | |
| # Get the current date for the temp directory | |
| $currentDate = Get-Date -Format "MM-dd-yyyy" | |
| # Create a date and camera-specific temp directory | |
| $dateCameraTempDir = Join-Path $rootTempDir "${currentDate}_${cameraType}" | |
| if (!(Test-Path $dateCameraTempDir)) { | |
| Write-Log "Creating temp directory: $dateCameraTempDir" | |
| New-Item -ItemType Directory -Path $dateCameraTempDir -Force | Out-Null | |
| } else { | |
| # Clean any existing files in the temp directory | |
| Write-Log "Cleaning existing files in temp directory" | |
| Remove-Item "$dateCameraTempDir\*" -Force -ErrorAction SilentlyContinue | |
| } | |
| # Copy files from card to temp directory | |
| Write-Log "Copying .mp4 files from $cameraDirPath to $dateCameraTempDir" | |
| $sourceFiles = Get-ChildItem -Path $cameraDirPath -Filter "*.mp4" | Sort-Object Name | |
| # Get date from first file for naming BEFORE copying | |
| if ($sourceFiles.Count -gt 0) { | |
| $firstFile = $sourceFiles | Select-Object -First 1 | |
| $fileDate = $firstFile.CreationTime.ToString("MM_dd_yyyy") | |
| Write-Log "Using date from original file: $fileDate" | |
| } else { | |
| # Fallback to current date if no files found | |
| $fileDate = Get-Date -Format "MM_dd_yyyy" | |
| Write-Log "No source files found, using current date: $fileDate" | |
| } | |
| $outputFileName = "${fileDate}_${cameraType}.mp4" | |
| Write-Log "Output file will be: $outputFileName" | |
| # Now copy the files | |
| foreach ($file in $sourceFiles) { | |
| Write-Log "Copying file: $($file.Name)" | |
| Copy-Item $file.FullName -Destination $dateCameraTempDir -ErrorAction Continue | |
| } | |
| # Get file count | |
| $fileCount = (Get-ChildItem -Path $dateCameraTempDir -Filter "*.mp4").Count | |
| if ($fileCount -eq 0) { | |
| Write-Log "No .mp4 files found in $cameraDirPath or copy operation failed. Skipping processing." | |
| return $false | |
| } | |
| # Get date from first file for naming | |
| $firstFile = Get-ChildItem -Path $dateCameraTempDir -Filter "*.mp4" | Sort-Object Name | Select-Object -First 1 | |
| $fileDate = $firstFile.CreationTime.ToString("MM_dd_yyyy") | |
| $outputFileName = "${fileDate}_${cameraType}.mp4" | |
| $outputFilePath = Join-Path $rootOutputDir $outputFileName | |
| Write-Log "Output file will be: $outputFileName" | |
| # Run the combine function | |
| $success = Combine-MP4Files -workingDirectory $dateCameraTempDir -outputFileName $outputFileName | |
| if (-not $success) { | |
| Write-Log "Failed to combine MP4 files" | |
| return $false | |
| } | |
| # Move the combined file to output directory | |
| if (Test-Path (Join-Path $dateCameraTempDir $outputFileName)) { | |
| Write-Log "Moving combined file to output directory" | |
| Move-Item (Join-Path $dateCameraTempDir $outputFileName) -Destination $outputFilePath -Force | |
| # Delete the entire temp directory | |
| Write-Log "Removing temporary directory" | |
| Remove-Item $dateCameraTempDir -Recurse -Force | |
| Write-Log "Successfully processed $cameraType camera files" | |
| return $true | |
| } else { | |
| Write-Log "Combined file not found. Combine operation may have failed." | |
| return $false | |
| } | |
| } catch { | |
| Write-Log "Error processing drive $driveLetter`: $($_.Exception.Message)" | |
| return $false | |
| } finally { | |
| # Always remove the lock file when done, even if there was an error | |
| Remove-DriveLock -driveLetter $driveLetter -cameraType $cameraType | |
| } | |
| } | |
| # Function to eject drive | |
| function Eject-Drive { | |
| param ( | |
| [string]$driveLetter | |
| ) | |
| Write-Log "Ejecting drive $driveLetter" | |
| try { | |
| $driveEject = New-Object -ComObject Shell.Application | |
| $driveEject.Namespace(17).ParseName("$driveLetter`:\").InvokeVerb("Eject") | |
| Write-Log "Drive $driveLetter ejected successfully" | |
| return $true | |
| } catch { | |
| Write-Log "Failed to eject drive $driveLetter`: $($_.Exception.Message)" | |
| return $false | |
| } | |
| } | |
| # Function to detect camera type from directory structure | |
| function Detect-CameraType { | |
| param ( | |
| [string]$driveLetter | |
| ) | |
| if (Test-Path "${driveLetter}:\DCIM") { | |
| $aDir = Get-ChildItem -Path "${driveLetter}:\DCIM" -Directory | Where-Object { $_.Name -like "DJI_*_A*" } | Select-Object -First 1 | |
| $bDir = Get-ChildItem -Path "${driveLetter}:\DCIM" -Directory | Where-Object { $_.Name -like "DJI_*_B*" } | Select-Object -First 1 | |
| $cDir = Get-ChildItem -Path "${driveLetter}:\DCIM" -Directory | Where-Object { $_.Name -like "DJI_*_C*" } | Select-Object -First 1 | |
| $dDir = Get-ChildItem -Path "${driveLetter}:\DCIM" -Directory | Where-Object { $_.Name -like "DJI_*_D*" } | Select-Object -First 1 | |
| if ($aDir) { | |
| Write-Log "Detected camera type A from directory structure" | |
| return "A" | |
| } | |
| elseif ($bDir) { | |
| Write-Log "Detected camera type B from directory structure" | |
| return "B" | |
| } | |
| elseif ($cDir) { | |
| Write-Log "Detected camera type C from directory structure" | |
| return "C" | |
| } | |
| elseif ($dDir) { | |
| Write-Log "Detected camera type D from directory structure" | |
| return "D" | |
| } | |
| } | |
| Write-Log "Could not detect camera type from directory structure" | |
| return "Unknown" | |
| } | |
| # Function to clear all locks at the start | |
| function Clear-AllLocks { | |
| if (Test-Path $lockFilePath) { | |
| Write-Log "Clearing all existing locks" | |
| $lockFiles = Get-ChildItem -Path $lockFilePath -Filter "*.lock" | |
| foreach ($lockFile in $lockFiles) { | |
| Write-Log "Removing lock file: $($lockFile.Name)" | |
| Remove-Item $lockFile.FullName -Force | |
| } | |
| } else { | |
| Write-Log "No lock directory found, creating one" | |
| New-Item -ItemType Directory -Path $lockFilePath -Force | Out-Null | |
| } | |
| } | |
| # Main processing logic | |
| function Process-Cards { | |
| Write-Log "======= Starting camera import process =======" | |
| # Clear all locks at startup to handle crashed previous runs | |
| Clear-AllLocks | |
| # Make sure root directories exist | |
| if (!(Test-Path $rootTempDir)) { | |
| Write-Log "Creating root temp directory: $rootTempDir" | |
| New-Item -ItemType Directory -Path $rootTempDir -Force | Out-Null | |
| } | |
| if (!(Test-Path $rootOutputDir)) { | |
| Write-Log "Creating root output directory: $rootOutputDir" | |
| New-Item -ItemType Directory -Path $rootOutputDir -Force | Out-Null | |
| } | |
| # Get all removable drives or just the specified one | |
| $removableDrives = @() | |
| if ($TargetDriveLetter -ne "") { | |
| $drive = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DeviceID.Substring(0, 1) -eq $TargetDriveLetter -and $_.DriveType -eq 2 } | |
| if ($drive) { | |
| $removableDrives += $drive | |
| } else { | |
| Write-Log "Target drive $TargetDriveLetter not found or is not a removable drive." | |
| return | |
| } | |
| } else { | |
| $removableDrives = Get-WmiObject Win32_LogicalDisk | Where-Object { $_.DriveType -eq 2 } | |
| } | |
| if ($removableDrives.Count -eq 0) { | |
| Write-Log "No removable drives found" | |
| return | |
| } | |
| Write-Log "Found $($removableDrives.Count) removable drives" | |
| # Track which drives were successfully processed | |
| $processedDrives = @() | |
| foreach ($drive in $removableDrives) { | |
| $driveLetter = $drive.DeviceID.Substring(0, 1) | |
| Write-Log "Evaluating drive: $driveLetter" | |
| # Determine which camera type to process | |
| $cameraType = $TargetCameraType | |
| # If no camera type was specified, detect it | |
| if ($cameraType -eq "") { | |
| $cameraType = Detect-CameraType -driveLetter $driveLetter | |
| # If we couldn't determine from directory structure, try physical drive | |
| if ($cameraType -eq "Unknown") { | |
| $physicalDrive = Get-PhysicalDrive -driveLetter $driveLetter | |
| Write-Log "Drive $driveLetter maps to physical drive: $physicalDrive" | |
| if ($physicalDrive -eq "\\.\PHYSICALDRIVE3") { | |
| $cameraType = "A" | |
| Write-Log "Determined camera type A from physical drive" | |
| } elseif ($physicalDrive -eq "\\.\PHYSICALDRIVE4") { | |
| $cameraType = "B" | |
| Write-Log "Determined camera type B from physical drive" | |
| } | |
| } | |
| } | |
| if ($cameraType -eq "Unknown") { | |
| Write-Log "Could not determine camera type for drive $driveLetter. Skipping." | |
| continue | |
| } | |
| Write-Log "Processing drive $driveLetter as camera type $cameraType" | |
| $success = Process-CameraDrive -driveLetter $driveLetter -cameraType $cameraType | |
| if ($success) { | |
| $processedDrives += $driveLetter | |
| } | |
| } | |
| # Eject the processed drives | |
| foreach ($driveLetter in $processedDrives) { | |
| Eject-Drive -driveLetter $driveLetter | |
| } | |
| Write-Log "======= Processing completed =======" | |
| } | |
| # Execute main function | |
| Process-Cards |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment