Skip to content

Instantly share code, notes, and snippets.

@morrisonbrett
Last active May 1, 2025 18:23
Show Gist options
  • Select an option

  • Save morrisonbrett/4e25bff390fe8ee3e8bdfcef8b98b6cd to your computer and use it in GitHub Desktop.

Select an option

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