Created
July 1, 2025 15:13
-
-
Save zsarge/e0c49cab7f287f30f8e1628d2dc0c6cb to your computer and use it in GitHub Desktop.
Update the times on photos based on their names.
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
""" | |
When you download photos from Google Photos, the files do not keep the appropriate Modified/Created times. | |
This script takes a folder path, and loops through the file names of all images from Google Photos. | |
If the images are images, the EXIF data is updated; Otherwise, we just update the file times. | |
This script was based on https://github.com/valentin-stamate/photo-date/tree/master | |
with input from Deepseek V3. | |
""" | |
import os | |
from datetime import datetime | |
import piexif | |
import re | |
def parse_filename_to_datetime(filename: str) -> datetime | None: | |
"""Parse datetime from filename using multiple patterns""" | |
# Pattern 1: PXL_YYYYMMDD_HHMMSS... (Google Pixel) | |
if filename.startswith('PXL_'): | |
parts = filename.split('_') | |
if len(parts) >= 3: | |
date_str = parts[1] | |
time_str = parts[2] | |
if len(date_str) == 8 and date_str.isdigit(): | |
# Extract leading digits from time portion | |
num_part = ''.join(char for char in time_str if char.isdigit()) | |
if len(num_part) >= 6: | |
time_part = num_part[:6] # Use first 6 digits (HHMMSS) | |
try: | |
return datetime.strptime(date_str + time_part, "%Y%m%d%H%M%S") | |
except ValueError: | |
pass | |
# Pattern 2: YYYYMMDDHHMMSS... (timestamp prefix) | |
if len(filename) >= 14 and filename[:14].isdigit(): | |
try: | |
return datetime.strptime(filename[:14], "%Y%m%d%H%M%S") | |
except ValueError: | |
pass | |
# Pattern 3: Standard camera names (IMG_xxxx, DSC_xxxx, etc.) | |
std_cam_pattern = r'^(IMG|DSC|PXL|MOV)_(\d{4,})' | |
match = re.match(std_cam_pattern, filename) | |
if match: | |
num_part = match.group(2) | |
if len(num_part) >= 8: | |
try: | |
# Try to parse as YYYYMMDD if we have at least 8 digits | |
return datetime.strptime(num_part[:8], "%Y%m%d") | |
except ValueError: | |
pass | |
return None | |
def update_image_metadata(path, date_time): | |
"""Update EXIF metadata and file timestamps for images""" | |
file_name = os.path.basename(path) | |
date_time_str = date_time.strftime("%Y:%m:%d %H:%M:%S") | |
try: | |
exif_dict = piexif.load(path) | |
# Update EXIF datetime fields | |
exif_dict['0th'][piexif.ImageIFD.DateTime] = date_time_str | |
exif_dict['Exif'][piexif.ExifIFD.DateTimeOriginal] = date_time_str | |
exif_dict['Exif'][piexif.ExifIFD.DateTimeDigitized] = date_time_str | |
exif_bytes = piexif.dump(exif_dict) | |
piexif.insert(exif_bytes, path) | |
# Update file system timestamps | |
update_file_timestamps(path, date_time) | |
print(f'Image: %-70s modified | New datetime: %s' % (file_name, date_time_str)) | |
except Exception as e: | |
print(f'Image: %-70s ERROR: %s' % (file_name, str(e))) | |
def update_file_timestamps(path, date_time): | |
"""Update file system timestamps only""" | |
timestamp = datetime.timestamp(date_time) | |
os.utime(path, (timestamp, timestamp)) | |
def visit(file_path: str): | |
"""Process individual file""" | |
name = os.path.basename(file_path) | |
ext = os.path.splitext(file_path)[1].lower() | |
# Extract datetime from filename | |
new_date_time = parse_filename_to_datetime(name) | |
if not new_date_time: | |
print(f"File: %-70s skipped (no date in filename)" % name) | |
return | |
# Handle images | |
if ext in ['.jpg', '.jpeg', '.tif', '.tiff']: | |
update_image_metadata(file_path, new_date_time) | |
# Handle videos | |
elif ext in ['.mp4', '.mov', '.avi', '.mkv']: | |
update_file_timestamps(file_path, new_date_time) | |
print(f'Video: %-70s timestamps updated | New datetime: %s' % (name, new_date_time)) | |
# Skip other file types | |
else: | |
print(f"File: %-70s skipped (unsupported format)" % name) | |
def change_recursive(root: str): | |
"""Process directory recursively""" | |
for entry in os.listdir(root): | |
path = os.path.join(root, entry) | |
if os.path.isdir(path): | |
change_recursive(path) | |
else: | |
visit(path) | |
def main(): | |
directory = input("Insert the path to your photos and videos: ") | |
change_recursive(directory) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment