Skip to content

Instantly share code, notes, and snippets.

@psa-jforestier
Created December 28, 2025 19:56
Show Gist options
  • Select an option

  • Save psa-jforestier/a48da39cc1f17cb129f061bf649e426c to your computer and use it in GitHub Desktop.

Select an option

Save psa-jforestier/a48da39cc1f17cb129f061bf649e426c to your computer and use it in GitHub Desktop.
A python script to fix date creation / modification of a file according to EXIF metadata. Work with image (thanks to exifread) and video (thanks to pymediainfo)
'''
exifixer : a python script to fix date creation / modification of a file according to EXIF metadata.
Work with image (thanks to exifread) and video (thanks to pymediainfo)
Use pip to install pytz pymediainfo exifread
usage: exifixer.py [-h] [--quick] [--fix] image_file
image_file : file or folder to analyze
--quick : scan only files without a lot of info displayed
--fix : update file creation and modification time based on the oldest date found in metadata (exif or video info)
'''
import exifread
import argparse
import os
import time
import pytz
from datetime import datetime
from pymediainfo import MediaInfo
def format_timestamp_to_exif(timestamp):
return datetime.fromtimestamp(timestamp).strftime('%Y:%m:%d %H:%M:%S')
def format_utc_to_exif(utc_date_str):
# Parse the UTC date string
if (len(utc_date_str) == len("2020-10-21 17:17:52 UTC")):
utc_time = datetime.strptime(utc_date_str, "%Y-%m-%d %H:%M:%S %Z")
elif (len(utc_date_str) == len("2020-10-21 17:17:52.000 UTC")):
utc_time = datetime.strptime(utc_date_str, "%Y-%m-%d %H:%M:%S.%f %Z")
# Set the timezone to UTC
utc_time = utc_time.replace(tzinfo=pytz.utc)
# Convert to local time
local_time = utc_time.astimezone()
# Format the local time to the specified format
local_date_str = local_time.strftime("%Y:%m:%d %H:%M:%S")
return local_date_str
def get_file_timestamps(file_path):
# Get creation and modification timestamps
creation_time = os.path.getctime(file_path)
modification_time = os.path.getmtime(file_path)
# Format the timestamps to match EXIF format
creation_time_exif = format_timestamp_to_exif(creation_time)
modification_time_exif = format_timestamp_to_exif(modification_time)
return creation_time_exif, modification_time_exif
def set_file_dates(file_path, date_string):
# Convert the date string to a datetime object
dt = datetime.strptime(date_string, "%Y:%m:%d %H:%M:%S")
# Get the timestamp in seconds since epoch
timestamp = dt.timestamp()
# Use os.utime to update the access and modification times (change creation time if on Windows)
os.utime(file_path, (timestamp, timestamp))
# On Windows, you may need to set the creation time separately
if os.name == 'nt': # If the OS is Windows
import time
import ctypes
# The following code requires admin privileges on some Windows configurations
handle = ctypes.windll.kernel32.CreateFileW(
file_path,
0x40000000, # GENERIC_WRITE
0, # No sharing
None,
3, # OPEN_EXISTING
0,
None,
)
if handle != -1:
# Convert to FILETIME structure
# FILETIME is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601
creation_time = int(timestamp * 1e7) + 116444736000000000 # Convert to Windows FILETIME
# Create a ctypes structure for FILETIME
class FILETIME(ctypes.Structure):
_fields_ = [("dwLowDateTime", ctypes.c_ulong),
("dwHighDateTime", ctypes.c_ulong)]
ft = FILETIME(creation_time & 0xFFFFFFFF, (creation_time >> 32) & 0xFFFFFFFF)
# Set the creation time
ctypes.windll.kernel32.SetFileTime(handle, ctypes.byref(ft), None, None)
ctypes.windll.kernel32.CloseHandle(handle)
def get_exif_data(image_path):
with open(image_path, 'rb') as f:
tags = exifread.process_file(f)
return tags
def get_video_info(video_path):
media_info = MediaInfo.parse(video_path)
video_data = {}
video_data['Image Make'] = ''
video_data['comandroidversion'] = ''
video_data['Encoded date'] = ''
# print(media_info.to_data())
for track in media_info.tracks:
if (track.track_type == 'General'):
video_data['Encoded date'] = track.encoded_date or track.recorded_date or track.file_last_modification_date
video_data['comandroidversion'] = track.comandroidversion
if track.track_type == 'Video':
video_data['Format'] = track.format
video_data['Duration'] = track.duration
video_data['Width'] = track.width
video_data['Height'] = track.height
video_data['Codec'] = track.codec
video_data['BitRate'] = track.bit_rate
video_data['Encoded date'] = track.encoded_date or video_data['Encoded date']
break
if (len(video_data['Encoded date']) == len("yyyy-mm-dd")):
video_data['Encoded date'] = video_data['Encoded date'] + " 12:00:00 UTC"
video_data['Encoded date'] = format_utc_to_exif(video_data['Encoded date'])
return video_data
def display_media_info(file_path, quick, fix):
print(f"File: {file_path}", end='')
out = "\r\n"
dates = []
if file_path.lower().endswith(('.webp', '.jpg', '.jpeg', '.png', '.tiff', '.gif')):
exif_data = get_exif_data(file_path)
out = out + " Camera Model :" + str(exif_data.get('Image Model')) + "\r\n"
out = out + " Image Make :" + str(exif_data.get('Image Make')) + "\r\n"
out = out + " Image DateTime :" + str(exif_data.get('Image DateTime')) + "\r\n"
out = out + " METADATA DateTimeOriginal :" + str(exif_data.get('EXIF DateTimeOriginal')) + "\r\n"
out = out + " METADATA DateTimeDigitized :" + str(exif_data.get('EXIF DateTimeDigitized')) + "\r\n"
dates.append(str(exif_data.get('Image DateTime')))
dates.append(str(exif_data.get('EXIF DateTimeOriginal')))
dates.append(str(exif_data.get('EXIF DateTimeDigitized')))
elif file_path.lower().endswith(('.amr', '.opus', '.m4a', '.3gp', '.mp4', '.mkv', '.mov', '.avi', '.wmv')):
video_info = get_video_info(file_path)
out = out + " Camera Model :" + str(video_info.get('Format')) + "\r\n"
out = out + " Image Maker :" + str(video_info.get('Codec')) + "\r\n"
out = out + " Image DateTime :" + str(video_info.get('Encoded date')) + "\r\n"
out = out + " METADATA DateTimeOriginal :" + str(video_info.get('Encoded date')) + "\r\n"
out = out + " METADATA DateTimeDigitized :" + str(video_info.get('Encoded date')) + "\r\n"
dates.append(video_info.get('Encoded date'))
else:
print("!! UNKOWN FORMAT !!")
# Get and print file timestamps
creation_time, modification_time = get_file_timestamps(file_path)
out = out + " File Creation Date :" + creation_time + "\r\n"
out = out + " File Modification Date :" + modification_time + "\r\n"
dates.append(creation_time)
dates.append(modification_time)
dates.sort()
oldest = dates[0]
out = out + " ==> " + oldest
if (quick == False):
print(out)
if (quick == True):
print(" \t==> " + oldest, end='')
if (fix == True and oldest != ''):
set_file_dates(file_path, oldest)
print(" Fixed", end='')
print()
# Set up argument parsing
parser = argparse.ArgumentParser(description='Read EXIF data from an image file or video file, or all files in a folder.')
parser.add_argument('image_file', type=str, help='Path to the image or video file or folder to analyze')
parser.add_argument('--quick', action='store_true', help='Display only the filename, but do an internal parsing')
parser.add_argument('--fix', action='store_true', help='Fix file modification and creation date according to the oldest date')
# Parse arguments
args = parser.parse_args()
# Check if the argument is a directory or a file
if os.path.isdir(args.image_file):
# Iterate over all files in the directory
for filename in os.listdir(args.image_file):
file_path = os.path.join(args.image_file, filename)
if os.path.isfile(file_path):
display_media_info(file_path, args.quick, args.fix)
elif os.path.isfile(args.image_file):
# Display information for a single file
display_media_info(args.image_file, args.quick, args.fix)
else:
print(f"Error: {args.image_file} is not a valid file or directory.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment