AVIF convert using inotify
inotify file watcher & AVIF conversion on Arch Linux
- Systemd Service (~/.config/systemd/user/image-watcher.service) - Runs the watch-and-convert.sh script automatically - Restarts on failure with a 10-second delay - Enabled to start at login
- Watch Script (watch-and-convert.sh:17) - Uses inotifywait (the inotify command-line tool) - Monitors ~/path/to/images recursively - Watches for two events: create and moved_to - Outputs the full file path when events occur
- Processing Pipeline - The script pipes inotifywait output to a while read loop - Checks if the file extension is an image format (jpg, png, gif, etc.) - Calls convert-to-avif.py on each new/moved image file
sudo pacman -Syu inotify-tools
systemd service
echo "[Unit]
Description=WebP Image Auto-Converter Watcher
After=default.target
[Service]
Type=simple
ExecStart=/path/to/script/watch-and-convert.sh
Restart=always
RestartSec=10
[Install]
WantedBy=default.target" > ~/.config/systemd/user/image-watcher.service
watch-and-convert.sh
#!/bin/bash
WATCH_DIR="$HOME/path/to/images"
SCRIPT_DIR="$HOME/path/to/script/dir"
CONVERTER="$SCRIPT_DIR/convert-to-avif.py"
echo "Starting image watcher..."
echo "Watching: $WATCH_DIR"
echo "Converting new images to AVIF at 85% quality"
echo ""
# Watch for new files or files moved into the directory
# -m = monitor (don't exit after first event)
# -r = recursive (watch subdirectories)
# -e = events to watch for
# --format = custom output format
inotifywait -m -r -e create -e moved_to --format '%w%f' "$WATCH_DIR" | while read filepath
do
# Get file extension
ext="${filepath##*.}"
ext_lower=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
# Check if it's an image file we want to convert
if "$ext_lower" == "jpg" || "$ext_lower" == "jpeg" || "$ext_lower" == "png" || "$ext_lower" == "gif" || "$ext_lower" == "tiff" || "$ext_lower" == "bmp" (link not found); then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] New image detected: $(basename "$filepath")"
# Small delay to ensure file is fully written
sleep 1
# Convert just this file
cd "$SCRIPT_DIR"
python "$CONVERTER" "$filepath" 2>&1
echo "---"
fi
done
convert-to-avif.py
#!/usr/bin/env python3
#
# This command resizes images in a directory
#
# When paired with a service like inotify
# it will watch the directory for changes
# and change only that new file
#
# If ran against whole directory it will
# convert all images.
#
# You have been warned.
#
import os
import sys
from pathlib import Path
from PIL import Image
import pillow_heif # Provides AVIF support through libheif
# Register HEIF/AVIF formats
pillow_heif.register_heif_opener()
AVIF_QUALITY = 85
MAX_WIDTH = 2000
MAX_HEIGHT = 2000
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.tiff', '.bmp', '.webp'}
def convert_and_delete_original(image_path):
"""Convert an image to AVIF format and delete the original."""
try:
img = Image.open(image_path)
# Skip animated images
if getattr(img, 'is_animated', False):
print(f"⊘ Skipping {image_path.name}: animated images not supported")
return True # Not a failure, just skipped
original_size = img.size
# Check if resize is needed
needs_resize = img.width > MAX_WIDTH or img.height > MAX_HEIGHT
if needs_resize:
# Calculate new dimensions maintaining aspect ratio
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.Resampling.LANCZOS)
# Create output path
avif_path = image_path.with_suffix('.avif')
# Convert to AVIF
img.save(avif_path, 'AVIF', quality=AVIF_QUALITY)
# Delete original
image_path.unlink()
resize_msg = f" [resized from {original_size[0]}x{original_size[1]}]" if needs_resize else ""
print(f"✓ {image_path.name} → {avif_path.name}{resize_msg} (original deleted)")
return True
except Exception as e:
print(f"✗ Failed to convert {image_path.name}: {e}")
return False
def process_directory(dir_path):
"""Recursively process all images in directory."""
converted = 0
failed = 0
for entry in sorted(dir_path.rglob('*')):
if entry.is_file() and entry.suffix.lower() in IMAGE_EXTENSIONS:
if convert_and_delete_original(entry):
converted += 1
else:
failed += 1
return converted, failed
def main():
target_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / 'Documents' / 'planetB' / 'images'
if not target_path.exists():
print(f"Error: Path '{target_path}' does not exist")
sys.exit(1)
# If it's a single file, process just that file
if target_path.is_file():
if target_path.suffix.lower() in IMAGE_EXTENSIONS:
success = convert_and_delete_original(target_path)
sys.exit(0 if success else 1)
else:
print(f"Error: {target_path.name} is not a supported image format")
sys.exit(1)
# Otherwise, process directory
print(f"Converting images to AVIF ({AVIF_QUALITY}% quality)")
print(f"Max dimensions: {MAX_WIDTH}x{MAX_HEIGHT} (larger images will be resized)")
print(f"Source directory: {target_path}")
print("NOTE: Original files will be DELETED after conversion\n")
try:
converted, failed = process_directory(target_path)
print(f"\n✅ Conversion complete!")
print(f" Converted: {converted} images")
if failed > 0:
print(f" Failed: {failed} images")
except Exception as e:
print(f"\n❌ Conversion failed: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
Then run the following to start the service
systemctl --user enable image-watcher.service
systemctl --user start image-watcher.service
systemctl --user status image-watcher.service