PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.44
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.44
51.1.86 51.1.84 51.1.85 51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 All 40 releases
king-addons / includes / extensions / alt-text-generator / Alt_Text_Generator.php

Alt_Text_Generator.php in King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder 51.1.44, at includes/extensions/alt-text-generator/Alt_Text_Generator.php

609 lines 25.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles automatic alt text generation for images using AI.
4 *
5 * @package King_Addons
6 */
7
8 namespace King_Addons;
9
10 if (!defined('ABSPATH')) {
11 exit; // Exit if accessed directly.
12 }
13
14 /**
15 * Class Alt_Text_Generator
16 */
17 class Alt_Text_Generator {
18
19 // Constants for batch processing
20 private const BATCH_OPTION_PENDING = 'king_addons_alt_text_pending_queue';
21 private const BATCH_OPTION_PROCESSING = 'king_addons_alt_text_processing';
22 private const CRON_HOOK = 'king_addons_process_alt_text_queue';
23 private const BATCH_SIZE = 1; // Process 1 image at a time to respect rate limits
24 private const RETRY_LIMIT = 3; // Maximum retry attempts
25 private const RATE_LIMIT_DELAY = 5; // Delay between batches in seconds
26
27 /**
28 * Constructor.
29 * Hooks into WordPress actions.
30 */
31 public function __construct() {
32 // Check if AI features are enabled
33 $options = get_option('king_addons_ai_options', []);
34
35 // Check if any alt text feature is enabled (with proper defaults)
36 $button_enabled = isset($options['enable_ai_alt_text_button']) ? (bool) $options['enable_ai_alt_text_button'] : true; // Default to true
37 $auto_enabled = isset($options['enable_ai_alt_text_auto_generation']) ? (bool) $options['enable_ai_alt_text_auto_generation'] : false; // Default to false
38
39 if (!$button_enabled && !$auto_enabled) {
40 return; // Don't initialize if both features are disabled
41 }
42
43 $has_api_key = !empty($options['openai_api_key']);
44
45 // Hook for new attachments (only if auto generation is enabled and API key exists).
46 if ($auto_enabled && $has_api_key) {
47 if(king_addons_freemius()->can_use_premium_code()){
48 add_action('add_attachment', array($this, 'add_to_processing_queue'));
49 add_action(self::CRON_HOOK, array($this, 'process_alt_text_queue'));
50
51 // Add custom cron interval
52 add_filter('cron_schedules', array($this, 'add_custom_cron_intervals'));
53
54 // Schedule recurring cron job if not already scheduled
55 if (!wp_next_scheduled(self::CRON_HOOK)) {
56 wp_schedule_event(time(), 'king_addons_alt_text_interval', self::CRON_HOOK);
57 }
58 }
59 }
60
61 // Hooks for Media Library integration (always enable if button is enabled, regardless of API key).
62 if ($button_enabled) {
63 add_filter('manage_media_columns', array($this, 'add_alt_text_column'));
64 add_action('manage_media_custom_column', array($this, 'display_alt_text_column'), 10, 2);
65 add_action('admin_enqueue_scripts', array($this, 'enqueue_media_scripts'));
66
67 // AJAX handler for manual generation (only if API key exists).
68 if ($has_api_key) {
69 add_action('wp_ajax_king_addons_generate_single_alt', array($this, 'handle_ajax_generate_single_alt'));
70 }
71
72 // AJAX handler for queue status (debugging)
73 add_action('wp_ajax_king_addons_alt_text_queue_status', array($this, 'handle_ajax_queue_status'));
74 }
75
76 // Hook for cleanup on plugin deactivation
77 register_deactivation_hook(KING_ADDONS_PATH . 'king-addons.php', array($this, 'cleanup_cron_jobs'));
78
79 // Hook to update cron schedule when settings change
80 add_action('update_option_king_addons_ai_options', array($this, 'update_cron_schedule'), 10, 3);
81 }
82
83 /**
84 * Enqueues scripts needed for the media library screen.
85 *
86 * @param string $hook The current admin page hook.
87 */
88 public function enqueue_media_scripts(string $hook): void {
89 // Load on the upload.php screen (Media Library List view) and post editing screens (for media modal)
90 if (!in_array($hook, ['upload.php', 'post.php', 'post-new.php', 'page.php', 'page-new.php'])) {
91 return;
92 }
93
94 wp_enqueue_style(
95 'king-addons-media-alt-text-styles',
96 KING_ADDONS_URL . 'includes/extensions/alt-text-generator/alt-text-styles.css',
97 array(),
98 KING_ADDONS_VERSION
99 );
100
101 wp_enqueue_script(
102 'king-addons-media-alt-text',
103 KING_ADDONS_URL . 'includes/extensions/alt-text-generator/alt-text-media.js',
104 array('jquery'),
105 KING_ADDONS_VERSION,
106 true
107 );
108
109 // Check if API key exists
110 $options = get_option('king_addons_ai_options', []);
111 $has_api_key = !empty($options['openai_api_key']);
112
113 // Pass data to JavaScript.
114 wp_localize_script('king-addons-media-alt-text', 'kingAddonsMediaAltText', array(
115 'ajax_url' => admin_url('admin-ajax.php'),
116 'nonce' => wp_create_nonce('king_addons_generate_alt_nonce'),
117 'generating_text' => esc_html__('Generate with AI', 'king-addons'),
118 'error_text' => esc_html__('Error', 'king-addons'),
119 'has_api_key' => $has_api_key,
120 'settings_url' => admin_url('admin.php?page=king-addons-ai-settings'),
121 ));
122 }
123
124 /**
125 * Adds a custom column to the Media Library list view.
126 *
127 * @param array $columns Existing columns.
128 * @return array Modified columns.
129 */
130 public function add_alt_text_column(array $columns): array {
131 // Add column before 'Date'.
132 $new_columns = array();
133 foreach ($columns as $key => $title) {
134 if ('date' === $key) {
135 $new_columns['king_addons_alt_text'] = esc_html__('AI Alt Text', 'king-addons');
136 }
137 $new_columns[$key] = $title;
138 }
139 // If 'date' column wasn't found, add it at the end.
140 if (!isset($new_columns['king_addons_alt_text'])) {
141 $new_columns['king_addons_alt_text'] = esc_html__('AI Alt Text', 'king-addons');
142 }
143 return $new_columns;
144 }
145
146 /**
147 * Displays content for the custom alt text column.
148 *
149 * @param string $column_name The name of the column being displayed.
150 * @param int $attachment_id The ID of the current attachment.
151 */
152 public function display_alt_text_column(string $column_name, int $attachment_id): void {
153 if ('king_addons_alt_text' !== $column_name) {
154 return;
155 }
156
157 // Only show for images.
158 if (!wp_attachment_is_image($attachment_id)) {
159 echo '—'; // Not an image
160 return;
161 }
162
163 $alt_text = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
164 $options = get_option('king_addons_ai_options', []);
165 $has_api_key = !empty($options['openai_api_key']);
166
167 echo '<div class="king-addons-alt-text-status" data-attachment-id="' . esc_attr($attachment_id) . '">';
168 if (!empty($alt_text)) {
169 echo '<span>' . esc_html($alt_text) . '</span>';
170 } else {
171 if ($has_api_key) {
172 // Button to generate alt text when API key exists.
173 printf(
174 '<button type="button" class="button button-secondary button-small king-addons-generate-alt-button">%s</button>',
175 esc_html__('Generate', 'king-addons')
176 );
177 echo '<span class="king-addons-alt-text-result king-addons-status-inline"></span>'; // For displaying results/errors
178 echo '<span class="spinner king-addons-spinner-inline"></span>';
179 } else {
180 // Link to settings when API key is missing.
181 printf(
182 '<a href="%s" class="button button-secondary button-small" target="_blank">%s</a>',
183 esc_url(admin_url('admin.php?page=king-addons-ai-settings')),
184 esc_html__('Set API Key', 'king-addons')
185 );
186 echo '<span class="king-addons-alt-text-result king-addons-status-inline"></span>';
187 }
188 }
189 echo '</div>';
190 }
191
192 /**
193 * Handles the AJAX request to generate alt text for a single image.
194 */
195 public function handle_ajax_generate_single_alt(): void {
196 check_ajax_referer('king_addons_generate_alt_nonce', 'nonce');
197
198 if (!current_user_can('upload_files')) {
199 wp_send_json_error(array('message' => esc_html__('Permission denied.', 'king-addons')), 403);
200 }
201
202 // Check if API key exists
203 $options = get_option('king_addons_ai_options', []);
204 if (empty($options['openai_api_key'])) {
205 wp_send_json_error(array(
206 'message' => esc_html__('OpenAI API key is not configured. Please set it in the AI settings.', 'king-addons'),
207 'needs_setup' => true
208 ), 400);
209 }
210
211 $attachment_id = isset($_POST['attachment_id']) ? intval($_POST['attachment_id']) : 0;
212
213 if (!$attachment_id || !wp_attachment_is_image($attachment_id)) {
214 wp_send_json_error(array('message' => esc_html__('Invalid attachment ID.', 'king-addons')), 400);
215 }
216
217 // Call the existing generation logic, but slightly refactored to return the result.
218 $result = $this->generate_alt_text_for_image($attachment_id, true); // Pass true to indicate AJAX context.
219
220 if (is_wp_error($result)) {
221 wp_send_json_error(array('message' => $result->get_error_message()), 500);
222 } elseif ($result === false) {
223 // Handle cases where generation failed silently within the function (e.g., API key missing)
224 wp_send_json_error(array('message' => esc_html__('Alt text generation failed. Check logs or API key.', 'king-addons')), 500);
225 } elseif (is_string($result)) {
226 // Success! Return the generated alt text.
227 wp_send_json_success(array('alt_text' => $result));
228 } else {
229 // Unexpected result.
230 wp_send_json_error(array('message' => esc_html__('An unexpected error occurred.', 'king-addons')), 500);
231 }
232 }
233
234 /**
235 * Handles the AJAX request to get queue status.
236 */
237 public function handle_ajax_queue_status(): void {
238 check_ajax_referer('king_addons_generate_alt_nonce', 'nonce');
239
240 if (!current_user_can('manage_options')) {
241 wp_send_json_error(array('message' => esc_html__('Permission denied.', 'king-addons')), 403);
242 }
243
244 $status = $this->get_queue_status();
245 wp_send_json_success($status);
246 }
247
248 /**
249 * Adds custom cron intervals.
250 *
251 * Note: Recommended interval is 60+ seconds to avoid OpenAI API rate limits.
252 * Lower intervals may cause API errors during high usage periods.
253 *
254 * @param array $schedules Existing cron schedules.
255 * @return array Modified schedules.
256 */
257 public function add_custom_cron_intervals(array $schedules): array {
258 // Get the interval from settings, default to 60 seconds
259 $options = get_option('king_addons_ai_options', []);
260 $interval = isset($options['ai_alt_text_generation_interval']) ? (int) $options['ai_alt_text_generation_interval'] : 60;
261
262 // Ensure interval is within acceptable range
263 $interval = max(10, min(3600, $interval));
264
265 $schedules['king_addons_alt_text_interval'] = array(
266 'interval' => $interval,
267 'display' => sprintf(esc_html__('Every %d Seconds (King Addons Alt Text)', 'king-addons'), $interval)
268 );
269 return $schedules;
270 }
271
272 /**
273 * Adds a new attachment to the processing queue instead of immediate processing.
274 *
275 * @param int $attachment_id The ID of the attachment just added.
276 */
277 public function add_to_processing_queue(int $attachment_id): void {
278 // Check if the attachment is an image.
279 if (!wp_attachment_is_image($attachment_id)) {
280 return;
281 }
282
283 // Check if alt text already exists
284 $existing_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
285 if (!empty($existing_alt)) {
286 return; // Skip if alt text already exists
287 }
288
289 // Get current queue
290 $queue = get_option(self::BATCH_OPTION_PENDING, []);
291
292 // Add to queue if not already present
293 if (!in_array($attachment_id, $queue)) {
294 $queue[] = $attachment_id;
295 update_option(self::BATCH_OPTION_PENDING, $queue, false);
296 }
297 }
298
299 /**
300 * Processes the alt text generation queue in batches.
301 */
302 public function process_alt_text_queue(): void {
303 // Check if already processing to avoid conflicts
304 $processing = get_option(self::BATCH_OPTION_PROCESSING, false);
305 if ($processing && (time() - $processing) < 300) { // 5 minute timeout
306 return; // Another process is already running
307 }
308
309 // Set processing flag
310 update_option(self::BATCH_OPTION_PROCESSING, time(), false);
311
312 try {
313 // Get pending queue
314 $queue = get_option(self::BATCH_OPTION_PENDING, []);
315
316 if (empty($queue)) {
317 delete_option(self::BATCH_OPTION_PROCESSING);
318 return; // Nothing to process
319 }
320
321 // Process batch
322 $batch = array_splice($queue, 0, self::BATCH_SIZE);
323
324 foreach ($batch as $attachment_id) {
325 $this->process_single_attachment_with_retry($attachment_id);
326
327 // Add delay between requests to respect rate limits
328 if (count($batch) > 1) {
329 sleep(2); // 2 second delay between individual requests
330 }
331 }
332
333 // Update queue
334 update_option(self::BATCH_OPTION_PENDING, $queue, false);
335
336 // Schedule next batch if queue is not empty
337 if (!empty($queue)) {
338 wp_schedule_single_event(time() + self::RATE_LIMIT_DELAY, self::CRON_HOOK);
339 }
340
341 } finally {
342 // Always clear processing flag
343 delete_option(self::BATCH_OPTION_PROCESSING);
344 }
345 }
346
347 /**
348 * Processes a single attachment with retry logic.
349 *
350 * @param int $attachment_id The attachment ID to process.
351 */
352 private function process_single_attachment_with_retry(int $attachment_id): void {
353 $retry_count = get_post_meta($attachment_id, '_king_addons_alt_retry_count', true);
354 $retry_count = $retry_count ? (int) $retry_count : 0;
355
356 if ($retry_count >= self::RETRY_LIMIT) {
357 // Max retries reached, skip this attachment
358 delete_post_meta($attachment_id, '_king_addons_alt_retry_count');
359 return;
360 }
361
362 $result = $this->generate_alt_text_for_image($attachment_id, false);
363
364 if ($result !== true) {
365 // Generation failed, increment retry count
366 update_post_meta($attachment_id, '_king_addons_alt_retry_count', $retry_count + 1);
367
368 // Add back to queue for retry (at the end)
369 $queue = get_option(self::BATCH_OPTION_PENDING, []);
370 if (!in_array($attachment_id, $queue)) {
371 $queue[] = $attachment_id;
372 update_option(self::BATCH_OPTION_PENDING, $queue, false);
373 }
374 } else {
375 // Success, clean up retry count
376 delete_post_meta($attachment_id, '_king_addons_alt_retry_count');
377 }
378 }
379
380 /**
381 * Generates alt text for a given image attachment using an AI service.
382 *
383 * @param int $attachment_id The ID of the attachment.
384 * @param bool $is_ajax Optional. Indicates if called via AJAX context. If true, returns result/error instead of void.
385 * @return bool|string|WP_Error Returns true on success (non-AJAX), generated alt text (string) on success (AJAX), specific error message (string) on failure (non-AJAX), or WP_Error on failure (AJAX).
386 */
387 public function generate_alt_text_for_image(int $attachment_id, bool $is_ajax = false) {
388 // Verify it's an image.
389 if (!wp_attachment_is_image($attachment_id)) {
390 $error_msg = esc_html__('Not an image.', 'king-addons');
391 return $is_ajax ? new \WP_Error('invalid_attachment', $error_msg) : $error_msg;
392 }
393
394 // Check if alt text already exists (only if not forced, future enhancement).
395 $existing_alt = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
396 if (!empty($existing_alt)) {
397 // If called via AJAX, maybe return existing text or an indication it wasn't generated.
398 // For now, we assume the button won't be shown if alt exists, so this path is mainly for the cron job.
399 return $is_ajax ? $existing_alt : true;
400 }
401
402 // Retrieve OpenAI API key and settings from King Addons AI options.
403 $options = get_option('king_addons_ai_options', []);
404 $api_key = $options['openai_api_key'] ?? '';
405 // Use the text model for vision analysis, not the image generation model
406 $model = $options['openai_model'] ?? 'gpt-4o';
407 // Get image detail level from settings (default to 'low')
408 $image_detail_level = $options['ai_alt_text_image_detail_level'] ?? 'low';
409
410 if (empty($api_key)) {
411 $error_msg = esc_html__('OpenAI API key is missing.', 'king-addons');
412 return $is_ajax ? new \WP_Error('missing_api_key', $error_msg) : $error_msg;
413 }
414
415 // Get image path instead of URL.
416 $image_path = get_attached_file($attachment_id);
417 if (!$image_path || !file_exists($image_path)) {
418 $error_msg = esc_html__('Could not retrieve image file path.', 'king-addons');
419 return $is_ajax ? new \WP_Error('no_image_path', $error_msg) : $error_msg;
420 }
421
422 // Read image data.
423 $image_data = file_get_contents($image_path);
424 if (false === $image_data) {
425 $error_msg = esc_html__('Could not read image file.', 'king-addons');
426 return $is_ajax ? new \WP_Error('read_image_failed', $error_msg) : $error_msg;
427 }
428
429 // Get MIME type.
430 $file_info = wp_check_filetype(basename($image_path));
431 if (!$file_info || empty($file_info['type'])) {
432 $error_msg = esc_html__('Could not determine image type.', 'king-addons');
433 return $is_ajax ? new \WP_Error('mime_type_failed', $error_msg) : $error_msg;
434 }
435 $mime_type = $file_info['type'];
436
437 // Encode image data in Base64.
438 $base64_image = base64_encode($image_data);
439
440 // Create the data URI.
441 $image_data_uri = "data:{$mime_type};base64,{$base64_image}";
442
443 // --- OpenAI API Call --- //
444 $api_endpoint = 'https://api.openai.com/v1/chat/completions';
445
446 $prompt_text = 'Generate a concise, descriptive alt text for this image, suitable for SEO and accessibility. Focus on the main subject and action. Maximum 125 characters.';
447
448 $payload = array(
449 'model' => $model,
450 'messages' => array(
451 array(
452 'role' => 'user',
453 'content' => array(
454 array(
455 'type' => 'text',
456 'text' => $prompt_text
457 ),
458 array(
459 'type' => 'image_url',
460 'image_url' => array(
461 'url' => $image_data_uri,
462 'detail' => $image_detail_level
463 )
464 )
465 )
466 )
467 ),
468 'max_tokens' => 50 // Limit the response length
469 );
470
471 $args = array(
472 'headers' => array(
473 'Authorization' => 'Bearer ' . $api_key,
474 'Content-Type' => 'application/json',
475 ),
476 'body' => wp_json_encode($payload),
477 'timeout' => 60, // Increased timeout
478 'method' => 'POST',
479 'data_format' => 'body',
480 );
481
482 $response = wp_remote_post($api_endpoint, $args);
483
484 // Handle the response.
485 if (is_wp_error($response)) {
486 $error_message = $response->get_error_message();
487 /* translators: %s: Error message returned from the network request. */
488 return $is_ajax ? $response : sprintf(esc_html__('Network error: %s', 'king-addons'), $error_message);
489 }
490
491 $response_code = wp_remote_retrieve_response_code($response);
492 $response_body = wp_remote_retrieve_body($response);
493 $decoded_body = json_decode($response_body, true);
494
495 if ($response_code !== 200 || !isset($decoded_body['choices'][0]['message']['content'])) {
496 $api_error_message = isset($decoded_body['error']['message']) ? $decoded_body['error']['message'] : esc_html__('API request failed or returned unexpected data.', 'king-addons');
497 /* translators: 1: HTTP response code, 2: API error message. */
498 return $is_ajax ? new \WP_Error('api_error', $api_error_message, array('status' => $response_code)) : sprintf(esc_html__('API error (%1$d): %2$s', 'king-addons'), $response_code, $api_error_message);
499 }
500
501 $generated_alt_text = $decoded_body['choices'][0]['message']['content'];
502 // --- End API Call --- //
503
504 // If the response starts with "I'm sorry" (case-insensitive, allow whitespace before), treat as error and do not save
505 if (preg_match('/^\s*I\'m sorry/i', $generated_alt_text)) {
506 $generated_alt_text = '';
507 }
508
509 // Sanitize and potentially trim the generated alt text.
510 $sanitized_alt_text = sanitize_text_field(trim($generated_alt_text));
511 $sanitized_alt_text = trim($sanitized_alt_text, '"'); // Remove surrounding quotes
512 $sanitized_alt_text = mb_substr($sanitized_alt_text, 0, 125); // Enforce length limit
513
514 // Update the image alt text meta data.
515 if (!empty($sanitized_alt_text)) {
516 if (update_post_meta($attachment_id, '_wp_attachment_image_alt', $sanitized_alt_text)) {
517 // Log success for debugging
518 error_log("King Addons Alt Text: Successfully generated alt text for attachment {$attachment_id}: {$sanitized_alt_text}");
519 return $is_ajax ? $sanitized_alt_text : true;
520 } else {
521 $error_msg = esc_html__('Failed to save the generated alt text.', 'king-addons');
522 error_log("King Addons Alt Text: Failed to save alt text for attachment {$attachment_id}");
523 return $is_ajax ? new \WP_Error('update_failed', $error_msg) : $error_msg;
524 }
525 } else {
526 $error_msg = esc_html__('AI returned empty or invalid text.', 'king-addons');
527 error_log("King Addons Alt Text: AI returned empty text for attachment {$attachment_id}");
528 return $is_ajax ? new \WP_Error('empty_alt_text', $error_msg) : $error_msg;
529 }
530
531 // Should not be reached in normal flow due to checks above
532 $error_msg = esc_html__('An unknown error occurred during generation.', 'king-addons');
533 return $is_ajax ? new \WP_Error('unknown_error', $error_msg) : $error_msg;
534 }
535
536 /**
537 * Cleans up cron jobs and options on plugin deactivation.
538 */
539 public function cleanup_cron_jobs(): void {
540 // Clear scheduled cron jobs
541 $timestamp = wp_next_scheduled(self::CRON_HOOK);
542 if ($timestamp) {
543 wp_unschedule_event($timestamp, self::CRON_HOOK);
544 }
545
546 // Clear options
547 delete_option(self::BATCH_OPTION_PENDING);
548 delete_option(self::BATCH_OPTION_PROCESSING);
549
550 // Clean up retry count meta for all attachments
551 global $wpdb;
552 $wpdb->delete(
553 $wpdb->postmeta,
554 array('meta_key' => '_king_addons_alt_retry_count'),
555 array('%s')
556 );
557 }
558
559 /**
560 * Updates the cron schedule if the relevant settings change.
561 *
562 * @param mixed $old_value The old value.
563 * @param mixed $new_value The new value.
564 * @param string $option The option name.
565 */
566 public function update_cron_schedule($old_value, $new_value, string $option = 'king_addons_ai_options'): void {
567
568 // Check if interval setting changed
569 $old_interval = isset($old_value['ai_alt_text_generation_interval']) ? (int) $old_value['ai_alt_text_generation_interval'] : 60;
570 $new_interval = isset($new_value['ai_alt_text_generation_interval']) ? (int) $new_value['ai_alt_text_generation_interval'] : 60;
571
572 // Check if auto generation setting changed
573 $old_auto = isset($old_value['enable_ai_alt_text_auto_generation']) ? (bool) $old_value['enable_ai_alt_text_auto_generation'] : false;
574 $new_auto = isset($new_value['enable_ai_alt_text_auto_generation']) ? (bool) $new_value['enable_ai_alt_text_auto_generation'] : false;
575
576 if ($old_interval !== $new_interval || $old_auto !== $new_auto) {
577 // Clear existing cron job
578 $timestamp = wp_next_scheduled(self::CRON_HOOK);
579 if ($timestamp) {
580 wp_unschedule_event($timestamp, self::CRON_HOOK);
581 }
582
583 // Reschedule if auto generation is enabled
584 if ($new_auto) {
585 wp_schedule_event(time(), 'king_addons_alt_text_interval', self::CRON_HOOK);
586 }
587 }
588 }
589
590 /**
591 * Gets queue status for debugging purposes.
592 *
593 * @return array Queue status information.
594 */
595 public function get_queue_status(): array {
596 $queue = get_option(self::BATCH_OPTION_PENDING, []);
597 $processing = get_option(self::BATCH_OPTION_PROCESSING, false);
598 $next_scheduled = wp_next_scheduled(self::CRON_HOOK);
599
600 return array(
601 'pending_count' => count($queue),
602 'pending_ids' => $queue,
603 'is_processing' => $processing ? true : false,
604 'processing_since' => $processing ? date('Y-m-d H:i:s', $processing) : null,
605 'next_run' => $next_scheduled ? date('Y-m-d H:i:s', $next_scheduled) : null,
606 );
607 }
608 }
609