Created
December 28, 2025 19:56
-
-
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)
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
| ''' | |
| 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