Skip to content

Instantly share code, notes, and snippets.

@ryochack
Last active December 29, 2024 17:20
Show Gist options
  • Save ryochack/d5fcfb51a628f01e9ed679dad760d107 to your computer and use it in GitHub Desktop.
Save ryochack/d5fcfb51a628f01e9ed679dad760d107 to your computer and use it in GitHub Desktop.
Migration Scripts from Google Photo to Synology Photos
#!/usr/bin/env bash
# Overwrite EXIF AllDates(DateTimeOriginal, DateTime, DateTimeDigitized) with photoTakenTime value of sidecar json
# The simplest way is to execute the following command, but this method cannot handle the "xxx(1).jpg" / "xxx.jpg(1).json" pair.
# $ exiftool -overwrite_original -d "%s" -tagsFromFile "%d/%F.json" "-AllDates<PhotoTakenTimeTimestamp" -r ${DIR}
function usage() {
cat << EOS
usage: $0 [JPG or DIR]
This script overwrites the DateTimeOriginal value in the EXIF of the JPG with the PhotoTakenTime value in the sidecar json file.
EOS
}
function overwrite_exif_with_json() {
local jpg="$1"
local json
if [[ -f "${jpg}.json" ]]; then
json="${jpg}.json"
elif [[ "${jpg}" =~ ^(.+)(\([0-9]+\))(\..+)$ ]]; then
# For some reason, Google Photo names the xxx(1).jpg sidecar file as xxx.jpg(1).json.
# Therefore, find xxx.jpg(1).json from xxx.jpg(1).jpg.
body="${BASH_REMATCH[1]}" # xxx
bracket="${BASH_REMATCH[2]}" # (1)
ext="${BASH_REMATCH[3]}" # .jpg
json="${body}${ext}${bracket}.json" # xxx.jpg(1).json
if [[ ! -f "${json}" ]]; then
# Missing JSON
return 0
fi
else
# Missing JSON
return 0
fi
exiftool -overwrite_original -d "%s" -tagsFromFile "${json}" "-AllDates<PhotoTakenTimeTimestamp" "${jpg}" -q
}
# ========== main ==========
ORG_IFS=${IFS}
if [ $# -eq 0 ]; then
usage
exit 1
else
TARGET="$1"
fi
if [[ -f ${TARGET} ]]; then
if [[ "${TARGET##*.}" != "jpg" ]]; then
echo "ERROR: require JPG file"
usage
exit 1
fi
validate_exif_and_json_time "${TARGET}"
elif [[ -d "${TARGET}" ]]; then
IFS=$'\n'
dir_list+=($(find ${TARGET} -mindepth 1 -maxdepth 1 -type d | sort))
for subdir in "${dir_list[@]}" ; do
echo ${subdir}
IFS=$'\n'
jpg_list=($(find "${subdir}" -type f -iname "*.jpg"))
IFS=${ORG_IFS}
for jpg in "${jpg_list[@]}" ; do
overwrite_exif_with_json "${jpg}"
done
done
else
echo "${TARGET} is unknown type"
fi
#!/usr/bin/env bash
# Checking target
# * Year/Month/Day/Hour/Minutes of EXIF (DateTimeOriginal)
# * Year/Month/Day/Hour/Minutes of JSON (photoTakenTime)
# * (Year of dir name)
DEFAULT_TZ="Asia/Tokyo"
function usage() {
cat << EOS
usage: $0 [JPG or DIR]
This script checks the validity of the timestamps of photos downloaded from Google Takeout.
Specifically, it compares the DateTimeOriginal value in the EXIF of the JPG with the PhotoTakenTime value in the sidecar json file.
EOS
}
function validate_exif_and_json_time() {
jpg="$1"
# Read EXIF DateTimeOriginal
exif_err=""
exif_time=$(exiftool -T -d "%Y %m %d %H %M %S" -DateTimeOriginal "${jpg}")
status=$?
if [ ${status} -ne 0 ]; then
echo "${jpg} : Failed to read EXIF. It might be broken file."
return 1
fi
if [[ "${exif_time}" == "-" ]]; then
exif_err="Missing EXIF"
else
read eyear emonth eday ehours emin esec <<< $(awk '{print $1, $2, $3, $4, $5, $6}' <<< ${exif_time})
if [[ ! "${eyear}" =~ ^[0-9]{4}$ ]]; then
exif_err="Invalid EXIF(${eyear}-${emonth}-${eday}T${ehours}:${emin}:${esec})"
fi
fi
# Read json photoTakenTime.timestamp
json_err=""
if [[ -f "${jpg}.json" ]]; then
json="${jpg}.json"
elif [[ "${jpg}" =~ ^(.+)(\([0-9]+\))(\..+)$ ]]; then
# For some reason, Google Photo names the xxx(1).jpg sidecar file as xxx.jpg(1).json.
# Therefore, find xxx.jpg(1).json from xxx.jpg(1).jpg.
body="${BASH_REMATCH[1]}" # xxx
bracket="${BASH_REMATCH[2]}" # (1)
ext="${BASH_REMATCH[3]}" # .jpg
json="${body}${ext}${bracket}.json" # xxx.jpg(1).json
if [[ ! -f "${json}" ]]; then
json_err="Missing JSON"
fi
else
json_err="Missing JSON"
fi
if [[ -z "${json_err}" ]]; then
json_time=$(TZ="${DEFAULT_TZ}" jq -r '.photoTakenTime.timestamp | tonumber | strflocaltime("%Y %m %d %H %M %S")' "${json}")
read jyear jmonth jday jhours jmin jsec <<< $(awk '{print $1, $2, $3, $4, $5, $6}' <<< $json_time)
if [[ ! ${jyear} =~ ^[0-9]{4}$ ]]; then
json_err="Invalid JSON(${jyear}-${jmonth}-${jday}T${jhours}:${jmin}:${jsec})"
fi
fi
# Dump error messages if there are problems
if [[ -n "${exif_err}" && -n "${json_err}" ]]; then
echo "${jpg} : ${exif_err} and ${json_err}"
elif [[ -n "${exif_err}" ]]; then
echo "${jpg} : ${exif_err}"
elif [[ -n "${json_err}" ]]; then
echo "${jpg} : ${json_err}"
else
if [[ "${eyear}" != "${jyear}" || "${emonth}" != "${jmonth}" \
|| "${eday}" != "${jday}" || "${ehours}" != "${jhours}" \
|| "${emin}" != "${jmin}" || "${esec}" != "${jsec}" ]]; then
echo "${jpg} : Differ Time"
echo " EXIF: ${eyear}-${emonth}-${eday}T${ehours}:${emin}:${esec}"
echo " JSON: ${jyear}-${jmonth}-${jday}T${jhours}:${jmin}:${jsec}"
fi
fi
}
# ========== main ==========
ORG_IFS=${IFS}
if [ $# -eq 0 ]; then
usage
exit 1
else
TARGET="$1"
fi
if [[ -f ${TARGET} ]]; then
if [[ "${TARGET##*.}" != "jpg" ]]; then
echo "ERROR: require JPG file"
usage
exit 1
fi
validate_exif_and_json_time "${TARGET}"
elif [[ -d "${TARGET}" ]]; then
dir_years=($(find ${TARGET_DIR} -maxdepth 1 -type d -name "Photos from *" | sort | awk '{print $3}'))
for dyear in "${dir_years[@]}" ; do
subdir="Photos from ${dyear}"
IFS=$'\n'
jpg_list=($(find "${subdir}" -type f -iname "*.jpg"))
IFS=${ORG_IFS}
for jpg in "${jpg_list[@]}" ; do
validate_exif_and_json_time "${jpg}"
done
done
else
echo "${TARGET} is unknown type"
fi
#!/usr/bin/env bash
set -eu
EXIFTOOL_VER="12.99"
INSTALL_DIR=/usr/local/share
BIN_DIR=/usr/local/bin
function usage() {
cat << EOS
usage: $0
This script installs ExifTool to Synology DSM7
options:
-u : uninstall exiftool
-h : show this help
EOS
}
function exiftool_install() {
wget https://exiftool.org/Image-ExifTool-${EXIFTOOL_VER}.tar.gz
tar xzf Image-ExifTool-${EXIFTOOL_VER}.tar.gz
rm Image-ExifTool-${EXIFTOOL_VER}.tar.gz
sudo -u root mv Image-ExifTool-${EXIFTOOL_VER} ${INSTALL_DIR}/ExifTool
sudo -u root ln -s ${INSTALL_DIR}/ExifTool/exiftool ${BIN_DIR}/exiftool
}
function exiftool_uninstall() {
sudo -u root rm -rf ${BIN_DIR}/exiftool ${INSTALL_DIR}/ExifTool
}
if [ "$#" -eq 0 ]; then
echo "Install exiftool..."
exiftool_install
echo "Installed exiftool"
else
case $1 in
-u)
echo "Uninstall exiftool..."
exiftool_uninstall
echo "Uninstalled exiftool"
;;
-h)
usage
;;
*)
usage
exit 1
;;
esac
fi
#!/usr/bin/env bash
set -eu
USERNAME=$(whoami)
function usage() {
cat << EOS
usage: $0 (OPTION) [extension...]
This script sorts photos and movies for Synology Photos.
Default target extensions are bellow.
jpg jpeg
arw nrf sr2 cr2 cr3 orf raf rw2
avi mov m4a mp4 mpg mpeg
To add target other extensions, speciry them as arguments.
e.g. $0 xml toml
Options:
--shared : Set Synology shared photo directory to target directory.
Default is personal photo direcotry "/var/services/homes/${USERNAME}/Photos"
If this option is set, Use "/var/services/photo".
--dir DIR: Set target photo directory manually.
EOS
}
PERSONAL_PHOTO_DIR=/var/services/homes/${USERNAME}/Photos
SHARED_PHOTO_DIR=/var/services/photo
IMAGE_EXTENSIONS=("jpg" "jpeg")
RAW_EXTENSIONS=("arw" "nrf" "sr2" "cr2" "cr3" "orf" "raf" "rw2")
MOVIE_EXTENSIONS=("avi" "mov" "m4a" "mp4" "mpg" "mpeg")
EXTENSIONS_LOWERCASE=(${IMAGE_EXTENSIONS[@]} ${RAW_EXTENSIONS[@]} ${MOVIE_EXTENSIONS[@]})
EXTENSIONS_UPPERCASE=(${IMAGE_EXTENSIONS[@]^^} ${RAW_EXTENSIONS[@]^^} ${MOVIE_EXTENSIONS[@]^^})
DEFAULT_TARGET_EXTENSIONS=(${EXTENSIONS_LOWERCASE[@]} ${EXTENSIONS_UPPERCASE[@]})
UNSORT_DIRNAME="UnsortablePhotos"
PHOTO_DIR=${PERSONAL_PHOTO_DIR}
TARGET_EXTENSIONS=(${DEFAULT_TARGET_EXTENSIONS[@]})
# === Parse arguments ===
while (( $# > 0 )); do
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
usage
exit 0
elif [[ "$1" == "--shared" ]]; then
PHOTO_DIR=${SHARED_PHOTO_DIR}
elif [[ "$1" == "--dir" ]]; then
shift
if [[ $# == 0 ]]; then
usage
exit 1
fi
PHOTO_DIR="$1"
else
TARGET_EXTENSIONS+=("$1")
fi
shift
done
echo "target photo dir: ${PHOTO_DIR}"
echo "target extensions: [${TARGET_EXTENSIONS[@]}]"
# === Sort photos uploaded to Synology Photos into date directories(${PHOTO_DIR}/%Y/%m) ===
exiftool -ExtractEmbedded -d "${PHOTO_DIR}/PhotoLibrary/%Y/%m" "-Directory<CreateDate" "-Directory<DateTimeOriginal" ${PHOTO_DIR}
# === Move unsorted files (they have no time info) ===
function get_extensions_exist() {
local extensions=("$@")
found_extensions=()
for ext in "${extensions[@]}"; do
if ls ${PHOTO_DIR}/*."${ext}" &> /dev/null; then
# found
found_extensions+=("${ext}")
fi
done
printf "%s\n" "${found_extensions[@]}" | sort | uniq
}
extensions=($(get_extensions_exist ${TARGET_EXTENSIONS[@]}))
if [[ -n "${extensions[@]}" ]]; then
# Convert from (jpg mp4 ...) to "${PHOTO_DIR}/*.jpg ${PHOTO_DIR}/*.mp4 ..."
unsortable_files="${extensions[*]/#/${PHOTO_DIR}/*.}"
install -d "${PHOTO_DIR}/${UNSORT_DIRNAME}"
mv ${unsortable_files} "${PHOTO_DIR}/${UNSORT_DIRNAME}"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment