Functions/GenXdev.Media/Get-MediaFileCreationDate.ps1

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

.LICENSE
Copyright (C) 2026 René Vaessen / GenXdev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

.PARAMETER FilePath
Path to the file to inspect. the path is expanded and normalized by
`GenXdev\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\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
        }
    }
}