I have a bunch of panorama’s, made with phones or my Niko DLSR stitched with Hugin. These are not being detected as panorama by Immich, due to missing panorama tags … Here is my fix
I’ve got a bunch of media in my library with the following issues:
Images : too dark (rotation can be done within Immich)
Videos : too dark, interlaced or rotated
I used to fix these by removing from Immich, change original media, and upload again. Then I had to fix the date of media with missing exif dates, using touch command.
Now I have a script which is doing the following:
Copy the URL containing the media
It downloads the media using API calls
It downloads the metadata for dates and tags
I calls a second python script in which you can do the following: Change contrast/brightness, rotate CW or CCW, and select deinterlace when needed.
Write the new image or movie to a temp file
When exiting the editor the first called python script uploads the altered media and fixes tags and dates
After that some jobs are being started: Thumbnail recreation, Encoded videos, metadata and face recognition.
Takes only a few seconds to fix an asset
No code, because of some Vibe coding. PM me if interested ..
If your data is in a external library, you can do this on the file in the library. After that you have to select it and do the following:
Refresh encoded videos
Refresh metadata
Refresh thumbnails
#!/bin/bash
#set -x
if [ "$#" -lt 2 ]; then
echo >&2 'fix filename 90||-90||0'
exit 1
fi
fix=0
exiftool $1 | grep "^Create Date" | grep 0000 && fix=1
dater=$(realpath $1| grep -oE '[0-9]{8}' | head -1 )
if [ $2 == "90" ] ; then
ffmpeg -i $1 -vf 'transpose=1' -c:a copy tmp_${1}
mv tmp_${1} $1
fi
if [ $2 == "-90" ] ; then
ffmpeg -i $1 -vf 'transpose=2' -c:a copy tmp_${1}
mv tmp_${1} $1
fi
if [ $fix == "1" ] ; then
echo "Fixing date"
touch -t ${dater}1200.00 $1
fi
Get location of file by pressing the (i)
Deinterlace video and touch date, when no exif date is embedded
#!/bin/bash
mkdir -p deinterlace
for video in DV*; do
[ -f "$video" ] || continue
: >2.log
echo "testing $video"
ffmpeg -i "$video" -filter:v idet -frames:v 500 -an -f rawvideo -y /dev/null 2>2.log
inter=$(
grep Parsed 2.log |
tail -2 |
cut -d: -f2- |
awk '
{
inter += $2 + $4
prog += $6
}
END {
print (inter > prog ? 1 : 0)
}'
)
if [ "$inter" -eq 1 ]; then
echo "convert $video"
ffmpeg -i "$video" -vf bwdif -c:v libx264 -crf 18 -preset medium \
-c:a aac -b:a 192k "deinterlace/${video%.*}-deinterlaced.mp4"
fi
dt=$(echo "$video" | grep -oE '[0-9]{2}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}' | tr '_' ' ' | tr '-' ' ')
if [ -n "$dt" ]; then
# convert DD-MM-YY HH-MM to YYYY-MM-DD HH:MM
read year month day hour minute <<< "$dt"
# assume 20xx for year
year=$((2000 + year))
touch -d "$year-$month-$day $hour:$minute:00" "deinterlace/${video%.*}-deinterlaced.mp4"
touch -d "$year-$month-$day $hour:$minute:00" "$video"
fi
done
FIX DATES FILES in /2006/20060701/ structure
This touches files which have no exif info, to the date of the directory to that date with time 12:00
find . -type f -iname "*.mpg" | while read -r file; do ymd=$(echo "$file" | grep -oE '[0-9]{8}' | head -1); touch -t "${ymd}1200.00" "$file"; done
Touch date of current directory to date in path
for f in *; do
[[ $f =~ ([0-9]{8}) ]] || continue
touch -t "${BASH_REMATCH[1]}1200.00" "$f"
done
Broken thumb nail fix
# goto movie file in your library
# Add flags using below command
ffmpeg -i 73a1a221be4c.mp4 -c copy -video_track_timescale 90000 -movflags +faststart output.mp4
# replace movie file
cat output.mp4 > 73a1a221be4c.mp4
rm output.mp4
# then fresh encoded videos in immich, then refresh metadata and thumbnails.
FIX Date unknown or incorrect dates
#!/bin/bash
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <movie-file> <YYYYMMDD>"
exit 1
fi
FILE="$1"
DATE="$2"
# Validate date format
if [[ ! "$DATE" =~ ^[0-9]{8}$ ]]; then
echo "Error: date must be YYYYMMDD"
exit 1
fi
# Convert YYYYMMDD -> YYYY:MM:DD 12:00:00 for EXIF metadata
YEAR="${DATE:0:4}"
MONTH="${DATE:4:2}"
DAY="${DATE:6:2}"
EXIFDATE="${YEAR}:${MONTH}:${DAY} 12:00:00"
# touch format: YYYYMMDD1200.00
TOUCHDATE="${DATE}1200.00"
echo "File: $FILE"
echo "Date: $DATE"
echo "Metadata: $EXIFDATE"
echo "Filesystem: $TOUCHDATE"
# Change movie metadata dates
exiftool -overwrite_original \
"-CreateDate=$EXIFDATE" \
"-ModifyDate=$EXIFDATE" \
"-TrackCreateDate=$EXIFDATE" \
"-TrackModifyDate=$EXIFDATE" \
"-MediaCreateDate=$EXIFDATE" \
"-MediaModifyDate=$EXIFDATE" \
"$FILE"
if [ $? -ne 0 ]; then
echo "Error: exiftool failed"
exit 1
fi
# Change filesystem timestamps
touch -t "$TOUCHDATE" "$FILE"
if [ $? -ne 0 ]; then
echo "Error: touch failed"
exit 1
fi
UPDATE 20260729 : There are EXIF filters in workflows now!
Put your media in the correct directories using workflows.
Below the regex for a Samsung phone
Image
^\d{8}_\d{6}\.(jpg|jpeg|heic|png)$
Video
^\d{8}_\d{6}\.(mp4|mov|mkv)$
WhatsApp Image
^IMG-\d{8}-WA\d+\.(jpg|jpeg|png|webp)$
WhatsApp Video
^VID-\d{8}-WA\d+\.(mp4|3gp)$
Signal Image
^(signal-|Signal-).*\.(jpg|jpeg|png|webp)$
Signal Video
^(signal-|Signal-).*\.(mp4|mov)$
When ingesting WhatsApp media, the dates in the database will contain the ingest date. This is because the GPS/Date and other exif information are removed from the Media in WhatsApp.
NOTES:
Always import your camera media first, these will contain all exif info, if you upload WhatsApp media containing the same image it can be skipped. (Look for deduplication tip below)
WhatsApp autouploaded using the App on your phone rarely needs adjusting. (Taking a photo and uploading it the same day will fix the wrong day issue)
Luckily the WhatsApp media contains the date in the filename.
This software will connect to your immich instance, searches for ingestdates and whatsapp filenames discrepancies. And wil fix these in the immich database.
I’ve got a directory containing above code for every user, with their own .env file, and custom filters
Sometimes media has a incorrect GPS location, or it is missing, or as above set as 0:0
You CAN change the location of Images using the MAP in Immich. (Select MAP > Day or image > Menu: Change location) (Also under Utilities) Immich WILL NOT change your image!, It will write a sidecar file with updated location info.
How I like to fix this: Download the images for which you want to remove the GPS information. Delete from Immich. Run below script over those images to remove Exif information and reupload.
exiftool -gps:all= FILENAME
Loads of the same images
Deduplicate? : Use Utilities > Review duplicates
Select camera instead of WhatsApp image to keep. (Most of the time bigger and has all exif information!)
Burst photos or simular photos? Use Stacking. This will show only ONE thumbnail in albums/timeline.
Manage people data in bulk : Options to update people data in bulk, and with advance filters
People Merge Suggestion : Option to bulk merge people with suggested faces based on similarity.
Update Missing Locations : Find assets in your library those are without location and update them with the location of the asset.
Potential Albums : Find albums that are potential to be created based on the assets and people in your library.
Analytics : Get analytics on your library like assets over time, exif data, etc.
Smart Search : Search your library with natural language, supports queries like “show me all my photos from 2024 of “
Bulk Date Offset : Offset the date of selected assets by a given amount of time. Majorly used to fix the date of assets that are out of sync with the actual date.
PYTHON script to download an album (with a filename filter)
NOTE: At the bottom you can remove the # comments to also REMOVE from immich
import requests
import os
IMMICH_URL = "http://192.168.1.2:2283/api"
API_KEY = "2Nk4sO4eEm001Cm1Dsnl3XXXXXXXXXXXXXXX"
ALBUM_ID = "c4ce0661-0c4c-4c49-b6c1-XXXXXXXXXXXXXXXXXXXXX"
FILENAME_PREFIX = "VID_" # filename filter
HEADERS = {
"x-api-key": API_KEY
}
DOWNLOAD_DIR = "./downloaded"
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
def get_album_assets(album_id):
r = requests.get(
f"{IMMICH_URL}/albums/{album_id}",
headers=HEADERS
)
r.raise_for_status()
return r.json()["assets"]
def filter_assets(assets):
# simulate SQL LIKE 'IMG_2023%'
return [
a for a in assets
if a["originalFileName"].startswith(FILENAME_PREFIX)
]
def download_asset(asset):
asset_id = asset["id"]
filename = asset["originalFileName"]
url = f"{IMMICH_URL}/assets/{asset_id}/original"
r = requests.get(url, headers=HEADERS, stream=True)
r.raise_for_status()
path = os.path.join(DOWNLOAD_DIR, filename)
with open(path, "wb") as f:
for chunk in r.iter_content(8192):
f.write(chunk)
return path
def delete_assets(asset_ids):
r = requests.delete(
f"{IMMICH_URL}/assets",
headers=HEADERS,
json={"ids": asset_ids}
)
r.raise_for_status()
def main():
print("Fetching album assets...")
assets = get_album_assets(ALBUM_ID)
print(f"Total assets in album: {len(assets)}")
print("Filtering by filename...")
filtered = filter_assets(assets)
print(f"Matched assets: {len(filtered)}")
downloaded = []
print("Downloading...")
for asset in filtered:
try:
path = download_asset(asset)
downloaded.append((asset["id"], path))
except Exception as e:
print(f"Download failed: {asset['id']} - {e}")
# VERIFY
print("Verifying...")
if len(downloaded) != len(filtered):
print("Download mismatch. Abort delete.")
return
for _, path in downloaded:
if not os.path.exists(path) or os.path.getsize(path) == 0:
print(f"Invalid file: {path}")
return
print("Verification OK")
# DELETE
ids_to_delete = [asset_id for asset_id, _ in downloaded]
#print("Deleting assets...")
#delete_assets(ids_to_delete)
print("Done!")
if __name__ == "__main__":
main()