'image', 'image/png' => 'image', 'image/gif' => 'image', 'image/webp' => 'image', //Videos 'video/mp4' => 'video', 'video/webm' => 'video', 'video/ogg' => 'video', 'video/ogv' => 'video', 'video/quicktime'=> 'video', // .mov files 'video/x-msvideo'=> 'video', //Documents 'application/pdf' => 'document', 'application/msword' => 'document', // .doc 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'document', // .docx 'application/vnd.ms-excel' => 'document', // .xls 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'document', // .xlsx 'text/plain' => 'document', // .txt 'text/csv' => 'document', 'application/rtf' => 'document' ]; /** * @var array Default configuration */ protected array $config = [ 'allowed_types' => [ //Images 'image/jpeg', 'image/png', 'image/gif', 'image/webp', //Videos 'video/mp4', 'video/webm', 'video/ogg', 'video/ogv', 'video/quicktime', // .mov files 'video/x-msvideo', //Documents 'application/pdf', 'application/msword', // .doc 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx 'application/vnd.ms-excel', // .xls 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx 'text/plain', // .txt 'text/csv', 'application/rtf' ], 'max_size' => [ 'image' => 5242880, // 5MB 'video' => 104857600, // 100MB 'document' => 10485760 // 10MB ], //Image Specific Settings 'convert_to_webp' => true, 'webp_quality' => 80, 'optimize_images' => true, 'create_thumbnails' => true, 'use_imagick' => null, // Will be set in constructor //Video specific settings 'extract_video_thumbnail' => true, 'video_thumbnail_time' => 0, // Document-specific settings 'extract_document_preview' => false, // General settings 'original_retention' => 2592000, // 30 days in seconds ]; protected string $post_type; protected int $user_id; protected int $term_id; protected string $upload_dir; protected string $upload_url; protected object $cache; protected object $notifications; protected int $max_retries = 3; protected int $retry_delay = 300; // 5 minutes public function __construct($post_type, $user_id, $config = []) { $this->post_type = $post_type; $this->user_id = $user_id; $this->config = wp_parse_args($config, $this->config); // Check for Imagick availability $this->config['use_imagick'] = extension_loaded('imagick'); $this->setupUploadDirs(); // Schedule cleanup of original files // if (!wp_next_scheduled('jvb_cleanup_original_uploads')) { // wp_schedule_event(time(), 'daily', 'jvb_cleanup_original_uploads'); // } // add_action('jvb_cleanup_original_uploads', [$this, 'cleanupOriginalFiles']); // Track upload statistics add_action('jvb_upload_complete', [$this, 'trackUploadStats']); add_action('jvb_upload_failed', [$this, 'track_failed_upload']); } /** * @param array $file_data * * @return array * @throws Exception */ public function secureUploadedFile(array $file_data): array { // Create a persistent temporary directory if it doesn't exist $temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$this->user_id}"; wp_mkdir_p($temp_dir); // Generate a unique filename for temporary storage $temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']); $temp_path = "{$temp_dir}/{$temp_filename}"; // Move the uploaded file to our temporary storage if (!move_uploaded_file($file_data['tmp_name'], $temp_path)) { // Fallback to copy if needed if (!copy($file_data['tmp_name'], $temp_path)) { throw new Exception('Failed to store uploaded file'); } } // Return metadata about the stored file return [ 'temp_path' => $temp_path, 'original_name' => $file_data['name'], 'mime_type' => $file_data['type'], 'file_size' => $file_data['size'], 'stored_at' => current_time('mysql') ]; } /** * Process file from secured storage */ public function processFileFromStorage(string $temp_path, string $post_type, int $post_id, int $term_id, string $original_filename): array|WP_Error { if (!file_exists($temp_path)) { throw new Exception('Temporary file no longer exists: ' . $temp_path); } $this->term_id = $term_id; $this->post_type = $post_type; $rel_path = $this->getUploadDirectory(); $base_filename = $this->generateFilename($original_filename, get_userdata($this->user_id)); return $this->processFile($temp_path, $rel_path, $base_filename, $post_id); } /** * Main entry point - detects file type and routes to appropriate processor * @throws Exception */ public function processFile(string $file_path, string $rel_path, string $filename, int $post_id): array|WP_Error { if (!file_exists($file_path)) { throw new Exception('File no longer exists: ' . $file_path); } $mime_type = mime_content_type($file_path); if (!in_array($mime_type, $this->config['allowed_types'])) { throw new Exception('Invalid file type: ' . $mime_type); } $file_type = (array_key_exists($mime_type, $this->allowedTypes)) ? $this->allowedTypes[$mime_type] : 'unknown'; // Route to appropriate processor switch ($file_type) { case 'image': return $this->processImage($file_path, $rel_path, $filename, $post_id); case 'video': return $this->processVideo($file_path, $rel_path, $filename, $post_id); case 'document': return $this->processDocument($file_path, $rel_path, $filename, $post_id); default: throw new Exception('Unknown file type category'); } } /** * Validate file size based on type */ protected function validateFileSize(string $file_path, string $file_type): bool { $file_size = filesize($file_path); $max_size = $this->config['max_size'][$file_type] ?? $this->config['max_size']['image']; if ($file_size > $max_size) { throw new Exception(sprintf( 'File size (%s) exceeds maximum allowed size (%s) for %s files', size_format($file_size), size_format($max_size), $file_type )); } return true; } /** * @param string $temp_path * @param string $post_type * @param int $post_id * @param int $term_id * @param string $original_filename * * @return array * @throws Exception */ public function processImageFromStorage(string $temp_path, string $post_type, int $post_id = 0, int $term_id = 0, string $original_filename = ''): array { if (!file_exists($temp_path)) { throw new Exception('Stored file no longer exists: ' . $temp_path); } $this->term_id = $term_id; $this->post_type = $post_type; // Get base upload directory based on content type $rel_path = $this->getUploadDirectory(); // Ensure the directory exists $full_upload_path = $this->upload_dir . '/' . $rel_path; if (!wp_mkdir_p($full_upload_path)) { throw new Exception("Failed to create upload directory: {$full_upload_path}"); } // Generate filename WITHOUT extension (generateFilename should not include extension) $base_filename = $this->generateFilename($original_filename, get_userdata($this->user_id)); // Process the image directly from our temporary storage return $this->processImage($temp_path, $rel_path, $base_filename, $post_id); } /** * @return void * @throws Exception */ protected function setupUploadDirs():void { $upload_info = wp_upload_dir(); if ($upload_info['error']) { throw new Exception($upload_info['error']); } $this->upload_dir = $upload_info['basedir']; $this->upload_url = $upload_info['baseurl']; } /** * @return string */ public function getUploadDirectory():string { // Default WordPress organization: year/month $default_path = date('Y/m'); /** * Filter the upload directory structure * * @param string $path The default upload path * @param string $post_type The post type being uploaded * @param int $user_id The user ID * @param int $term_id The term ID (if applicable) */ return apply_filters('jvb_upload_directory', $default_path, $this->post_type, $this->user_id, $this->term_id); } /** * Generate filename with extensible filtering * Default: generic SEO-friendly format * * @param string $original_name * @param object $user_data * @return string */ public function generateFilename(string $original_name, object $user_data): string { // Default generic filename: {post_type}-{user_id}-{counter} $filename = sprintf( '%s-%s', sanitize_title($this->post_type), $this->user_id ); /** * Filter the generated filename (without extension) * * @param string $filename The generated filename * @param string $original_name The original filename * @param object $user_data WordPress user data object * @param string $post_type The post type * @param int $user_id The user ID * @param int $term_id The term ID (if applicable) */ return apply_filters('jvb_upload_filename', $filename, $original_name, $user_data, $this->post_type, $this->user_id, $this->term_id).'-'.$this->getNextFileCounter(); } /** * @return string */ protected function getNextFileCounter(): string { // Get counter key for this post type $counter_key = 'upload_counter_' . str_replace(['/', '\\'], '_', $this->post_type); // Get current counter value, default to 0 $counter = (int)get_user_meta($this->user_id, $counter_key, true) ?: 0; // Increment counter $counter++; // Update counter in user meta update_user_meta($this->user_id, $counter_key, $counter); // Return formatted counter return sprintf('%08d', $counter); } /** * Generate alt text with filtering for customization * Default: basic or empty alt text * * @param string $file * @param object $user_data * @param int|null $post_id * @return string */ protected function generateAltText(string $file, object $user_data, int|null $post_id = null): string { // Default: basic alt text or empty $alt_text = ''; /** * Filter the generated alt text * * @param string $alt_text The generated alt text * @param string $file The file path * @param object $user_data WordPress user data object * @param int|null $post_id The post ID (if applicable) * @param string $post_type The post type * @param int $user_id The user ID * @param int $term_id The term ID (if applicable) */ return apply_filters('jvb_upload_alt_text', $alt_text, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id); } /** * Generate image title with filtering for customization * Default: WordPress default behavior (filename-based) * * @param string $file * @param object $user_data * @param int|null $post_id * @return string */ protected function generateImageTitle(string $file, object $user_data, int|null $post_id = null): string { // Default: Use filename without extension (WordPress default behavior) $title = pathinfo($file, PATHINFO_FILENAME); /** * Filter the generated image title * * @param string $title The generated title * @param string $file The file path * @param object $user_data WordPress user data object * @param int|null $post_id The post ID (if applicable) * @param string $post_type The post type * @param int $user_id The user ID * @param int $term_id The term ID (if applicable) */ return apply_filters('jvb_upload_image_title', $title, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id); } /** * @param array $file_data * @param array $options * * @return array|WP_Error */ public function handleContentUpload(array $file_data, array $options = []): array|WP_Error { try { if (!isset($file_data['tmp_name']) || !is_uploaded_file($file_data['tmp_name'])) { throw new Exception('No valid file uploaded.'); } $user_data = get_userdata($this->user_id); $post_id = $options['post_id'] ?? 0; $this->term_id = $options['term_id'] ?? 0; // Get base upload directory based on content type $base_dir = $this->getUploadDirectory(); $original_dir = 'originals'; $rel_path = $base_dir; $this->ensureUploadDirs($rel_path, $original_dir); $filename = $this->generateFilename($file_data['name'], $user_data); // Store original file $original_file = $this->storeOriginalFile( $file_data['tmp_name'], $base_dir, $original_dir, $filename ); // Process immediately return $this->processImage($original_file, $rel_path, $filename, $post_id); } catch (Exception $e) { return new WP_Error('upload_failed', $e->getMessage()); } } /** * @param string $rel_path * @param string $original_dir * * @return void * @throws Exception */ protected function ensureUploadDirs($rel_path, $original_dir): void { $dirs = [ "{$this->upload_dir}/{$rel_path}", "{$this->upload_dir}/{$rel_path}/{$original_dir}" ]; foreach ($dirs as $dir) { if (!wp_mkdir_p($dir)) { throw new Exception("Failed to create directory: {$dir}"); } } } /** * @param string $tmp_file * @param string $base_dir * @param string $original_dir * @param string $filename * @return string * @throws Exception */ protected function storeOriginalFile(string $tmp_file, string $base_dir, string $original_dir, string $filename): string { $ext = pathinfo($tmp_file, PATHINFO_EXTENSION); $original_path = "{$this->upload_dir}/{$base_dir}/{$original_dir}/{$filename}.{$ext}"; if (!move_uploaded_file($tmp_file, $original_path)) { throw new Exception('Failed to store original file'); } return $original_path; } /** * Process image with better error handling * @throws Exception */ protected function processImage(string $original_file, string $rel_path, string $filename, int $post_id): array { // Validate the original file still exists if (!file_exists($original_file)) { throw new Exception('Original file no longer exists: ' . $original_file); } $this->validateFileSize($original_file, 'image'); // Verify file type before processing $mime_type = mime_content_type($original_file); if (!in_array($mime_type, $this->config['allowed_types'])) { throw new Exception('Invalid file type detected during processing: ' . $mime_type); } // Ensure the upload directory exists $full_upload_dir = $this->upload_dir . '/' . $rel_path; if (!wp_mkdir_p($full_upload_dir)) { throw new Exception("Failed to create upload directory: {$full_upload_dir}"); } // Convert to WebP if enabled if ($this->config['convert_to_webp'] && $mime_type !== 'image/webp') { $final_path = "{$full_upload_dir}/{$filename}.webp"; if ($this->config['use_imagick']) { $this->convertWithImagick($original_file, $final_path); } else { $this->convertWithGd($original_file, $final_path); } } else { // Just copy the original with its extension $original_ext = pathinfo($original_file, PATHINFO_EXTENSION); $final_path = "{$full_upload_dir}/{$filename}.{$original_ext}"; if (!copy($original_file, $final_path)) { throw new Exception("Failed to copy file from {$original_file} to {$final_path}"); } } // Verify the final file was created if (!file_exists($final_path)) { throw new Exception("Final processed file was not created: {$final_path}"); } // Generate title text $title = $this->generateImageTitle( $final_path, get_userdata($this->user_id), $post_id ); // Create attachment with title $attachment_id = $this->createAttachment($final_path, $title, $post_id); // Generate thumbnails if ($this->config['create_thumbnails']) { $this->generateThumbnails($attachment_id); } // Update post attachments with new file info $this->updatePostAttachments($attachment_id, $final_path); return [ 'success' => true, 'attachment_id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'file' => $final_path ]; } /** * @param string $source * @param string $destination * * @return void * @throws Exception */ protected function convertWithImagick(string $source, string $destination, string $toType = 'webp'): void { $allowed = ['webp', 'jpeg', 'jpg', 'png']; if (!in_array($toType, $allowed)) { return; } try { $image = new Imagick($source); $image->setImageFormat('webp'); $image->setImageCompressionQuality($this->config['webp_quality']); $image->writeImage($destination); $image->clear(); } catch (Exception $e) { throw new Exception('WebP conversion with Imagick failed: ' . $e->getMessage()); } } /** * Fixed convertWithGd method with better error handling */ protected function convertWithGd(string $source, string $destination, string $toType = 'webp'): void { $mime_type = mime_content_type($source); // Ensure destination directory exists $dest_dir = dirname($destination); if (!wp_mkdir_p($dest_dir)) { throw new Exception("Failed to create destination directory: {$dest_dir}"); } switch ($mime_type) { case 'image/webp': $image = imagecreatefromwebp($source); break; case 'image/jpeg': $image = imagecreatefromjpeg($source); break; case 'image/png': $image = imagecreatefrompng($source); if ($image !== false) { imagepalettetotruecolor($image); imagealphablending($image, true); imagesavealpha($image, true); } break; case 'image/gif': $image = imagecreatefromgif($source); if ($image !== false) { imagepalettetotruecolor($image); } break; default: throw new Exception('Unsupported image type for GD conversion: ' . $mime_type); } if ($image === false) { throw new Exception('Failed to create image resource from source file: ' . $source); } // Convert to WebP $result = imagewebp($image, $destination, $this->config['webp_quality']); // Clean up memory imagedestroy($image); if (!$result) { throw new Exception('WebP conversion with GD failed - imagewebp returned false'); } // Verify the file was actually created if (!file_exists($destination)) { throw new Exception('WebP file was not created despite imagewebp returning true'); } } /** * @return void */ public function cleanupOriginalFiles(): void { $cutoff = time() - $this->config['original_retention']; // Get upload directory and find original directories $pattern = "{$this->upload_dir}/**/originals"; $original_dirs = glob($pattern, GLOB_ONLYDIR); foreach ($original_dirs as $original_dir) { $files = glob("{$original_dir}/*"); foreach ($files as $file) { if (filemtime($file) < $cutoff) { unlink($file); } } } } /** * @param string|null $temp_dir * @return void */ protected function cleanupTempFiles(string|null $temp_dir = null): void { if (is_null($temp_dir)) { $temp_dir = $this->upload_dir . '/tmp'; } if (is_dir($temp_dir)) { $files = glob($temp_dir . '/*'); foreach ($files as $file) { if (is_file($file)) { @unlink($file); } } @rmdir($temp_dir); } } /** * @param int $retention_days * @return void */ public function cleanupUserFiles(int $retention_days = 30): void { $cutoff = time() - ($retention_days * DAY_IN_SECONDS); $user_dir = $this->getUploadDirectory(); if (!is_dir($user_dir)) { return; } $this->cleanupDirectory($user_dir, $cutoff); } /** * @param string $dir * @param int $cutoff * @return void */ protected function cleanupDirectory(string $dir, int $cutoff): void { $files = glob($dir . '/*'); foreach ($files as $file) { if (is_dir($file)) { $this->cleanupDirectory($file, $cutoff); } elseif (filemtime($file) < $cutoff) { @unlink($file); } } } /** * @param string $file * @param string $title * @param int $post_id * @return int|WP_Error * @throws Exception */ protected function createAttachment(string $file, string $title, int $post_id): int|WP_Error { $file_url = str_replace($this->upload_dir, $this->upload_url, $file); $attachment = [ 'post_mime_type' => mime_content_type($file), 'post_title' => $title, 'post_name' => sanitize_title($title), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $file_url ]; $attach_id = wp_insert_attachment($attachment, $file, $post_id); if (is_wp_error($attach_id)) { throw new Exception($attach_id->get_error_message()); } // Generate and set alt text for images only if (str_starts_with(mime_content_type($file), 'image/')) { $alt_text = $this->generateAltText($file, get_userdata($this->user_id), $post_id); update_post_meta($attach_id, '_wp_attachment_image_alt', $alt_text); } return $attach_id; } /** * Generate thumbnails using WordPress's built-in image size system * This will create all registered image sizes (thumbnail, medium, large, and any custom sizes) * Sites can register custom image sizes using add_image_size() * * @param int $attachment_id * @return void */ protected function generateThumbnails(int $attachment_id): void { require_once(ABSPATH . 'wp-admin/includes/image.php'); $metadata = wp_generate_attachment_metadata($attachment_id, get_attached_file($attachment_id)); wp_update_attachment_metadata($attachment_id, $metadata); } /** * @param $result * @return void */ protected function trackUploadStats($result) { $stats_key = "upload_stats_{$this->user_id}"; $stats = wp_cache_get($stats_key) ?: [ 'total_uploads' => 0, 'successful_uploads' => 0, 'failed_uploads' => 0, 'total_size' => 0, 'last_upload' => null ]; if ($result['success']) { $stats['successful_uploads']++; $stats['total_size'] += filesize($result['file']); } else { $stats['failed_uploads']++; } $stats['total_uploads']++; $stats['last_upload'] = current_time('mysql'); wp_cache_set($stats_key, $stats, '', DAY_IN_SECONDS); } /** * @param int $attachment_id * @param string $new_file_path * @return void */ protected function updatePostAttachments(int $attachment_id, string $new_file_path): void { // Update attachment post $file_url = str_replace($this->upload_dir, $this->upload_url, $new_file_path); $filename = basename($new_file_path); wp_update_post([ 'ID' => $attachment_id, 'guid' => $file_url, 'post_mime_type' => mime_content_type($new_file_path), 'post_title' => $filename ]); // Update attachment metadata update_post_meta($attachment_id, '_wp_attached_file', str_replace($this->upload_dir . '/', '', $new_file_path)); // Update attachment metadata including sizes $metadata = wp_get_attachment_metadata($attachment_id); if ($metadata) { $metadata['file'] = str_replace($this->upload_dir . '/', '', $new_file_path); // Update thumbnail paths if they exist if (!empty($metadata['sizes'])) { foreach ($metadata['sizes'] as $size => $info) { $old_file = $info['file']; $new_file = preg_replace( '/\.(jpe?g|png|gif)$/i', '.webp', $old_file ); $metadata['sizes'][$size]['file'] = $new_file; $metadata['sizes'][$size]['mime-type'] = 'image/webp'; } } wp_update_attachment_metadata($attachment_id, $metadata); } // If this is a profile/featured image, update those references $post_id = wp_get_post_parent_id($attachment_id); if ($post_id) { $featured_image_id = get_post_thumbnail_id($post_id); if ($featured_image_id === $attachment_id) { // Re-set the featured image to trigger any necessary updates set_post_thumbnail($post_id, $attachment_id); } } // Clear any caches clean_attachment_cache($attachment_id); clean_post_cache($post_id); } /** * Clean up empty temporary directories * * @param int $user_id * @return void */ public function cleanupEmptyTempDirs(int $user_id): void { $temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$user_id}"; if (is_dir($temp_dir)) { // Check if directory is empty $files = scandir($temp_dir); $is_empty = (count($files) <= 2); // Only . and .. entries if ($is_empty) { // Try to remove the empty directory @rmdir($temp_dir); // Also check if parent temp directory is empty $parent_temp_dir = "{$this->upload_dir}/jvb_temp_uploads"; $parent_files = scandir($parent_temp_dir); $parent_is_empty = (count($parent_files) <= 2); // Only . and .. entries if ($parent_is_empty) { @rmdir($parent_temp_dir); } } } } /** * Process video files */ protected function processVideo(string $original_file, string $rel_path, string $filename, int $post_id): array { $this->validateFileSize($original_file, self::TYPE_VIDEO); $full_upload_dir = $this->upload_dir . '/' . $rel_path; if (!wp_mkdir_p($full_upload_dir)) { throw new Exception("Failed to create upload directory: {$full_upload_dir}"); } // Keep original extension for videos $original_ext = pathinfo($original_file, PATHINFO_EXTENSION); $final_path = "{$full_upload_dir}/{$filename}.{$original_ext}"; if (!copy($original_file, $final_path)) { throw new Exception("Failed to copy video file"); } if (!file_exists($final_path)) { throw new Exception("Final video file was not created: {$final_path}"); } // Generate title $title = $this->generateMediaTitle($final_path, get_userdata($this->user_id), $post_id, 'video'); $attachment_id = $this->createAttachment($final_path, $title, $post_id); // Extract video metadata $video_metadata = $this->extractVideoMetadata($final_path); if ($video_metadata) { update_post_meta($attachment_id, '_jvb_video_metadata', $video_metadata); } // Generate video thumbnail if enabled if ($this->config['extract_video_thumbnail']) { $thumbnail_id = $this->generateVideoThumbnail($final_path, $attachment_id, $post_id); if ($thumbnail_id) { update_post_meta($attachment_id, '_jvb_video_thumbnail', $thumbnail_id); } } return [ 'success' => true, 'type' => self::TYPE_VIDEO, 'attachment_id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'file' => $final_path, 'metadata' => $video_metadata ?? null ]; } /** * Process document files */ protected function processDocument(string $original_file, string $rel_path, string $filename, int $post_id): array { $this->validateFileSize($original_file, self::TYPE_DOCUMENT); $full_upload_dir = $this->upload_dir . '/' . $rel_path; if (!wp_mkdir_p($full_upload_dir)) { throw new Exception("Failed to create upload directory: {$full_upload_dir}"); } // Keep original extension for documents $original_ext = pathinfo($original_file, PATHINFO_EXTENSION); $final_path = "{$full_upload_dir}/{$filename}.{$original_ext}"; if (!copy($original_file, $final_path)) { throw new Exception("Failed to copy document file"); } if (!file_exists($final_path)) { throw new Exception("Final document file was not created: {$final_path}"); } // Generate title $title = $this->generateMediaTitle($final_path, get_userdata($this->user_id), $post_id, 'document'); $attachment_id = $this->createAttachment($final_path, $title, $post_id); // Extract document metadata $doc_metadata = $this->extractDocumentMetadata($final_path); if ($doc_metadata) { update_post_meta($attachment_id, '_jvb_document_metadata', $doc_metadata); } return [ 'success' => true, 'type' => self::TYPE_DOCUMENT, 'attachment_id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'file' => $final_path, 'metadata' => $doc_metadata ?? null ]; } /** * Extract video metadata using getID3 or FFmpeg if available */ protected function extractVideoMetadata(string $file_path): ?array { $metadata = [ 'filesize' => filesize($file_path), 'mime_type' => mime_content_type($file_path) ]; // Try FFmpeg first (more reliable) if ($this->hasFFmpeg()) { $ffmpeg_data = $this->getVideoMetadataFFmpeg($file_path); if ($ffmpeg_data) { return array_merge($metadata, $ffmpeg_data); } } // Fallback to getID3 if available if (class_exists('getID3')) { $getID3 = new \getID3(); $file_info = $getID3->analyze($file_path); if (isset($file_info['video'])) { $metadata['duration'] = $file_info['playtime_seconds'] ?? null; $metadata['width'] = $file_info['video']['resolution_x'] ?? null; $metadata['height'] = $file_info['video']['resolution_y'] ?? null; $metadata['codec'] = $file_info['video']['codec'] ?? null; $metadata['bitrate'] = $file_info['bitrate'] ?? null; } } return $metadata; } /** * Get video metadata using FFmpeg */ protected function getVideoMetadataFFmpeg(string $file_path): ?array { $ffprobe_path = $this->getFFprobePath(); if (!$ffprobe_path) { return null; } $command = sprintf( '%s -v quiet -print_format json -show_format -show_streams %s', escapeshellarg($ffprobe_path), escapeshellarg($file_path) ); $output = shell_exec($command); if (!$output) { return null; } $data = json_decode($output, true); if (!$data) { return null; } $metadata = []; // Get duration if (isset($data['format']['duration'])) { $metadata['duration'] = (float) $data['format']['duration']; } // Get video stream info foreach ($data['streams'] ?? [] as $stream) { if ($stream['codec_type'] === 'video') { $metadata['width'] = $stream['width'] ?? null; $metadata['height'] = $stream['height'] ?? null; $metadata['codec'] = $stream['codec_name'] ?? null; $metadata['bitrate'] = $stream['bit_rate'] ?? null; break; } } return $metadata; } /** * Check if FFmpeg is available */ protected function hasFFmpeg(): bool { return $this->getFFprobePath() !== null; } /** * Get FFprobe path (companion tool to FFmpeg) */ protected function getFFprobePath(): ?string { $paths = ['ffprobe', '/usr/bin/ffprobe', '/usr/local/bin/ffprobe']; foreach ($paths as $path) { if (shell_exec("which {$path}")) { return $path; } } return null; } /** * Generate video thumbnail */ protected function generateVideoThumbnail(string $video_path, int $video_attachment_id, int $post_id): ?int { if (!$this->hasFFmpeg()) { return null; } $ffmpeg_path = str_replace('ffprobe', 'ffmpeg', $this->getFFprobePath()); if (!$ffmpeg_path) { return null; } // Generate thumbnail filename $thumbnail_dir = dirname($video_path) . '/thumbnails'; wp_mkdir_p($thumbnail_dir); $thumbnail_filename = pathinfo($video_path, PATHINFO_FILENAME) . '-thumb.jpg'; $thumbnail_path = $thumbnail_dir . '/' . $thumbnail_filename; // Extract frame at specified time $time = $this->config['video_thumbnail_time']; $command = sprintf( '%s -i %s -ss %d -vframes 1 -q:v 2 %s 2>&1', escapeshellarg($ffmpeg_path), escapeshellarg($video_path), $time, escapeshellarg($thumbnail_path) ); shell_exec($command); if (!file_exists($thumbnail_path)) { return null; } // Create attachment for thumbnail $title = get_the_title($video_attachment_id) . ' - Thumbnail'; $thumbnail_id = $this->createAttachment($thumbnail_path, $title, $post_id); return $thumbnail_id; } /** * Extract document metadata */ protected function extractDocumentMetadata(string $file_path): array { $metadata = [ 'filesize' => filesize($file_path), 'mime_type' => mime_content_type($file_path), 'extension' => pathinfo($file_path, PATHINFO_EXTENSION) ]; // PDF-specific metadata if ($metadata['mime_type'] === 'application/pdf') { $pdf_metadata = $this->extractPdfMetadata($file_path); if ($pdf_metadata) { $metadata = array_merge($metadata, $pdf_metadata); } } return $metadata; } /** * Extract PDF metadata */ protected function extractPdfMetadata(string $file_path): ?array { $metadata = []; // Try using pdfinfo if available if (shell_exec('which pdfinfo')) { $output = shell_exec('pdfinfo ' . escapeshellarg($file_path)); if ($output) { if (preg_match('/Pages:\s+(\d+)/', $output, $matches)) { $metadata['pages'] = (int) $matches[1]; } if (preg_match('/Title:\s+(.+)/', $output, $matches)) { $metadata['title'] = trim($matches[1]); } if (preg_match('/Author:\s+(.+)/', $output, $matches)) { $metadata['author'] = trim($matches[1]); } } } // Fallback to basic file parsing if pdfinfo not available if (empty($metadata)) { $content = file_get_contents($file_path, false, null, 0, 1024); if (preg_match('/\/Count\s+(\d+)/', $content, $matches)) { $metadata['pages'] = (int) $matches[1]; } } return $metadata ?: null; } /** * Generate media title (for videos and documents) */ protected function generateMediaTitle(string $file_path, object $user_data, int $post_id, string $type): string { $filename = pathinfo($file_path, PATHINFO_FILENAME); $post_title = $post_id ? get_the_title($post_id) : ''; $title = sprintf( '%s %s by %s', ucfirst($type), $post_title ? "for {$post_title}" : $filename, $user_data->display_name ); /** * Filter the generated media title */ return apply_filters('jvb_media_title', $title, $file_path, $user_data, $post_id, $type); } }