From c54eccc2d59099f8e164998692e47498447374cc Mon Sep 17 00:00:00 2001 From: Henk Date: Tue, 19 May 2026 14:59:18 +1000 Subject: [PATCH] fix: postmeta tracking on synchronous AVIF + add reconcile-meta (v0.2.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two improvements after v0.2.1 deploy revealed the avif_status bug wasn't fully fixed: Fix: Format_Generator::make_avif() now calls record_avif_outcome() at the end of the synchronous path. Previously only the cron path recorded outcomes, so wp h4b-img generate-missing-siblings (synchronous) left 4067 stale 'queued' rows even though it successfully generated 603 AVIFs on disk. process_avif_job() simplified to a thin wrapper around make_avif(avif_async=false). Added: wp h4b-img reconcile-meta — walks _h4b_img_optim postmeta, checks for .webp / .avif files on disk, and updates avif_status / webp size fields to match reality. One-shot reconciliation for stale records left by earlier plugin versions. --dry-run supported. Co-Authored-By: Claude Opus 4.7 --- h4b-image-optim.php | 4 +- includes/class-cli-siblings.php | 98 +++++++++++++++++++++++++++++ includes/class-cli.php | 4 ++ includes/class-format-generator.php | 12 ++-- 4 files changed, 112 insertions(+), 6 deletions(-) diff --git a/h4b-image-optim.php b/h4b-image-optim.php index 6e8c32a..f4109ab 100644 --- a/h4b-image-optim.php +++ b/h4b-image-optim.php @@ -3,7 +3,7 @@ * Plugin Name: H4B Image Optim * Plugin URI: https://gitea.help4bis.com/help4bis/h4b-image-optim * Description: ICC-safe image optimisation with WebP + AVIF generation. Replaces Smush Pro without the grey-wash bug. No CDN. - * Version: 0.2.1 + * Version: 0.2.2 * Author: help4bis (Henk + Claude) * Author URI: https://help4bis.com * License: GPL-2.0-or-later @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'H4B_IMG_OPTIM_VERSION', '0.2.1' ); +define( 'H4B_IMG_OPTIM_VERSION', '0.2.2' ); define( 'H4B_IMG_OPTIM_FILE', __FILE__ ); define( 'H4B_IMG_OPTIM_DIR', plugin_dir_path( __FILE__ ) ); define( 'H4B_IMG_OPTIM_URL', plugin_dir_url( __FILE__ ) ); diff --git a/includes/class-cli-siblings.php b/includes/class-cli-siblings.php index e169579..d434c4c 100644 --- a/includes/class-cli-siblings.php +++ b/includes/class-cli-siblings.php @@ -143,6 +143,104 @@ final class CLI_Siblings { ) ); } + /** + * Reconcile attachment postmeta with what's actually on disk. + * + * For each `_h4b_img_optim` size entry, if avif_status is 'queued' or 'never_generated' + * but the .avif file exists on disk, update to 'done' with the actual byte size. + * Similarly for webp. + * + * Use this after manual generate-missing-siblings runs OR to fix the stale + * 'queued' values from a v0.2.0 era bulk run. + * + * ## OPTIONS + * + * [--dry-run] + * : Report counts only. + * + * ## EXAMPLES + * wp h4b-img reconcile-meta --dry-run + * wp h4b-img reconcile-meta + */ + public function reconcile_meta( $args, $assoc ): void { + $dry_run = ! empty( $assoc['dry-run'] ); + global $wpdb; + $basedir = trailingslashit( wp_get_upload_dir()['basedir'] ); + + $rows = $wpdb->get_results( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key='_h4b_img_optim'" ); + \WP_CLI::log( sprintf( 'Scanning %d attachments…', count( $rows ) ) ); + + $updates = [ + 'avif_done_marked' => 0, + 'avif_already_done' => 0, + 'avif_still_missing' => 0, + 'webp_done_marked' => 0, + ]; + + foreach ( $rows as $r ) { + $meta = @unserialize( $r->meta_value, [ 'allowed_classes' => false ] ); + if ( ! is_array( $meta ) || empty( $meta['sizes'] ) ) { + continue; + } + $rel_parent = get_post_meta( $r->post_id, '_wp_attached_file', true ); + if ( ! $rel_parent ) { + continue; + } + $parent_dir = dirname( $rel_parent ); + $attached_meta = wp_get_attachment_metadata( $r->post_id ); + $changed = false; + foreach ( $meta['sizes'] as $size_key => &$entry ) { + // Resolve the file path for this size key + if ( $size_key === 'full' ) { + $file_rel = $rel_parent; + } elseif ( is_array( $attached_meta ) && ! empty( $attached_meta['sizes'][ $size_key ]['file'] ) ) { + $f = $attached_meta['sizes'][ $size_key ]['file']; + $file_rel = ( $parent_dir === '.' ) ? $f : "$parent_dir/$f"; + } else { + continue; + } + $src = $basedir . $file_rel; + $avif = $src . '.avif'; + $webp = $src . '.webp'; + + // AVIF + if ( is_readable( $avif ) ) { + if ( ( $entry['avif_status'] ?? '' ) !== 'done' ) { + $entry['avif'] = filesize( $avif ); + $entry['avif_status'] = 'done'; + $updates['avif_done_marked']++; + $changed = true; + } else { + $updates['avif_already_done']++; + } + } elseif ( ( $entry['avif_status'] ?? '' ) === 'queued' ) { + $updates['avif_still_missing']++; + } + + // WebP — only mark if currently null/missing + if ( is_readable( $webp ) && empty( $entry['webp'] ) ) { + $entry['webp'] = filesize( $webp ); + $updates['webp_done_marked']++; + $changed = true; + } + } + unset( $entry ); + + if ( $changed && ! $dry_run ) { + Attachment_Meta::set( (int) $r->post_id, $meta ); + } + } + + \WP_CLI::success( sprintf( + "Reconciliation %s.\n avif_status 'queued' → 'done' on disk: %d\n avif already correctly 'done': %d\n avif still missing (queue stale): %d\n webp filesize backfilled in meta: %d", + $dry_run ? 'dry-run' : 'done', + $updates['avif_done_marked'], + $updates['avif_already_done'], + $updates['avif_still_missing'], + $updates['webp_done_marked'] + ) ); + } + /** * Build the set of all JPG/PNG paths registered in wp_attachment_metadata. * Returns rel-path => true. diff --git a/includes/class-cli.php b/includes/class-cli.php index 1061bcc..b9ab824 100644 --- a/includes/class-cli.php +++ b/includes/class-cli.php @@ -19,6 +19,10 @@ final class CLI { \WP_CLI::add_command( 'h4b-img rescue', CLI_Rescue::class ); \WP_CLI::add_command( 'h4b-img migrate-from-smush', CLI_Migrate::class ); \WP_CLI::add_command( 'h4b-img generate-missing-siblings', CLI_Siblings::class ); + \WP_CLI::add_command( 'h4b-img reconcile-meta', + function ( $args, $assoc ) { + ( new CLI_Siblings() )->reconcile_meta( $args, $assoc ); + } ); } /** diff --git a/includes/class-format-generator.php b/includes/class-format-generator.php index 2ba993e..47ac75e 100644 --- a/includes/class-format-generator.php +++ b/includes/class-format-generator.php @@ -115,11 +115,15 @@ final class Format_Generator { return $res; } - return self::encode_avif_now( $source, $settings ); + // Synchronous path: encode + record outcome in attachment postmeta. + $result = self::encode_avif_now( $source, $settings ); + self::record_avif_outcome( $source, $result ); + return $result; } /** - * Cron entry point — encodes one queued path and updates per-attachment meta. + * Cron entry point — encodes one queued path. Recording happens inside + * make_avif() now, so this is a thin wrapper that forces synchronous mode. */ public static function process_avif_job( string $source ): void { if ( ! is_readable( $source ) ) { @@ -127,8 +131,8 @@ final class Format_Generator { return; } $settings = Settings::all(); - $result = self::encode_avif_now( $source, $settings ); - self::record_avif_outcome( $source, $result ); + $settings['avif_async'] = false; // we ARE the queue handler — never re-queue + self::make_avif( $source, $settings ); } /**