Add Media Usage Indicators in the WordPress Media Library – Code Snippet

Media libraries can easily grow very big. It becomes hard to know which images are used on the site and which are just taking up space. I have with AI made a PHP code snippet that adds usage indicators directly inside the WordPress Media Library, so one can instantly see which images are used. It becomes a simple way to reduce clutter and keep the site running smoothly. Here is an example of what it looks like.

The PHP code I added into a Code snippet plugin is this:

/**
 * Media Usage Indicator – Optimized, Fast, Safe Version + Pre-Scan
 *
 * How to add safely:
 * 1. Use a code snippet plugin (like Fluent Snippets) or a mini custom plugin.
 * 2. No PHP opening tag needed in Code Snippets.
 * 3. Activate in admin only; safe to deactivate anytime — all indicators and meta will stop and clean up.
 *
 * Features:
 *  - Small green badge ✅ for used media items (no layout shift)
 *  - Works with FileBird folders
 *  - Supports HEIC → JPG uploads
 *  - Detects images in posts, galleries, Gutenberg blocks, Media & Text blocks, featured images, and ACF/custom meta
 *  - Batches AJAX calls for speed and low resource usage
 *  - Manual pre-scan for existing media to show all badges immediately
 *  - Fully reversible on deactivation
 */

if ( ! defined('ABSPATH') ) exit;

/* === 1. Update _is_used meta when posts/pages are saved === */
add_action('save_post', function($post_id){
    if ( wp_is_post_revision($post_id) || wp_is_post_autosave($post_id) ) return;

    $content = get_post_field('post_content', $post_id);
    $used_ids = [];

    // Gutenberg image block: "id":123
    if (preg_match_all('/"id":(\d+)/', $content, $matches)) {
        $used_ids = array_merge($used_ids, $matches[1]);
    }

    // Gutenberg gallery block: "ids":[1,2,3]
    if (preg_match_all('/"ids":\[(.*?)\]/', $content, $matches)) {
        foreach ($matches[1] as $group) {
            $ids = array_map('intval', explode(',', $group));
            $used_ids = array_merge($used_ids, $ids);
        }
    }

    // Featured image
    $thumb_id = get_post_thumbnail_id($post_id);
    if ($thumb_id) $used_ids[] = $thumb_id;

    // Meta fields (ACF/custom)
    $meta_values = get_post_meta($post_id);
    foreach ($meta_values as $values) {
        foreach ($values as $val) {
            if (is_numeric($val)) $used_ids[] = intval($val);
        }
    }

    $used_ids = array_unique($used_ids);

    // Update _is_used meta for attachments
    foreach ($used_ids as $att_id) {
        update_post_meta($att_id, '_is_used', 1);
    }

    // Remove _is_used for attachments no longer used in this post
    $all_att_ids = get_attached_media('', $post_id);
    foreach ($all_att_ids as $att) {
        if (!in_array($att->ID, $used_ids)) {
            delete_post_meta($att->ID, '_is_used');
        }
    }

}, 20);

