db = $db; $this->logger = $logger; } /** * Get base upload directory for MLS media * * @return string Absolute path */ public function get_upload_dir() { $upload_dir = wp_upload_dir(); return $upload_dir['basedir'] . '/' . self::UPLOAD_SUBDIR; } /** * Get base upload URL for MLS media * * @return string URL */ public function get_upload_url() { $upload_dir = wp_upload_dir(); return $upload_dir['baseurl'] . '/' . self::UPLOAD_SUBDIR; } /** * Get storage directory for a specific listing * * @param string $listing_key Listing key * @return string Absolute path */ public function get_listing_dir($listing_key) { // Use first 2 characters as subdirectory to prevent too many files in one folder $prefix = substr($listing_key, 0, 2); return $this->get_upload_dir() . '/' . $prefix . '/' . $listing_key; } /** * Sync media for a property * * @param string $listing_key Listing key * @param array $media_array Media array from API * @param bool $force Force re-download all media */ public function sync_property_media($listing_key, $media_array, $force = false) { global $wpdb; if (empty($media_array)) { return; } $received_keys = array(); foreach ($media_array as $media) { $media_key = $media['MediaKey'] ?? null; if (!$media_key) { continue; } $received_keys[] = $media_key; // Check if media record exists $existing = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->db->media_table()} WHERE listing_key = %s AND media_key = %s", $listing_key, $media_key )); $data = array( 'listing_key' => $listing_key, 'media_key' => $media_key, 'media_type' => $media['MediaType'] ?? 'Photo', 'media_order' => $media['Order'] ?? 0, 'media_url' => $media['MediaURL'] ?? null, 'image_width' => $media['ImageWidth'] ?? null, 'image_height' => $media['ImageHeight'] ?? null, 'media_modification_timestamp' => isset($media['MediaModificationTimestamp']) ? date('Y-m-d H:i:s', strtotime($media['MediaModificationTimestamp'])) : null, 'updated_at' => current_time('mysql'), ); if ($existing) { // Update existing record $wpdb->update( $this->db->media_table(), $data, array('id' => $existing->id) ); // Check if we need to re-download if ($force || $this->needs_download($existing, $media)) { $this->download_media($existing->id); } } else { // Insert new record $data['created_at'] = current_time('mysql'); $wpdb->insert($this->db->media_table(), $data); // Queue download $this->download_media($wpdb->insert_id); } } // Delete media that no longer exists if (!empty($received_keys)) { $placeholders = implode(',', array_fill(0, count($received_keys), '%s')); $values = array_merge(array($listing_key), $received_keys); $orphaned = $wpdb->get_results($wpdb->prepare( "SELECT id, local_path FROM {$this->db->media_table()} WHERE listing_key = %s AND media_key NOT IN ({$placeholders})", $values )); foreach ($orphaned as $record) { // Delete file if exists if ($record->local_path) { $file_path = $this->get_upload_dir() . '/' . $record->local_path; if (file_exists($file_path)) { unlink($file_path); } } // Delete record $wpdb->delete($this->db->media_table(), array('id' => $record->id)); } } } /** * Check if media needs to be downloaded * * @param object $existing Existing media record * @param array $new_data New media data from API * @return bool */ private function needs_download($existing, $new_data) { // No local file if (empty($existing->local_path)) { return true; } // File doesn't exist $file_path = $this->get_upload_dir() . '/' . $existing->local_path; if (!file_exists($file_path)) { return true; } // Media URL changed if ($existing->media_url !== ($new_data['MediaURL'] ?? null)) { return true; } return false; } /** * Download a media file * * @param int $media_id Media record ID * @return bool Success */ public function download_media($media_id) { global $wpdb; $media = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$this->db->media_table()} WHERE id = %d", $media_id )); if (!$media || empty($media->media_url)) { return false; } // Increment attempt counter $wpdb->update( $this->db->media_table(), array('download_attempts' => $media->download_attempts + 1), array('id' => $media_id) ); // Download file $response = wp_remote_get($media->media_url, array( 'timeout' => 60, 'stream' => false, )); if (is_wp_error($response)) { $this->logger->warning('Media download failed', array( 'media_id' => $media_id, 'error' => $response->get_error_message(), )); $wpdb->update( $this->db->media_table(), array('download_error' => $response->get_error_message()), array('id' => $media_id) ); return false; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { $wpdb->update( $this->db->media_table(), array('download_error' => "HTTP {$status_code}"), array('id' => $media_id) ); return false; } $body = wp_remote_retrieve_body($response); if (empty($body)) { $wpdb->update( $this->db->media_table(), array('download_error' => 'Empty response'), array('id' => $media_id) ); return false; } // Determine file extension from content type or URL $content_type = wp_remote_retrieve_header($response, 'content-type'); $extension = $this->get_extension_from_content_type($content_type, $media->media_url); // Create directory $listing_dir = $this->get_listing_dir($media->listing_key); if (!file_exists($listing_dir)) { wp_mkdir_p($listing_dir); } // Save file $filename = $media->media_order . '.' . $extension; $file_path = $listing_dir . '/' . $filename; if (file_put_contents($file_path, $body) === false) { $wpdb->update( $this->db->media_table(), array('download_error' => 'Failed to write file'), array('id' => $media_id) ); return false; } // Calculate relative path $prefix = substr($media->listing_key, 0, 2); $relative_path = $prefix . '/' . $media->listing_key . '/' . $filename; $local_url = $this->get_upload_url() . '/' . $relative_path; // Update record $wpdb->update( $this->db->media_table(), array( 'local_path' => $relative_path, 'local_url' => $local_url, 'file_size' => strlen($body), 'mime_type' => $content_type, 'downloaded_at' => current_time('mysql'), 'download_error' => null, ), array('id' => $media_id) ); return true; } /** * Get file extension from content type * * @param string $content_type Content type header * @param string $url Original URL as fallback * @return string File extension */ private function get_extension_from_content_type($content_type, $url) { // Extract main type from content-type header $content_type = strtolower(explode(';', $content_type)[0]); $map = array( 'image/jpeg' => 'jpg', 'image/jpg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif', 'image/webp' => 'webp', ); if (isset($map[$content_type])) { return $map[$content_type]; } // Fallback to URL extension $path = parse_url($url, PHP_URL_PATH); $ext = pathinfo($path, PATHINFO_EXTENSION); return $ext ?: 'jpg'; } /** * Delete all media for a property * * @param string $listing_key Listing key */ public function delete_property_media($listing_key) { global $wpdb; // Delete files $listing_dir = $this->get_listing_dir($listing_key); if (file_exists($listing_dir)) { $this->recursive_delete($listing_dir); } // Delete records $wpdb->delete( $this->db->media_table(), array('listing_key' => $listing_key) ); } /** * Recursively delete a directory * * @param string $dir Directory path */ private function recursive_delete($dir) { if (!is_dir($dir)) { return; } $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $file) { $path = $dir . '/' . $file; if (is_dir($path)) { $this->recursive_delete($path); } else { unlink($path); } } rmdir($dir); } /** * Get media for a listing * * @param string $listing_key Listing key * @return array Media records */ public function get_listing_media($listing_key) { global $wpdb; return $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$this->db->media_table()} WHERE listing_key = %s ORDER BY media_order ASC", $listing_key )); } /** * Get primary image URL for a listing * * @param string $listing_key Listing key * @return string|null Image URL */ public function get_primary_image($listing_key) { global $wpdb; $media = $wpdb->get_row($wpdb->prepare( "SELECT local_url, media_url FROM {$this->db->media_table()} WHERE listing_key = %s AND local_path IS NOT NULL ORDER BY media_order ASC LIMIT 1", $listing_key )); if ($media && $media->local_url) { return $media->local_url; } return null; } /** * Download pending media (for batch processing) * * @param int $limit Max media to download * @return array Stats */ public function download_pending($limit = 100) { global $wpdb; $pending = $wpdb->get_results($wpdb->prepare( "SELECT id FROM {$this->db->media_table()} WHERE local_path IS NULL AND media_url IS NOT NULL AND download_attempts < 3 LIMIT %d", $limit )); $stats = array( 'total' => count($pending), 'success' => 0, 'failed' => 0, ); foreach ($pending as $media) { if ($this->download_media($media->id)) { $stats['success']++; } else { $stats['failed']++; } } return $stats; } /** * Clean up orphaned media (files without database records) * * @return int Number of files deleted */ public function cleanup_orphaned_files() { $deleted = 0; $base_dir = $this->get_upload_dir(); if (!is_dir($base_dir)) { return 0; } // Iterate through prefix directories foreach (scandir($base_dir) as $prefix) { if ($prefix === '.' || $prefix === '..' || !is_dir($base_dir . '/' . $prefix)) { continue; } $prefix_dir = $base_dir . '/' . $prefix; // Iterate through listing directories foreach (scandir($prefix_dir) as $listing_key) { if ($listing_key === '.' || $listing_key === '..') { continue; } $listing_dir = $prefix_dir . '/' . $listing_key; if (!is_dir($listing_dir)) { continue; } // Check if listing exists in database global $wpdb; $exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE listing_key = %s", $listing_key )); if (!$exists) { $this->recursive_delete($listing_dir); $deleted++; } } } return $deleted; } }