|
#!/bin/env bash |
|
# Requires: bash >= 4.0, jq >= 1.5, curl >= 7.58.0 |
|
# |
|
# This script uses the immich API to find images that have WhatsApp-like filenames, |
|
# then extracts dates from filenames and timestamps, compares them, and updates the API |
|
# if mismatches are found. It assumes that the filename is correct enough for your needs, |
|
# and that the timestamp is not. |
|
# |
|
# --- Usage --- |
|
# To run this script from the command line, ensure that it is executable and that you |
|
# have the required environment variables set. |
|
# API_KEY: The key used for authenticating API requests. |
|
# BASE_URL: The base URL of the API. |
|
# > API_KEY=XXXX BASE_URL=http://localhost:2283 ./whatsapp.sh |
|
|
|
set -euo pipefail |
|
|
|
# Ensure API_KEY and BASE_URL are set |
|
if [[ -z "${API_KEY:-}" ]]; then |
|
echo "Error: API_KEY is not set." >&2 |
|
exit 1 |
|
fi |
|
|
|
if [[ -z "${BASE_URL:-}" ]]; then |
|
echo "Error: BASE_URL is not set." >&2 |
|
exit 1 |
|
fi |
|
|
|
# --- Functions --- |
|
# icurl: A wrapper around curl for making API requests with JSON headers and |
|
# authentication. |
|
icurl() { |
|
local path="$1" |
|
shift |
|
local response |
|
response=$(/usr/bin/curl "$@" -s -w "\n%{http_code}" --header 'Content-Type: application/json' --header 'Accept: application/json' --header "X-Api-Key: ${API_KEY}" "${BASE_URL}/api${path}") |
|
local http_code |
|
http_code=$(echo "$response" | tail -n1) |
|
local body |
|
body=$(echo "$response" | head -n-1) |
|
|
|
if [[ $http_code -ge 200 && $http_code -le 299 ]]; then |
|
echo "$body" | /usr/bin/jq |
|
else |
|
echo -e "Error: API request failed with status code $http_code\n$body" >&2 |
|
exit 1 |
|
fi |
|
} |
|
|
|
# fetch_metadata: Fetches metadata from the API for a given page. |
|
fetch_metadata() { |
|
local page="$1" |
|
local json_payload |
|
json_payload=$(jq -n --argjson page "$page" '{page: $page, withExif: true, isVisible: true, language: "en-GB", originalFileName: "-WA"}') |
|
icurl /search/metadata -d "$json_payload" |
|
} |
|
|
|
# Tagging constants |
|
TAG_NAME="WHATSAPP_FIXED" |
|
TAG_ID="" |
|
ASSETS_TO_TAG=() |
|
|
|
# get_or_create_tag: Retrieves the tag ID or creates it if it doesn't exist. |
|
get_or_create_tag() { |
|
local tags_response |
|
tags_response=$(icurl /tags -X GET) |
|
|
|
# Try to find existing tag |
|
TAG_ID=$(echo "$tags_response" | jq -r --arg name "$TAG_NAME" '.[] | select(.name == $name) | .id') |
|
|
|
if [[ -z "$TAG_ID" || "$TAG_ID" == "null" ]]; then |
|
echo "Tag '$TAG_NAME' not found. Creating..." |
|
local create_response |
|
local json_payload |
|
json_payload=$(jq -n --arg name "$TAG_NAME" '{"type":"CUSTOM","name":$name}') |
|
create_response=$(icurl /tags -X POST -d "$json_payload") |
|
TAG_ID=$(echo "$create_response" | jq -r '.id') |
|
echo "Created tag '$TAG_NAME' with ID: $TAG_ID" |
|
else |
|
echo "Found tag '$TAG_NAME' with ID: $TAG_ID" |
|
fi |
|
} |
|
|
|
# bulk_tag_assets: Adds the fixed tag to all collected assets. |
|
bulk_tag_assets() { |
|
if [[ ${#ASSETS_TO_TAG[@]} -eq 0 ]]; then |
|
echo "No assets to tag." |
|
return |
|
fi |
|
|
|
echo "Tagging ${#ASSETS_TO_TAG[@]} assets..." |
|
|
|
# Construct JSON payload using jq to ensure correct formatting |
|
local json_payload |
|
# Generate a JSON array of strings from the bash array |
|
local ids_json |
|
ids_json=$(printf '%s\n' "${ASSETS_TO_TAG[@]}" | jq -R . | jq -s .) |
|
|
|
# Construct the final object |
|
json_payload=$(jq -n --argjson ids "$ids_json" '{"ids": $ids}') |
|
|
|
icurl /tags/"${TAG_ID}"/assets -X PUT -d "$json_payload" > /dev/null |
|
echo "Successfully tagged all assets." |
|
} |
|
|
|
# Initialize counters |
|
inspected_count=0 |
|
changed_count=0 |
|
skipped_count=0 |
|
|
|
# process_asset: Processes an asset by comparing and updating date information if |
|
# necessary. Checks for existing tag to skip. |
|
process_asset() { |
|
local asset_id="$1" |
|
local file_name="$2" |
|
local timestamp="$3" |
|
local tags_str="$4" |
|
local exif_date="$5" |
|
|
|
# Check if already tagged |
|
if [[ "$tags_str" == *"$TAG_ID"* ]]; then |
|
skipped_count=$((skipped_count + 1)) |
|
return |
|
fi |
|
|
|
# Increment inspected count |
|
inspected_count=$((inspected_count + 1)) |
|
|
|
# Extract date from filename |
|
local date_from_filename="" |
|
if [[ "$file_name" =~ ([0-9]{8})-WA ]]; then |
|
date_from_filename="${BASH_REMATCH[1]}" |
|
fi |
|
|
|
if [[ -z "$date_from_filename" ]]; then |
|
# echo "Skipping $file_name (no date in filename)" |
|
return |
|
fi |
|
|
|
# Check against EXIF date first (source of truth) |
|
local date_from_exif="" |
|
if [[ -n "$exif_date" && "$exif_date" != "null" ]]; then |
|
# EXIF format is usually YYYY:MM:DD HH:mm:ss or YYYY-MM-DDTHH:mm:ss |
|
# Remove separators to get YYYYMMDD |
|
date_from_exif=$(echo "$exif_date" | cut -c 1-10 | tr -d ':-') |
|
fi |
|
|
|
if [[ "$date_from_exif" == "$date_from_filename" ]]; then |
|
# Date is already correct in EXIF, skip |
|
return |
|
fi |
|
|
|
# Fallback check against localDateTime if EXIF was missing (or just mismatched) |
|
# But technically if EXIF was missing, date_from_exif is empty != date_from_filename, so we proceed. |
|
# If EXIF was present but mismatched, we modify. |
|
|
|
local time_part |
|
time_part=$(echo "$timestamp" | cut -d'T' -f2-) |
|
local year=${date_from_filename:0:4} |
|
local month=${date_from_filename:4:2} |
|
local day=${date_from_filename:6:2} |
|
local new_date_part="${year}-${month}-${day}" |
|
local new_timestamp="${new_date_part}T${time_part}" |
|
|
|
local json_payload |
|
json_payload=$(jq -n --arg dt "$new_timestamp" '{"dateTimeOriginal": $dt}') |
|
|
|
local new_date_time_original |
|
# We only care if the command succeeds, capturing output for logging |
|
new_date_time_original=$(icurl /assets/"${asset_id}" -X PUT -d "$json_payload" | jq -r '.exifInfo.dateTimeOriginal') |
|
|
|
echo "Asset: $asset_id, $file_name (EXIF: $exif_date) -> $new_date_time_original" |
|
|
|
# Queue for tagging |
|
ASSETS_TO_TAG+=("$asset_id") |
|
|
|
# Increment changed count |
|
changed_count=$((changed_count + 1)) |
|
} |
|
|
|
# --- Main Execution --- |
|
|
|
# 1. Get or Create Tag |
|
get_or_create_tag |
|
if [[ -z "$TAG_ID" || "$TAG_ID" == "null" ]]; then |
|
echo "Error: Failed to obtain TAG_ID for '$TAG_NAME'. Exiting." >&2 |
|
exit 1 |
|
fi |
|
|
|
# --- Pagination --- |
|
# PAGE: Tracks the current page of results. |
|
# CONTINUE: Controls whether to continue fetching pages. |
|
PAGE=1 |
|
CONTINUE=true |
|
while [[ "$CONTINUE" == true ]]; do |
|
# Fetch metadata from the API. |
|
RESULT=$(fetch_metadata "$PAGE") |
|
if [[ $(echo "$RESULT" | jq '.assets.items | length') -eq 0 ]]; then |
|
CONTINUE=false |
|
else |
|
# Parse items, including tags and dateTimeOriginal. |
|
while IFS=$'\t' read -r asset_id file_name timestamp tags exif_date; do |
|
process_asset "$asset_id" "$file_name" "$timestamp" "$tags" "$exif_date" |
|
done <<< "$(echo "$RESULT" | jq -r '.assets.items[] | [.id, .originalFileName, .localDateTime, ((.tags // []) | map(.id) | join(",")), .exifInfo.dateTimeOriginal] | @tsv')" |
|
PAGE=$((PAGE + 1)) |
|
fi |
|
done |
|
|
|
# Bulk tag assets |
|
bulk_tag_assets |
|
|
|
# Print summary |
|
echo "Summary: $inspected_count assets inspected, $changed_count dates changed, $skipped_count skipped." |
This worked great. But it ends with an error and the tag is not there. Looks like it is too many pictures. But everything has a date.