Last active
November 1, 2024 15:48
-
-
Save iUltimateLP/77411c6984543ad8bf65dd37d3832c04 to your computer and use it in GitHub Desktop.
β‘ Bash script that automates Exiftool to feed back metadata from a Google Photos Takeout Export's JSON sidecar format into the original media files π‘
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
#!/bin/bash | |
# This is a small helper script that utilizes Exiftool to feed back metadata from a Google Photos Takeout Export's JSON sidecar format | |
# into the original media files | |
# Usage: ./migrate-takeout -i <input_dir> -o <output_dir> -e <exiftool_path> | |
# Tells Bash to stop on errors if variables are unset | |
set -eu | |
# Parse command line options | |
while getopts i:o:e: opt ; do | |
case "$opt" in | |
i) input_dir=$OPTARG ;; | |
o) output_dir=$OPTARG ;; | |
e) exiftool_path=$OPTARG ;; | |
*) echo -e "\033[0;33mInvalid options specified. Usage: ./migrate-takeout -i <input_dir> -o <output_dir> -e <exiftool_path>\033[0m" ; exit 2 ;; | |
esac | |
done | |
shift $((OPTIND-1)) | |
echo -e "\033[0;36mRunning Google Photos migration script on input \"$input_dir\" and output \"$output_dir\".\033[0m" | |
# Check if input dir exists | |
if [ ! -d "$input_dir" ]; then | |
echo -e "\033[0;33mInput dir \"$input_dir\" does not exist, exiting.\033[0m" | |
exit | |
fi | |
# Convert relative paths to absolute | |
input_dir=$(realpath "$input_dir") | |
output_dir=$(realpath "$output_dir") | |
exiftool_path=$(realpath "$exiftool_path") | |
# Create the output directory (and ignore if it already exists) | |
mkdir -p "$output_dir" | |
# Iterate recursively over all media files in the target directory | |
find "$input_dir" -type f -iname "*.jpg" -o -iname "*.png" -o -iname "*.mp4" -o -iname "*.mov" | while read -r file; | |
do | |
# Figure out a few variables based on the file name | |
media_file="$file" | |
extension="${file##*.}" | |
file_name=$(basename "$media_file") | |
base_name="${file_name%.*}" # Remove extension | |
out_file="${output_dir}/${base_name}.$extension" | |
# Figure out the path of the JSON sidecar. Since JSON sidecar names can be cropped | |
# (e.g. Screenshot_2022-12-01-06-44-03-257_com.snapchat.jpg => Screenshot_2022-12-01-06-44-03-257_com.snapcha.json) | |
# First, lets check if the file is already present without any changes | |
json_sidecar="${file_name}.supplemental-metadata.json" | |
json_file=$(find "$input_dir" -type f -name "$(basename "$json_sidecar")" | head -n 1) | |
# If JSON file not found yet, apply progressive trimming to handle truncation | |
if [ ! -f "$json_file" ]; then | |
temp_name="${file_name}.supplemental-metadata" | |
while [ -z "$json_file" ] && [ ${#temp_name} -gt 0 ]; do | |
json_file=$(find "$input_dir" -type f -name "$(basename "$temp_name").json" | head -n 1) | |
temp_name="${temp_name:0:-1}" # Remove last character from temp_name if no match | |
done | |
fi | |
# Check if file exists now | |
if [ ! -f "$json_file" ]; then | |
echo -e "\033[0;33mFile \"$media_file\" has no JSON sidecar, ignoring.\033[0m" | |
continue # Skip if file is still not existing | |
fi | |
# Retrieve a few params from JSON sidecar (will return nothing if value is null or zero) | |
latitude=$(jq '.geoData.latitude | select( . != null and . != 0 )' "$json_file") | |
longitude=$(jq '.geoData.longitude | select( . != null and . != 0 )' "$json_file") | |
altitude=$(jq '.geoData.altitude | select( . != null and . != 0 )' "$json_file") | |
google_photos_origin=$(jq '.googlePhotosOrigin.mobileUpload.deviceType | select( . != null )' "$json_file" | tr -d '"') | |
echo -e "\033[0;36mProcessing $media_file => $out_file\033[0m" | |
# A few Exiftool parameters explained: | |
# -q quiet processing | |
# -dateFormat how to format dates (%s means as a string, which uses internal UNIX timestamp conversion then) | |
# -tagsFromFile instead of loading EXIF metadata tags from the input media file, load tags from this file instead (JSON is supported) | |
# -o store the output in the following path | |
# The "stringified" arguments are all EXIF tags that are written. Access to the tags loaded from -tagsFromFile is allowed here, which gives us access to | |
# stuff like "PhotoTakenTimeTimestamp" or "CreationTimeTimestamp" which happen to be part of the JSON. The rest is fed in from jq. | |
exiftool_common_options=( | |
-q | |
-dateFormat "%s" | |
-tagsFromFile "$json_file" | |
) | |
exiftool_date_options=( | |
"-AllDates<PhotoTakenTimeTimestamp" | |
"-XMP-Exif:DateTimeOriginal<PhotoTakenTimeTimestamp" | |
"-PNG:CreationTime<PhotoTakenTimeTimestamp" | |
"-QuickTime:TrackCreateDate<PhotoTakenTimeTimestamp" | |
"-QuickTime:TrackModifyDate<PhotoTakenTimeTimestamp" | |
"-QuickTime:MediaCreateDate<PhotoTakenTimeTimestamp" | |
"-QuickTime:MediaModifyDate<PhotoTakenTimeTimestamp" | |
"-DateTimeOriginal<PhotoTakenTimeTimestamp" | |
"-FileCreateDate<PhotoTakenTimeTimestamp" | |
"-FileModifyDate<PhotoTakenTimeTimestamp" | |
) | |
exiftool_gps_options=( | |
"-GpsLatitude*=$latitude" | |
"-GpsLongitude*=$longitude" | |
"-GpsAltitude*=$altitude" | |
-GPSCoordinates="$latitude, $longitude, $altitude" | |
) | |
exiftool_keyword_options=( | |
-Keywords+="$google_photos_origin" # This is mostly used for photos | |
) | |
# Test if GPS coordinates were found | |
if [ -z "$latitude" ] || [ -z "$longitude" ] | |
then | |
# Run command line with just dates, no GPS data | |
$exiftool_path "${exiftool_common_options[@]}" "${exiftool_date_options[@]}" "${exiftool_keyword_options[@]}" -o "$out_file" "$media_file" | |
else | |
# Run command line with GPS data | |
$exiftool_path "${exiftool_common_options[@]}" "${exiftool_date_options[@]}" "${exiftool_gps_options[@]}" "${exiftool_keyword_options[@]}" -o "$out_file" "$media_file" | |
fi | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment