WordPress Media Library Automation with WP-CLI: Complete Guide

Managing thousands of media files in WordPress becomes tedious through the dashboard. WP-CLI transforms media library management from a manual chore into an automated, scriptable process, saving hours of work and ensuring consistency across large image collections.

Understanding WordPress Media Management

WordPress stores media files in the uploads directory while maintaining database records for each attachment. These records include metadata like file paths, dimensions, captions, alt text, and custom fields. Effective media automation requires managing both filesystem operations and database records simultaneously.

The media library grows rapidly on content-heavy sites. Manual management becomes impractical when dealing with bulk operations like regenerating thumbnails, updating metadata, or migrating files. WP-CLI provides the tools to automate these tasks efficiently.

Bulk Image Import and Upload

Traditional WordPress uploads through the dashboard are slow for large batches. WP-CLI enables efficient bulk imports from local directories or remote URLs.

# Import single image
wp media import /path/to/image.jpg --post_id=123 --title="Image Title" --alt="Alt text"

# Import all images from a directory
wp media import /path/to/images/*.jpg --porcelain

# Import images and assign to specific post
for file in /path/to/images/*.jpg; do
    wp media import "$file" --post_id=456 --featured_image
done

# Import from URL
wp media import https://example.com/image.jpg --title="Remote Image"

# Bulk import with metadata from CSV
while IFS=',' read -r filepath title alt caption; do
    wp media import "$filepath" --title="$title" --alt="$alt" --caption="$caption"
done < images.csv

Create a comprehensive bulk import script:

#!/bin/bash
# bulk-media-import.sh - Import media with metadata

set -euo pipefail

WP_PATH="/var/www/html"
IMPORT_DIR="/path/to/import"
LOG_FILE="/var/log/media-import-$(date +%Y%m%d).log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# Check if import directory exists
if [ ! -d "$IMPORT_DIR" ]; then
    log "ERROR: Import directory does not exist: $IMPORT_DIR"
    exit 1
fi

log "Starting bulk media import from $IMPORT_DIR"

# Count files to process
TOTAL_FILES=$(find "$IMPORT_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" \) | wc -l)
log "Found $TOTAL_FILES files to import"

IMPORTED=0
FAILED=0

# Process each image
find "$IMPORT_DIR" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.gif" \) | while read -r file; do
    FILENAME=$(basename "$file")
    log "Processing: $FILENAME"

    # Extract metadata from filename (example: product-123-description.jpg)
    if [[ "$FILENAME" =~ ^([^-]+)-([0-9]+)-(.+)\..+$ ]]; then
        TYPE="${BASH_REMATCH[1]}"
        POST_ID="${BASH_REMATCH[2]}"
        DESCRIPTION="${BASH_REMATCH[3]}"

        # Import with metadata
        if ATTACHMENT_ID=$(wp --path="$WP_PATH" media import "$file" \
            --post_id="$POST_ID" \
            --title="$DESCRIPTION" \
            --alt="$DESCRIPTION" \
            --porcelain 2>&1); then

            log "SUCCESS: Imported $FILENAME (ID: $ATTACHMENT_ID)"
            ((IMPORTED++))

            # Move processed file to archive
            mkdir -p "$IMPORT_DIR/processed"
            mv "$file" "$IMPORT_DIR/processed/"
        else
            log "ERROR: Failed to import $FILENAME: $ATTACHMENT_ID"
            ((FAILED++))
        fi
    else
        # Import without post association
        if ATTACHMENT_ID=$(wp --path="$WP_PATH" media import "$file" --porcelain 2>&1); then
            log "SUCCESS: Imported $FILENAME (ID: $ATTACHMENT_ID)"
            ((IMPORTED++))
            mv "$file" "$IMPORT_DIR/processed/"
        else
            log "ERROR: Failed to import $FILENAME"
            ((FAILED++))
        fi
    fi
done

log "Import completed. Imported: $IMPORTED, Failed: $FAILED"

Thumbnail Regeneration

Changing themes or image sizes requires regenerating thumbnails for existing images. WP-CLI makes this process efficient.

# Regenerate all thumbnails
wp media regenerate --yes

# Regenerate only missing thumbnails
wp media regenerate --only-missing --yes

# Regenerate for specific image sizes
wp media regenerate --image_size=thumbnail,medium --yes

# Regenerate images from specific year
wp media regenerate --yes $(wp post list --post_type=attachment --year=2024 --format=ids)

# Regenerate for specific attachment IDs
wp media regenerate 123 456 789 --yes

# Skip thumbnail generation (only register sizes)
wp media regenerate --skip-delete --yes

Automated thumbnail regeneration with progress tracking:

#!/bin/bash
# regenerate-thumbnails.sh - Regenerate with progress tracking

WP_PATH="/var/www/html"

# Get total attachment count
TOTAL=$(wp --path="$WP_PATH" post list --post_type=attachment --post_mime_type=image --format=count)
echo "Total images to process: $TOTAL"

# Process in batches
BATCH_SIZE=50
PROCESSED=0

while [ $PROCESSED -lt $TOTAL ]; do
    echo "Processing batch: $PROCESSED to $((PROCESSED + BATCH_SIZE))"

    # Get batch of attachment IDs
    IDS=$(wp --path="$WP_PATH" post list \
        --post_type=attachment \
        --post_mime_type=image \
        --posts_per_page=$BATCH_SIZE \
        --offset=$PROCESSED \
        --format=ids)

    if [ -n "$IDS" ]; then
        wp --path="$WP_PATH" media regenerate $IDS --yes
    fi

    PROCESSED=$((PROCESSED + BATCH_SIZE))
    echo "Progress: $PROCESSED / $TOTAL ($(( PROCESSED * 100 / TOTAL ))%)"

    # Sleep to avoid overloading server
    sleep 2
done

echo "Thumbnail regeneration completed"

Image Optimization

Optimize images to reduce file sizes and improve site performance.

# Install optimization plugin
wp plugin install ewww-image-optimizer --activate

# Configure optimization settings
wp option update ewww_image_optimizer_auto true
wp option update ewww_image_optimizer_jpg_level 30
wp option update ewww_image_optimizer_png_level 10

# Optimize all existing images
wp ewwwio optimize all

# Optimize specific images
wp media regenerate --yes --image_size=full

Alternative optimization with ImageMagick:

#!/bin/bash
# optimize-images.sh - Optimize images using ImageMagick

WP_PATH="/var/www/html"
UPLOADS_DIR="$WP_PATH/wp-content/uploads"
QUALITY=85

echo "Optimizing images in $UPLOADS_DIR"

# Find and optimize JPG files
find "$UPLOADS_DIR" -type f -iname "*.jpg" -o -iname "*.jpeg" | while read -r file; do
    echo "Optimizing: $file"

    # Create backup
    cp "$file" "$file.backup"

    # Optimize with ImageMagick
    convert "$file" -strip -interlace Plane -quality $QUALITY "$file"

    # Check if optimization saved space
    ORIGINAL_SIZE=$(stat -f%z "$file.backup" 2>/dev/null || stat -c%s "$file.backup")
    NEW_SIZE=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file")

    if [ "$NEW_SIZE" -lt "$ORIGINAL_SIZE" ]; then
        SAVED=$((ORIGINAL_SIZE - NEW_SIZE))
        echo "Saved: $SAVED bytes"
        rm "$file.backup"
    else
        # Restore if no improvement
        mv "$file.backup" "$file"
    fi
done

# Optimize PNG files
find "$UPLOADS_DIR" -type f -iname "*.png" | while read -r file; do
    echo "Optimizing: $file"
    optipng -o2 "$file"
done

echo "Image optimization completed"

Metadata Management

Update alt text, captions, titles, and descriptions programmatically.

# Update alt text for specific attachment
wp post meta update 123 _wp_attachment_image_alt "New alt text"

# Update attachment title
wp post update 123 --post_title="New Title"

# Update caption
wp post update 123 --post_excerpt="New caption"

# Update description
wp post update 123 --post_content="New description"

# Bulk update alt text based on filename
for id in $(wp post list --post_type=attachment --format=ids); do
    FILENAME=$(wp post get $id --field=post_title)
    # Generate alt text from filename
    ALT_TEXT=$(echo "$FILENAME" | sed 's/-/ /g' | sed 's/\.[^.]*$//')
    wp post meta update $id _wp_attachment_image_alt "$ALT_TEXT"
    echo "Updated alt text for ID $id: $ALT_TEXT"
done

Comprehensive metadata update script:

#!/bin/bash
# update-media-metadata.sh - Bulk metadata updates

WP_PATH="/var/www/html"
CSV_FILE="media-metadata.csv"

# CSV format: attachment_id,title,alt,caption,description

while IFS=',' read -r id title alt caption description; do
    # Skip header row
    if [ "$id" == "attachment_id" ]; then
        continue
    fi

    echo "Updating attachment ID: $id"

    # Update title
    if [ -n "$title" ]; then
        wp --path="$WP_PATH" post update "$id" --post_title="$title"
    fi

    # Update alt text
    if [ -n "$alt" ]; then
        wp --path="$WP_PATH" post meta update "$id" _wp_attachment_image_alt "$alt"
    fi

    # Update caption
    if [ -n "$caption" ]; then
        wp --path="$WP_PATH" post update "$id" --post_excerpt="$caption"
    fi

    # Update description
    if [ -n "$description" ]; then
        wp --path="$WP_PATH" post update "$id" --post_content="$description"
    fi

    echo "Metadata updated for attachment $id"
done < "$CSV_FILE"

echo "Bulk metadata update completed"

Media Library Cleanup

Remove unused, broken, or duplicate media files.

# Find unattached media (not assigned to any post)
wp post list --post_type=attachment --post_parent=0 --format=ids

# Delete unattached media
wp post delete $(wp post list --post_type=attachment --post_parent=0 --format=ids) --force

# Find and remove broken attachments (missing files)
for id in $(wp post list --post_type=attachment --format=ids); do
    FILE=$(wp post meta get $id _wp_attached_file)
    FULL_PATH="/var/www/html/wp-content/uploads/$FILE"

    if [ ! -f "$FULL_PATH" ]; then
        echo "Missing file for attachment $id: $FILE"
        wp post delete $id --force
    fi
done

# Find duplicate images by hash
find /var/www/html/wp-content/uploads -type f -exec md5sum {} + | \
    sort | \
    uniq -w32 -D

Automated cleanup script:

#!/bin/bash
# media-cleanup.sh - Clean up media library

set -euo pipefail

WP_PATH="/var/www/html"
DRY_RUN=false

if [ "${1:-}" == "--dry-run" ]; then
    DRY_RUN=true
    echo "Running in dry-run mode (no changes will be made)"
fi

echo "=== Media Library Cleanup ==="

# Find orphaned attachments (older than 30 days, never used)
echo "Finding orphaned attachments..."
ORPHANED_IDS=$(wp --path="$WP_PATH" post list \
    --post_type=attachment \
    --post_parent=0 \
    --post_status=inherit \
    --format=ids \
    --date_query='[{"before":"30 days ago"}]')

if [ -n "$ORPHANED_IDS" ]; then
    ORPHANED_COUNT=$(echo "$ORPHANED_IDS" | wc -w)
    echo "Found $ORPHANED_COUNT orphaned attachments"

    if [ "$DRY_RUN" == false ]; then
        echo "Deleting orphaned attachments..."
        wp --path="$WP_PATH" post delete $ORPHANED_IDS --force
    else
        echo "Would delete: $ORPHANED_IDS"
    fi
else
    echo "No orphaned attachments found"
fi

# Check for broken file links
echo "Checking for broken file links..."
UPLOADS_DIR="$WP_PATH/wp-content/uploads"
BROKEN_COUNT=0

for id in $(wp --path="$WP_PATH" post list --post_type=attachment --format=ids); do
    FILE=$(wp --path="$WP_PATH" post meta get $id _wp_attached_file)
    FULL_PATH="$UPLOADS_DIR/$FILE"

    if [ ! -f "$FULL_PATH" ]; then
        echo "Broken attachment $id: $FILE"
        ((BROKEN_COUNT++))

        if [ "$DRY_RUN" == false ]; then
            wp --path="$WP_PATH" post delete $id --force
        fi
    fi
done

echo "Found $BROKEN_COUNT broken attachments"

# Remove unused image sizes
echo "Cleaning up unused image sizes..."
find "$UPLOADS_DIR" -type f -regextype posix-extended \
    -regex '.*-[0-9]+x[0-9]+\.(jpg|jpeg|png|gif)$' \
    -mtime +90 | head -10

echo "Media cleanup completed"

Automate featured image assignment across posts.

# Set featured image for specific post
wp post meta set 123 _thumbnail_id 456

# Assign first attached image as featured image
for post_id in $(wp post list --post_type=post --format=ids); do
    # Get first attachment
    ATTACHMENT_ID=$(wp post list --post_type=attachment --post_parent=$post_id --posts_per_page=1 --format=ids)

    if [ -n "$ATTACHMENT_ID" ]; then
        wp post meta set $post_id _thumbnail_id $ATTACHMENT_ID
        echo "Set featured image for post $post_id"
    fi
done

# Download and set featured image from URL
URL="https://example.com/image.jpg"
POST_ID=123

ATTACHMENT_ID=$(wp media import "$URL" --post_id=$POST_ID --porcelain)
wp post meta set $POST_ID _thumbnail_id $ATTACHMENT_ID

File Organization and Migration

Reorganize uploads directory or migrate media between sites.

#!/bin/bash
# organize-uploads.sh - Organize uploads by year/month

WP_PATH="/var/www/html"
UPLOADS_DIR="$WP_PATH/wp-content/uploads"

# Enable year/month organization
wp --path="$WP_PATH" option update uploads_use_yearmonth_folders 1

# Migrate existing files to year/month structure
for id in $(wp --path="$WP_PATH" post list --post_type=attachment --format=ids); do
    # Get attachment date
    DATE=$(wp --path="$WP_PATH" post get $id --field=post_date)
    YEAR=$(date -d "$DATE" +%Y 2>/dev/null || echo "")
    MONTH=$(date -d "$DATE" +%m 2>/dev/null || echo "")

    if [ -n "$YEAR" ] && [ -n "$MONTH" ]; then
        # Get current file path
        FILE=$(wp --path="$WP_PATH" post meta get $id _wp_attached_file)

        # Skip if already organized
        if [[ "$FILE" =~ ^[0-9]{4}/[0-9]{2}/ ]]; then
            continue
        fi

        # Create target directory
        TARGET_DIR="$UPLOADS_DIR/$YEAR/$MONTH"
        mkdir -p "$TARGET_DIR"

        # Move file
        BASENAME=$(basename "$FILE")
        if [ -f "$UPLOADS_DIR/$FILE" ]; then
            mv "$UPLOADS_DIR/$FILE" "$TARGET_DIR/$BASENAME"

            # Update database
            NEW_PATH="$YEAR/$MONTH/$BASENAME"
            wp --path="$WP_PATH" post meta update $id _wp_attached_file "$NEW_PATH"

            echo "Moved: $FILE -> $NEW_PATH"
        fi
    fi
done

echo "Upload organization completed"

Media Export and Backup

Create backups of media library with metadata.

#!/bin/bash
# backup-media.sh - Backup media library

WP_PATH="/var/www/html"
BACKUP_DIR="/backups/media"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_PATH="$BACKUP_DIR/media-backup-$TIMESTAMP"

mkdir -p "$BACKUP_PATH"

echo "Starting media backup..."

# Export attachment metadata
wp --path="$WP_PATH" post list \
    --post_type=attachment \
    --format=json > "$BACKUP_PATH/attachments-metadata.json"

# Backup uploads directory
tar -czf "$BACKUP_PATH/uploads.tar.gz" \
    -C "$WP_PATH/wp-content" \
    uploads/

# Get backup size
BACKUP_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1)
echo "Backup completed: $BACKUP_SIZE at $BACKUP_PATH"

# Clean old backups (keep last 7)
ls -t "$BACKUP_DIR" | tail -n +8 | xargs -I {} rm -rf "$BACKUP_DIR/{}"

Leave a Reply

Your email address will not be published. Required fields are marked *