From 72b932b25e05acde9520355546a2cbce6dfd5257 Mon Sep 17 00:00:00 2001 From: "Hanson.xyz Dev" Date: Mon, 15 Dec 2025 23:45:44 -0600 Subject: [PATCH] Add on-demand media URL refresh for expired MLS Grid tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MLS Grid media URLs expire after ~24 hours. Instead of running scheduled syncs, this adds on-demand refresh when images are requested: - Add is_url_expired() to parse expires timestamp from media URLs - Add refresh_media_urls() to fetch fresh URLs from API for one listing - Add get_property_media() API method using ListingId filter - Image endpoint checks URL expiration before fetching - If expired, refreshes URLs from API then proceeds with fetch This is more efficient than scheduled full syncs because: - Only refreshes URLs for listings actually being viewed - Zero overhead for unviewed listings - Scales naturally with traffic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../includes/class-mls-api-client.php | 36 +++++++++ .../includes/class-mls-image-endpoint.php | 29 +++++++ .../includes/class-mls-media-handler.php | 81 +++++++++++++++++++ 3 files changed, 146 insertions(+) diff --git a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-api-client.php b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-api-client.php index 951f1197..02e6c196 100644 --- a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-api-client.php +++ b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-api-client.php @@ -378,6 +378,42 @@ class MLS_API_Client { return $this->request('Property', $params); } + /** + * Get a single property by listing ID with media + * + * Used to refresh media URLs for a specific listing without + * fetching the entire dataset. + * + * Note: MLS Grid only allows filtering by ListingId (not ListingKey) + * for the Property resource. The caller must provide the listing_id. + * + * @param string $listing_id The MLS listing ID (not listing_key) + * @return array|WP_Error Property data with Media or error + */ + public function get_property_media($listing_id) { + $params = array(); + + $system = $this->options->get_originating_system(); + + // Filter by ListingId (MLS Grid only allows certain fields for filtering) + $params['$filter'] = "OriginatingSystemName eq '{$system}' and ListingId eq '{$listing_id}'"; + $params['$expand'] = 'Media'; + $params['$top'] = 1; + + $response = $this->request('Property', $params); + + if (is_wp_error($response)) { + return $response; + } + + // Return first result or null + if (isset($response['value']) && !empty($response['value'])) { + return $response['value'][0]; + } + + return null; + } + /** * Get next page of results * diff --git a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-image-endpoint.php b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-image-endpoint.php index 9c04d080..32776164 100644 --- a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-image-endpoint.php +++ b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-image-endpoint.php @@ -137,6 +137,11 @@ class MLS_Image_Endpoint { /** * Get source image path, fetching from MLS if needed + * + * This method handles: + * 1. Returning cached local images if available + * 2. Checking if media URL has expired and refreshing if needed + * 3. Fetching images from MLS Grid on demand */ private function get_source_image($listing_key, $index) { global $wpdb; @@ -165,6 +170,30 @@ class MLS_Image_Endpoint { } } + // Check if the media URL has expired before trying to fetch + if ($this->media_handler->is_url_expired($media->media_url)) { + $this->logger->debug('Media URL expired, refreshing', array( + 'listing_key' => $listing_key, + 'index' => $index, + )); + + // Refresh media URLs from API + if ($this->media_handler->refresh_media_urls($listing_key)) { + // Re-fetch the record with fresh URL + $media = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$db->media_table()} + WHERE listing_key = %s AND media_order = %d + LIMIT 1", + $listing_key, + $index + )); + + if (!$media) { + return null; + } + } + } + // Fetch from MLS on demand $url = $this->media_handler->get_image_url($media, true); if (!$url) { diff --git a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-media-handler.php b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-media-handler.php index fd98adb0..44893da4 100644 --- a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-media-handler.php +++ b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-media-handler.php @@ -494,6 +494,87 @@ class MLS_Media_Handler { )); } + /** + * Check if a media URL has expired + * + * MLS Grid media URLs contain an 'expires' parameter with Unix timestamp. + * Returns true if the URL is expired or will expire within the buffer time. + * + * @param string $media_url The media URL to check + * @param int $buffer_seconds Buffer time before actual expiration (default: 300 = 5 min) + * @return bool True if expired or expiring soon + */ + public function is_url_expired($media_url, $buffer_seconds = 300) { + if (empty($media_url)) { + return true; + } + + // Extract expires parameter from URL + if (preg_match('/expires=(\d+)/', $media_url, $matches)) { + $expires = (int) $matches[1]; + return (time() + $buffer_seconds) >= $expires; + } + + // If no expires param found, assume expired to be safe + return true; + } + + /** + * Refresh media URLs for a listing from the API + * + * Fetches fresh media data from MLS Grid and updates the database. + * This is used when cached URLs have expired. + * + * Note: MLS Grid API only allows filtering by ListingId, not ListingKey. + * This method looks up the listing_id from the local database first. + * + * @param string $listing_key Listing key + * @return bool True on success, false on failure + */ + public function refresh_media_urls($listing_key) { + global $wpdb; + + $plugin = mls_plugin(); + $api_client = $plugin->get_api_client(); + + // Look up listing_id from database (MLS Grid API requires ListingId for filtering) + $listing_id = $wpdb->get_var($wpdb->prepare( + "SELECT listing_id FROM {$this->db->properties_table()} WHERE listing_key = %s", + $listing_key + )); + + if (!$listing_id) { + $this->logger->warning('Cannot refresh media: listing_id not found', array( + 'listing_key' => $listing_key, + )); + return false; + } + + // Fetch property with media from API using ListingId + $property = $api_client->get_property_media($listing_id); + + if (is_wp_error($property) || !$property) { + $this->logger->warning('Failed to refresh media URLs', array( + 'listing_key' => $listing_key, + 'listing_id' => $listing_id, + 'error' => is_wp_error($property) ? $property->get_error_message() : 'Property not found', + )); + return false; + } + + // Update media records with fresh URLs + if (isset($property['Media']) && is_array($property['Media'])) { + $this->sync_property_media($listing_key, $property['Media']); + $this->logger->debug('Refreshed media URLs', array( + 'listing_key' => $listing_key, + 'media_count' => count($property['Media']), + )); + return true; + } + + return false; + } + /** * Clean up orphaned media files (files without database records) *