/* === 2. Front-end admin indicators (Media Library + FileBird) === */
add_action('admin_print_footer_scripts', function() {
    ?>
    <style>
    /* Small green checkmark badge */
    .usage-indicator-badge {
        position: absolute;
        top: 4px;
        right: 4px;
        width: 18px;
        height: 18px;
        font-size: 12px;
        line-height: 18px;
        color: #fff;
        background-color: #008000;
        text-align: center;
        font-weight: bold;
        border-radius: 3px;
        z-index: 10;
        pointer-events: none;
    }
    </style>

    <script>
    jQuery(document).ready(function($){
        const ajaxNonce = '<?php echo wp_create_nonce("check_is_used_nonce"); ?>';
        const batchSize = 10;

        function addIndicators(){
            const $items = $('.attachments .attachment, .fbv-media-item, .fb-media-item').filter(function(){
                return $(this).find('img').length > 0;
            }).not('.usage-checked');

            if ($items.length === 0) return;

            let itemsArray = $items.toArray();
            for (let i=0; i<itemsArray.length; i+=batchSize){
                let batch = itemsArray.slice(i, i+batchSize);
                batch.forEach(function(el){
                    const $el = $(el);
                    $el.addClass('usage-checked'); // mark as checked
                    const id = $el.data('id');
                    if (!id) return;

                    $.post(ajaxurl, {
                        action: 'check_is_used',
                        attachment_id: id,
                        nonce: ajaxNonce
                    }, function(response){
                        if (!response || !response.success) return;
                        if (response.data.used){
                            if ($el.find('.usage-indicator-badge').length === 0){
                                $el.css('position','relative');
                                $el.append('<div class="usage-indicator-badge">✓</div>');
                            }
                        }
                    });
                });
            }
        }

        setTimeout(addIndicators, 400);

        // Recheck when DOM changes (FileBird folder switch or lazy load)
        let pending = false;
        const observer = new MutationObserver(function(){
            if (pending) return;
            pending = true;
            setTimeout(function(){
                addIndicators();
                pending = false;
            }, 300);
        });
        observer.observe(document.body, { childList: true, subtree: true });

        // Folder click detection (FileBird)
        $(document).on('click', '.fbv-folder, .fb-folder', function(){
            setTimeout(addIndicators, 300);
        });
    });
    </script>
    <?php
});

/* === 3. AJAX handler reads _is_used meta === */
add_action('wp_ajax_check_is_used', function(){
    check_ajax_referer('check_is_used_nonce','nonce');
    $attachment_id = intval($_POST['attachment_id'] ?? 0);
    $used = get_post_meta($attachment_id, '_is_used', true) ? true : false;
    wp_send_json_success( ['used' => $used] );
});

/* === 4. Manual pre-scan for all existing media === */
add_action('admin_init', function() {
    if (!current_user_can('manage_options')) return;

    // Trigger via URL: ?run_media_scan=1
    if (isset($_GET['run_media_scan']) && $_GET['run_media_scan'] == '1') {

        $args = [
            'post_type'      => 'attachment',
            'post_status'    => 'inherit',
            'posts_per_page' => 100,
            'fields'         => 'ids',
        ];

        $offset = 0;
        do {
            $args['offset'] = $offset;
            $attachments = get_posts($args);

            foreach ($attachments as $att_id) {
                $used = false;

                global $wpdb;
                $url = wp_get_attachment_url($att_id);
                if (!$url) continue;

                // Post content check
                $in_content = $wpdb->get_var($wpdb->prepare(
                    "SELECT ID FROM $wpdb->posts WHERE post_content LIKE %s LIMIT 1",
                    '%' . $wpdb->esc_like($url) . '%'
                ));
                if ($in_content) $used = true;

                // Featured image check
                if (!$used) {
                    $featured = $wpdb->get_var($wpdb->prepare(
                        "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_thumbnail_id' AND meta_value=%d LIMIT 1",
                        $att_id
                    ));
                    if ($featured) $used = true;
                }

                // Meta fields check
                if (!$used) {
                    $in_meta = $wpdb->get_var($wpdb->prepare(
                        "SELECT post_id FROM $wpdb->postmeta WHERE meta_value=%d OR meta_value LIKE %s LIMIT 1",
                        $att_id,
                        '%' . $wpdb->esc_like($url) . '%'
                    ));
                    if ($in_meta) $used = true;
                }

                if ($used) {
                    update_post_meta($att_id, '_is_used', 1);
                } else {
                    delete_post_meta($att_id, '_is_used');
                }
            }

            $offset += count($attachments);

        } while (count($attachments) > 0);

        echo '<div style="padding:20px; background:#dff0d8; color:#3c763d;">Media pre-scan complete!</div>';
        exit;
    }
});

/* === 5. Cleanup on deactivation === */
register_deactivation_hook(__FILE__, function(){
    global $wpdb;
    $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key='_is_used'");
});

NB!
I would suggest activating the code snippet. Then checking the Media Library for used images. Then cleaning up the Media Library. When done deactivating the snippet. Just have what is needed active so when it is not in use deactivate it.

Share the article:

Leave a Reply

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