Last active
November 7, 2020 15:59
-
-
Save outadoc/4e9538f40bcb607c83db525f1e0a67d1 to your computer and use it in GitHub Desktop.
Google Photos Takeout export EXIF fixer
This file contains 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
# sort in date/month directories | |
exiftool -r "-Directory<DateTimeOriginal" -d "%Y/%m" -v . | |
# use CreationDate if DateTimeOriginal didn't work | |
exiftool "-Directory<CreationDate" -d "%Y/%m" -v . |
This file contains 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
#!/usr/bin/env python3 | |
import argparse | |
import os | |
import re | |
from pathlib import Path | |
import json | |
from datetime import datetime, timezone | |
def main(): | |
parser = argparse.ArgumentParser() | |
parser.add_argument('photos_directory') | |
parser.add_argument('metadata_directory') | |
args = parser.parse_args() | |
photos_directory = os.path.abspath(args.photos_directory) | |
metadata_directory = os.path.abspath(args.metadata_directory) | |
print("photos dir: \t" + photos_directory) | |
print("metadata dir: \t" + metadata_directory) | |
if not confirm(): | |
exit(1) | |
jsons = list(Path(metadata_directory).glob("*")) | |
photos = list(Path(photos_directory).rglob("*")) | |
photos_timestamps = dict((x, y) for x, y in process_metadata_list(jsons)) | |
process_photo_list(photos, photos_timestamps) | |
def process_metadata_list(jsons): | |
total = len(jsons) | |
i = 0 | |
for file in jsons: | |
i = i + 1 | |
print("processing metadata file " + str(i) + " out of " + str(total)) | |
if file.is_file(): | |
m = process_metadata(file) | |
#print(m) | |
if m: | |
yield m | |
def process_metadata(file): | |
with file.open() as f: | |
j = json.load(f) | |
if 'title' in j: | |
title = j['title'].replace('%', '_') | |
return (title, { | |
'taken': int(j['photoTakenTime']['timestamp']), | |
'created': int(j['creationTime']['timestamp']), | |
'modified': int(j['modificationTime']['timestamp']), | |
'latitude': j['geoData']['latitude'], | |
'longitude': j['geoData']['longitude'], | |
'altitude': j['geoData']['altitude'] | |
}) | |
else: | |
return None | |
def process_photo_list(photos, photos_timestamps): | |
total = len(photos) | |
i = 0 | |
for file in photos: | |
i = i + 1 | |
print("processing " + str(i) + " out of " + str(total)) | |
if file.is_file(): | |
basename = normalize_photo_name(os.path.basename(file.name)) | |
if basename in photos_timestamps: | |
process_photo(file, photos_timestamps[basename]) | |
else: | |
print('w: no metadata found for ' + basename) | |
def normalize_photo_name(basename): | |
#return re.sub(r'\([0-9]+\)\.', '.', basename) | |
return basename | |
def process_photo(file, photo_metadata): | |
print(file) | |
cmd = ( | |
"exiftool -overwrite_original " | |
"-FileCreateDate=\"" + | |
timestamp_to_datetime(photo_metadata['created']) + "\" " | |
"-FileModifyDate=\"" + | |
timestamp_to_datetime(photo_metadata['modified']) + "\" " | |
"-DateTimeOriginal=\"" + | |
timestamp_to_datetime(photo_metadata['taken']) + "\" " | |
"-xmp:dateTimeOriginal=\"" + | |
timestamp_to_datetime_extended(photo_metadata['taken']) + "\" " | |
"-GPSLatitudeRef=" + str(photo_metadata['latitude']) + " " | |
"-GPSLongitudeRef=" + str(photo_metadata['longitude']) + " " | |
"-GPSAltitudeRef=" + str(photo_metadata['altitude']) + " " | |
"\"" + str(file) + "\"" | |
) | |
#print(cmd) | |
os.system(cmd) | |
def timestamp_to_datetime(timestamp): | |
time = datetime.fromtimestamp(timestamp, tz=timezone.utc) | |
return time.strftime("%Y:%m:%d %H:%M:%S") | |
def timestamp_to_datetime_extended(timestamp): | |
time = datetime.fromtimestamp(timestamp, tz=timezone.utc) | |
return time.strftime("%Y:%m:%d %H:%M:%SZ") | |
def confirm(): | |
answer = "" | |
while answer not in ["y", "n"]: | |
answer = input("is this ok? [Y/N] ").lower() | |
return answer == "y" | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment