Plugin Directory

Changeset 3498073


Ignore:
Timestamp:
04/03/2026 09:34:14 AM (15 hours ago)
Author:
vedathemes
Message:

image download bug fix

Location:
podcast-player
Files:
189 added
4 edited

Legend:

Unmodified
Added
Removed
  • podcast-player/trunk/README.txt

    r3484403 r3498073  
    55Tested up to: 6.9
    66Requires PHP: 5.6
    7 Stable tag: 8.0.2
     7Stable tag: 8.0.3
    88License: GPLv3 or later
    99License URI: http://www.gnu.org/licenses/gpl-3.0.html
     
    107107
    108108== Changelog ==
     109= 8.0.3 =
     110* Bug Fix: Image download background jobs fix.
     111
    109112= 8.0.2 =
    110113* Modify: Code cleanup and improvements
  • podcast-player/trunk/backend/inc/class-background-tasks.php

    r3462195 r3498073  
    3232     */
    3333    const MAX_TOTAL_IMAGE_DOWNLOADS = 1000;
     34
     35    /**
     36     * Short-lived image lock TTL to reduce duplicate sideloads while a request is still processing.
     37     */
     38    const IMAGE_DOWNLOAD_LOCK_TTL = 900;
    3439
    3540    /**
     
    121126                // Backfill normalized hash for future de-dupes if missing.
    122127                if ( $norm_hash ) {
    123                     add_post_meta( $matched_post_id, 'pp_featured_key_norm', $norm_hash, true );
     128                    update_post_meta( $matched_post_id, 'pp_featured_key_norm', $norm_hash );
    124129                }
    125130                $completed[ $key ] = array_merge( $item, array( 'post_id' => $matched_post_id ) );
     
    165170                $completed[ $key ] = array_merge( $item, array( 'post_id' => false ) );
    166171            } else {
     172                $lookup = $this->get_image_lookup_data( $image_url );
     173                $existing_attachment_id = $this->find_existing_attachment_for_image( $lookup );
     174                if ( $existing_attachment_id ) {
     175                    $this->persist_image_lookup_meta( $existing_attachment_id, $lookup );
     176                    $completed[ $key ] = array_merge( $item, array( 'post_id' => $existing_attachment_id ) );
     177                    continue;
     178                }
     179
     180                $lock_key = $this->get_image_lock_key( $lookup );
     181                if ( ! $this->acquire_image_download_lock( $lock_key ) ) {
     182                    // Another request is already working on this exact image. Leave it pending.
     183                    continue;
     184                }
     185
     186                try {
     187                    $existing_attachment_id = $this->find_existing_attachment_for_image( $lookup );
     188                    if ( $existing_attachment_id ) {
     189                        $this->persist_image_lookup_meta( $existing_attachment_id, $lookup );
     190                        $completed[ $key ] = array_merge( $item, array( 'post_id' => $existing_attachment_id ) );
     191                        continue;
     192                    }
     193
    167194                // 1. Download the REAL URL (unchanged!)
    168                 $tmp = download_url( $image_url );
    169 
    170                 if ( is_wp_error( $tmp ) ) {
    171                     return ( array( $tmp, false ) );
     195                    $tmp = download_url( $image_url );
     196
     197                    if ( is_wp_error( $tmp ) ) {
     198                        return ( array( $tmp, false ) );
     199                    }
     200
     201                    // 2. Force a correct "filename" for WP regardless of the remote URL
     202                    // Try using MIME type to get correct extension
     203                    $headers  = wp_remote_head( $image_url );
     204                    $mime     = wp_remote_retrieve_header( $headers, 'content-type' );
     205                    $ext      = Get_Fn::get_extension_from_mime( $mime );
     206                    $filename = $lookup['filename_stem'] . '.' . $ext;
     207
     208                    // 3. Build array as if it was a file upload
     209                    $file = array(
     210                        'name'     => $filename,
     211                        'tmp_name' => $tmp,
     212                    );
     213
     214                    // 4. Let WP handle the upload
     215                    $attachment_id = media_handle_sideload( $file, 0, $title );
     216
     217                    if ( ! is_wp_error( $attachment_id ) ) {
     218
     219                        // Count successful downloads.
     220                        $this->increment_image_download_count();
     221
     222                        $this->persist_image_lookup_meta( $attachment_id, $lookup );
     223
     224                        // Let's do post_meta verification to see if data is getting saved correctly.
     225                        $stored = get_post_meta( $attachment_id, 'pp_featured_key', true );
     226                        if ( $stored !== $lookup['raw_hash'] ) {
     227                            wp_delete_attachment( $attachment_id, true );
     228                            $this->disable_image_downloads();
     229                            return array(
     230                                new \WP_Error(
     231                                    'meta-write-failed',
     232                                    esc_html__( 'Failed to persist image meta. Downloads stopped.', 'podcast-player' )
     233                                ),
     234                                false
     235                            );
     236                        }
     237
     238                        $completed[ $key ] = array_merge( $item, array( 'post_id' => $attachment_id ) );
     239                    } else {
     240                        wp_delete_file( $tmp );
     241                        return ( array( $attachment_id, false ) );
     242                    }
     243                } finally {
     244                    $this->release_image_download_lock( $lock_key );
    172245                }
    173 
    174                 // 2. Force a correct "filename" for WP regardless of the remote URL
    175                 // Try using MIME type to get correct extension
    176                 $headers  = wp_remote_head( $image_url );
    177                 $mime     = wp_remote_retrieve_header( $headers, 'content-type' );
    178                 $ext      = Get_Fn::get_extension_from_mime( $mime );
    179                 $filename = 'podcast-episode-image-' . md5( $image_url ) . '.' . $ext;
    180 
    181                 // 3. Build array as if it was a file upload
    182                 $file = [
    183                     'name'     => $filename,
    184                     'tmp_name' => $tmp,
    185                 ];
    186 
    187                 // 4. Let WP handle the upload
    188                 $attachment_id = media_handle_sideload( $file, 0, $title );
    189 
    190                 if ( ! is_wp_error( $attachment_id ) ) {
    191 
    192                     // Count successful downloads.
    193                     $this->increment_image_download_count();
    194 
    195                     add_post_meta( $attachment_id, 'pp_featured_key', md5( $image_url ), true );
    196                     $normalized_url = Utility_Fn::normalize_media_url( $image_url );
    197                     add_post_meta( $attachment_id, 'pp_featured_key_norm', md5( $normalized_url ), true );
    198 
    199                     // Let's do post_meta verification to see if data is getting saved correctly.
    200                     $stored = get_post_meta( $attachment_id, 'pp_featured_key', true );
    201                     if ( $stored !== md5( $image_url ) ) {
    202                         $this->disable_image_downloads();
    203                         return array(
    204                             new \WP_Error(
    205                                 'meta-write-failed',
    206                                 esc_html__( 'Failed to persist image meta. Downloads stopped.', 'podcast-player' )
    207                             ),
    208                             false
    209                         );
    210                     }
    211 
    212                     $completed[ $key ] = array_merge( $item, array( 'post_id' => $attachment_id ) );
    213                 } else {
    214                     wp_delete_file( $tmp );
    215                     return ( array( $attachment_id, false ) );
    216                 }
    217246            }
    218247        }
     
    380409        delete_option( 'pp_total_image_downloads' );
    381410    }
     411
     412    /**
     413     * Build stable lookup values used for de-dupe checks and locks.
     414     *
     415     * @param string $image_url Image URL.
     416     * @return array
     417     */
     418    private function get_image_lookup_data( $image_url ) {
     419        $raw_hash      = md5( $image_url );
     420        $normalized_url = Utility_Fn::normalize_media_url( $image_url );
     421        $norm_hash     = md5( $normalized_url );
     422
     423        return array(
     424            'raw_hash'      => $raw_hash,
     425            'norm_hash'     => $norm_hash,
     426            'filename_stem' => 'podcast-episode-image-' . $raw_hash,
     427        );
     428    }
     429
     430    /**
     431     * Find a previously downloaded attachment for this image.
     432     *
     433     * Falls back to generated filename matching so we can recover from cases where the
     434     * attachment was created but our custom de-dupe meta was never written.
     435     *
     436     * @param array $lookup Lookup data.
     437     * @return int
     438     */
     439    private function find_existing_attachment_for_image( $lookup ) {
     440        global $wpdb;
     441
     442        $hashes = array_filter(
     443            array_unique(
     444                array(
     445                    isset( $lookup['raw_hash'] ) ? $lookup['raw_hash'] : '',
     446                    isset( $lookup['norm_hash'] ) ? $lookup['norm_hash'] : '',
     447                )
     448            )
     449        );
     450
     451        if ( ! empty( $hashes ) ) {
     452            $in_clause = implode(
     453                ',',
     454                array_map(
     455                    function ( $hash ) use ( $wpdb ) {
     456                        return $wpdb->prepare( '%s', $hash );
     457                    },
     458                    $hashes
     459                )
     460            );
     461
     462            $post_id = (int) $wpdb->get_var(
     463                "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key IN ( 'pp_featured_key', 'pp_featured_key_norm' ) AND meta_value IN ( $in_clause ) ORDER BY post_id ASC LIMIT 1"
     464            );
     465            if ( $post_id ) {
     466                return $post_id;
     467            }
     468        }
     469
     470        $filename_stem = isset( $lookup['filename_stem'] ) ? $lookup['filename_stem'] : '';
     471        if ( empty( $filename_stem ) ) {
     472            return 0;
     473        }
     474
     475        return (int) $wpdb->get_var(
     476            $wpdb->prepare(
     477                "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_wp_attached_file' AND meta_value LIKE %s ORDER BY post_id ASC LIMIT 1",
     478                '%' . $wpdb->esc_like( $filename_stem ) . '%'
     479            )
     480        );
     481    }
     482
     483    /**
     484     * Persist de-dupe metadata for a downloaded attachment.
     485     *
     486     * @param int   $attachment_id Attachment ID.
     487     * @param array $lookup        Lookup data.
     488     */
     489    private function persist_image_lookup_meta( $attachment_id, $lookup ) {
     490        $raw_hash  = isset( $lookup['raw_hash'] ) ? $lookup['raw_hash'] : '';
     491        $norm_hash = isset( $lookup['norm_hash'] ) ? $lookup['norm_hash'] : '';
     492
     493        if ( $raw_hash ) {
     494            update_post_meta( $attachment_id, 'pp_featured_key', $raw_hash );
     495        }
     496
     497        if ( $norm_hash ) {
     498            update_post_meta( $attachment_id, 'pp_featured_key_norm', $norm_hash );
     499        }
     500    }
     501
     502    /**
     503     * Get a transient key for a specific image download.
     504     *
     505     * @param array $lookup Lookup data.
     506     * @return string
     507     */
     508    private function get_image_lock_key( $lookup ) {
     509        $hash = ! empty( $lookup['norm_hash'] ) ? $lookup['norm_hash'] : $lookup['raw_hash'];
     510        return 'pp_img_lock_' . $hash;
     511    }
     512
     513    /**
     514     * Acquire a short-lived lock for a specific image download.
     515     *
     516     * @param string $lock_key Lock key.
     517     * @return bool
     518     */
     519    private function acquire_image_download_lock( $lock_key ) {
     520        if ( get_transient( $lock_key ) ) {
     521            return false;
     522        }
     523
     524        return set_transient( $lock_key, 1, self::IMAGE_DOWNLOAD_LOCK_TTL );
     525    }
     526
     527    /**
     528     * Release a short-lived image download lock.
     529     *
     530     * @param string $lock_key Lock key.
     531     */
     532    private function release_image_download_lock( $lock_key ) {
     533        delete_transient( $lock_key );
     534    }
    382535}
  • podcast-player/trunk/helper/core/class-background-jobs.php

    r3462195 r3498073  
    3838
    3939    /**
     40     * Current task ID being processed.
     41     *
     42     * @var string
     43     */
     44    private $current_task_id = '';
     45
     46    /**
     47     * Current task payload being processed.
     48     *
     49     * @var array
     50     */
     51    private $current_task = array();
     52
     53    /**
     54     * Ensure the shutdown handler is only registered once per request.
     55     *
     56     * @var bool
     57     */
     58    private $shutdown_registered = false;
     59
     60    /**
    4061     * Get the name of the queue.
    4162     *
     
    217238     */
    218239    private function handle() {
     240        $this->register_shutdown_handler();
    219241        $this->lock_process();
    220242        $queue           = $this->get_tasks_queue();
    221243        $current_task_id = array_key_first( $queue );
    222244        $current_task    = $queue[ $current_task_id ];
     245        $this->current_task_id = $current_task_id;
     246        $this->current_task    = $current_task;
    223247        $error           = false;
    224248
     
    244268
    245269        if ( $error ) {
    246             $current_task['attempts'] = $current_task['attempts'] + 1;
    247             $new_queue                = $this->get_tasks_queue();
    248             if ( $current_task['attempts'] < 3 ) {
    249                 $new_queue[ $current_task_id ] = $current_task;
    250             } else {
    251                 unset( $new_queue[ $current_task_id ] );
    252 
    253                 // If error is in image download. Let's disable image download to prevent infinite loop.
    254                 if ( 'download_image' === $current_task['type'] ) {
    255                     $options = get_option( 'pp-common-options', array() );
    256                     $options['img_save'] = 'no';
    257                     update_option( 'pp-common-options', $options );
    258                 }
    259             }
    260             $this->set_tasks_queue( $new_queue );
    261             $this->log_error( $error, $current_task['data'] );
     270            $this->handle_task_error( $current_task_id, $current_task, $error );
    262271        }
    263272
     
    265274        sleep( 5 );
    266275        $this->unlock_process();
     276        $this->reset_current_task();
    267277
    268278        // If import tasks are pending and current task didn't error, re-dispatch to prioritize completion.
     
    270280            $this->dispatch_internal( true );
    271281        }
     282    }
     283
     284    /**
     285     * Register a shutdown handler to capture fatal processing failures.
     286     */
     287    private function register_shutdown_handler() {
     288        if ( $this->shutdown_registered ) {
     289            return;
     290        }
     291
     292        register_shutdown_function( array( $this, 'handle_fatal_shutdown' ) );
     293        $this->shutdown_registered = true;
     294    }
     295
     296    /**
     297     * Handle fatal shutdowns that bypass normal exception handling.
     298     */
     299    public function handle_fatal_shutdown() {
     300        $error = error_get_last();
     301        if ( empty( $this->current_task_id ) || empty( $this->current_task ) || ! $this->is_fatal_error( $error ) ) {
     302            return;
     303        }
     304
     305        $message = isset( $error['message'] ) ? $error['message'] : esc_html__( 'Unknown fatal error.', 'podcast-player' );
     306        $this->handle_task_error( $this->current_task_id, $this->current_task, $message );
     307        $this->unlock_process();
     308        $this->reset_current_task();
     309    }
     310
     311    /**
     312     * Determine if the provided shutdown error should be treated as fatal.
     313     *
     314     * @param array|false $error Last PHP error.
     315     * @return bool
     316     */
     317    private function is_fatal_error( $error ) {
     318        if ( empty( $error ) || ! is_array( $error ) || empty( $error['type'] ) ) {
     319            return false;
     320        }
     321
     322        return in_array(
     323            (int) $error['type'],
     324            array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ),
     325            true
     326        );
     327    }
     328
     329    /**
     330     * Update queue state after a task failure.
     331     *
     332     * @param string $task_id  Task ID.
     333     * @param array  $task     Task payload.
     334     * @param string $error    Error message.
     335     */
     336    private function handle_task_error( $task_id, $task, $error ) {
     337        $lock_token = $this->acquire_queue_lock();
     338        $new_queue  = $this->get_tasks_queue( false );
     339
     340        if ( ! isset( $new_queue[ $task_id ] ) ) {
     341            $this->release_queue_lock( $lock_token );
     342            return;
     343        }
     344
     345        $new_queue[ $task_id ]['attempts'] = isset( $new_queue[ $task_id ]['attempts'] ) ? (int) $new_queue[ $task_id ]['attempts'] + 1 : 1;
     346        $attempts                         = $new_queue[ $task_id ]['attempts'];
     347
     348        if ( $attempts >= 3 ) {
     349            unset( $new_queue[ $task_id ] );
     350
     351            // If error is in image download. Let's disable image download to prevent infinite loop.
     352            if ( isset( $task['type'] ) && 'download_image' === $task['type'] ) {
     353                $options = get_option( 'pp-common-options', array() );
     354                $options['img_save'] = 'no';
     355                update_option( 'pp-common-options', $options );
     356            }
     357        }
     358
     359        $this->set_tasks_queue( $new_queue );
     360        $this->release_queue_lock( $lock_token );
     361        $this->log_error( $error, isset( $task['data'] ) ? $task['data'] : array() );
     362    }
     363
     364    /**
     365     * Clear current task tracking after processing completes.
     366     */
     367    private function reset_current_task() {
     368        $this->current_task_id = '';
     369        $this->current_task    = array();
    272370    }
    273371
  • podcast-player/trunk/podcast-player.php

    r3484403 r3498073  
    1515 * Plugin URI:        https://easypodcastpro.com
    1616 * Description:       Host your podcast episodes anywhere, display them only using podcast feed url. Use custom widget or shortcode to display podcast player anywhere on your site.
    17  * Version:           8.0.2
     17 * Version:           8.0.3
    1818 * Author:            vedathemes
    1919 * Author URI:        https://easypodcastpro.com
     
    3030
    3131// Currently plugin version.
    32 define( 'PODCAST_PLAYER_VERSION', '8.0.2' );
     32define( 'PODCAST_PLAYER_VERSION', '8.0.3' );
    3333
    3434// Define plugin constants.
Note: See TracChangeset for help on using the changeset viewer.