Skip to content

Instantly share code, notes, and snippets.

@iUltimateLP
Last active November 1, 2024 15:48
Show Gist options
  • Save iUltimateLP/77411c6984543ad8bf65dd37d3832c04 to your computer and use it in GitHub Desktop.
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 πŸ’‘
#!/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