Functions/GenXdev.FileSystem/Get-MediaFileCreationDate.ps1

<##############################################################################
Part of PowerShell module : GenXdev.FileSystem
Original cmdlet filename : Get-MediaFileCreationDate.ps1
Original author : René Vaessen / GenXdev
Version : 3.24.2026
################################################################################
Copyright (c) René Vaessen / GenXdev
 
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
 
    http://www.apache.org/licenses/LICENSE-2.0
 
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
################################################################################>

<#
.SYNOPSIS
Extracts the best-effort creation date for media and other files.
 
.DESCRIPTION
Attempts several strategies to determine an accurate creation or capture
date for the specified file. strategies include reading image EXIF metadata,
parsing date/time information from filenames, and falling back to the file's
last-write time when no other reliable information is available. the function
always returns a [DateTime]; when no date can be determined it returns
[DateTime]::MinValue.
 
.PARAMETER FilePath
Path to the file to inspect. the path is expanded and normalized by
`GenXdev.FileSystem\Expand-Path` before inspection.
 
.EXAMPLE
Get-MediaFileCreationDate -FilePath '.\IMG_20250601_123000.jpg'
 
.EXAMPLE
#>


function Get-MediaFileCreationDate {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$FilePath
    )
    process {
        # Expand and normalize the incoming path using your custom module helper
        $FilePath = GenXdev.FileSystem\Expand-Path $FilePath

        # Defensive check: if path doesn't exist, gracefully fallback or return epoch/null
        # but to guarantee no throwing, we safely fetch item details
        $fileInfo = $null
        try {
            if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $FilePath -PathType Leaf) {
                $fileInfo = Microsoft.PowerShell.Management\Get-Item -LiteralPath $FilePath -Force
            }
        }
        catch {
            # Suppress errors completely
        }

        if ($null -eq $fileInfo) {
            # Fallback safe return if the file cannot be read or found at all
            return [DateTime]::MinValue
        }

        # --- Method 1: Try Image Metadata for Photos ---
        if ($fileInfo.Extension -match '\.(jpg|jpeg|tif|tiff|png)$') {
            $img = $null
            try {
                # Load picture metadata using the standard .NET System.Drawing namespace
                Microsoft.PowerShell.Utility\Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue

                $img = [System.Drawing.Image]::FromFile($fileInfo.FullName)

                # EXIF tags: 0x9003 = DateTimeOriginal, 0x0132 = DateTime (Modified)
                $dateProperty = $null
                try { $dateProperty = $img.GetPropertyItem(0x9003) } catch {}
                if ($null -eq $dateProperty) {
                    try { $dateProperty = $img.GetPropertyItem(0x0132) } catch {}
                }

                if ($null -ne $dateProperty) {
                    # EXIF string format is typically "YYYY:MM:DD HH:MM:SS\0"
                    $asciiStr = [System.Text.Encoding]::ASCII.GetString($dateProperty.Value)
                    if ($null -ne $asciiStr) {
                        $asciiStr = $asciiStr.Replace("`0", "").Trim()

                        # Convert custom EXIF colons into standard hyphenated date format
                        if ($asciiStr -match '^(\d{4}):(\d{2}):(\d{2})\s(.*)$') {
                            $parsedDateString = "$($Matches[1])-$($Matches[2])-$($Matches[3]) $($Matches[4])"
                            $extractedDate = $parsedDateString -as [DateTime]

                            if ($null -ne $extractedDate) {
                                $img.Dispose()
                                return $extractedDate
                            }
                        }
                    }
                }
            }
            catch {
                # Suppress exceptions completely and step to next method
            }
            finally {
                # Guaranteed cleanup of the file handle lock if open
                if ($null -ne $img) {
                    try { $img.Dispose() } catch {}
                }
            }
        }

        # --- Method 2: Intelligent Filename Parsing ---
        try {
            $baseName = $fileInfo.BaseName
            # Looks for 2026-06-14, 2026_06_14, or 20260614 bordered by non-digits
            $dateRegex = '(?<!\d)(\d{4})([-_]?)(\d{2})\2(\d{2})(?!\d)'

            if ($baseName -match $dateRegex) {
                $year = $Matches[1]
                $month = $Matches[3]
                $day = $Matches[4]

                # Check if there is an accompanying timestamp right after it (e.g., _060619)
                # Looking for an optional non-digit separator followed by HHMMSS
                $timeRegex = "(?<!\d)$year[-_]?$month[-_]?$day[-_ ]?(\d{2})(\d{2})(\d{2})(?!\d)"
                if ($baseName -match $timeRegex) {
                    $hour = $Matches[1]
                    $minute = $Matches[2]
                    $second = $Matches[3]
                    $constructedDateStr = "$year-$month-$day ${hour}:${minute}:$second"
                }
                else {
                    $constructedDateStr = "$year-$month-$day"
                }

                # Natively cast via the safe type operator; never throws an overload error
                $parsedDate = $constructedDateStr -as [DateTime]
                if ($null -ne $parsedDate) {
                    return $parsedDate
                }
            }
        }
        catch {
            # Suppress filename parsing exceptions completely
        }

        # --- Method 3: Ultimate Fallback to File Last Modified Date ---
        try {
            return $fileInfo.LastWriteTime
        }
        catch {
            # Ultimate safety fallback if file IO is entirely restricted
            return [DateTime]::MinValue
        }
    }
}