fix: postmeta tracking on synchronous AVIF + add reconcile-meta (v0.2.2)

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 <noreply@anthropic.com>
This commit is contained in:
Henk
2026-05-19 14:59:18 +10:00
parent 868dbe0ff4
commit e5ce0d2585
4 changed files with 112 additions and 6 deletions

View File

@@ -3,7 +3,7 @@
* Plugin Name: H4B Image Optim * Plugin Name: H4B Image Optim
* Plugin URI: https://gitea.help4bis.com/help4bis/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. * 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: help4bis (Henk + Claude)
* Author URI: https://help4bis.com * Author URI: https://help4bis.com
* License: GPL-2.0-or-later * License: GPL-2.0-or-later
@@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; 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_FILE', __FILE__ );
define( 'H4B_IMG_OPTIM_DIR', plugin_dir_path( __FILE__ ) ); define( 'H4B_IMG_OPTIM_DIR', plugin_dir_path( __FILE__ ) );
define( 'H4B_IMG_OPTIM_URL', plugin_dir_url( __FILE__ ) ); define( 'H4B_IMG_OPTIM_URL', plugin_dir_url( __FILE__ ) );

View File

@@ -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. * Build the set of all JPG/PNG paths registered in wp_attachment_metadata.
* Returns rel-path => true. * Returns rel-path => true.

View File

@@ -19,6 +19,10 @@ final class CLI {
\WP_CLI::add_command( 'h4b-img rescue', CLI_Rescue::class ); \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 migrate-from-smush', CLI_Migrate::class );
\WP_CLI::add_command( 'h4b-img generate-missing-siblings', CLI_Siblings::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 );
} );
} }
/** /**

View File

@@ -115,11 +115,15 @@ final class Format_Generator {
return $res; 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 { public static function process_avif_job( string $source ): void {
if ( ! is_readable( $source ) ) { if ( ! is_readable( $source ) ) {
@@ -127,8 +131,8 @@ final class Format_Generator {
return; return;
} }
$settings = Settings::all(); $settings = Settings::all();
$result = self::encode_avif_now( $source, $settings ); $settings['avif_async'] = false; // we ARE the queue handler — never re-queue
self::record_avif_outcome( $source, $result ); self::make_avif( $source, $settings );
} }
/** /**