Browse Source

Fix JellyStat poster metadata popups with ID sanitization

- Add sanitizeId() JavaScript function to handle problematic IDs
- Apply ID sanitization to both movies and TV shows poster sections
- Use sanitized IDs for HTML element IDs and CSS selectors
- Preserve original IDs in data-key attributes for API calls
- Enhance getJellyStatMetadata() to return real metadata from JellyStat
- Add getPosterUrl() helper function for poster URL generation
- Improve debugging with console logging for poster URLs and IDs

This fixes issues where metadata popups wouldn't appear for items with
IDs containing special characters or starting with digits, ensuring
consistent popup functionality across all poster content.
mgomon 8 months ago
parent
commit
99197cdf2c
1 changed files with 178 additions and 33 deletions
  1. 178 33
      api/homepage/jellystat.php

+ 178 - 33
api/homepage/jellystat.php

@@ -204,31 +204,123 @@ trait JellyStatHomepageItem
             return false;
         }
         
-        // For now, return basic metadata from the key (which is the item ID)
-        // In the future this could be enhanced to fetch full metadata from JellyStat API
-        $metadata = [
-            'guid' => (string)$key,
-            'summary' => 'This item data is from JellyStat analytics. Click the JellyStat button to view detailed statistics and analytics for this content.',
-            'rating' => '0',
-            'duration' => '0',
-            'originallyAvailableAt' => '',
-            'year' => '',
-            'tagline' => 'JellyStat Analytics',
-            'genres' => [], // Empty array will be handled as string in frontend
-            'actors' => []  // Empty array will be handled as string in frontend
-        ];
+        // Get JellyStat data to find the item details
+        $url = $this->config['jellyStatURL'] ?? '';
+        $token = $this->config['jellyStatApikey'] ?? '';
+        $days = intval($this->config['homepageJellyStatDays'] ?? 30);
         
-        // Create a mock item structure similar to Emby's format
-        $item = [
-            'uid' => (string)$key,
-            'title' => 'JellyStat Item',
-            'type' => 'jellystat',
-            'nowPlayingImageURL' => 'plugins/images/homepage/no-np.png', // Safe fallback image
-            'address' => $this->qualifyURL($this->config['jellyStatURL'] ?? ''),
-            'tabName' => 'jellystat',
-            'openTab' => 'true',
-            'metadata' => $metadata
-        ];
+        if (empty($url) || empty($token)) {
+            $this->setAPIResponse('error', 'JellyStat URL or API key not configured', 500);
+            return false;
+        }
+        
+        // Fetch all JellyStat data
+        $disableCert = $this->config['jellyStatDisableCertCheck'] ?? false;
+        $customCert = $this->config['jellyStatUseCustomCertificate'] ?? false;
+        $internalUrl = $this->config['jellyStatInternalURL'] ?? '';
+        $apiUrl = !empty($internalUrl) ? $internalUrl : $url;
+        $options = $this->requestOptions($apiUrl, null, $disableCert, $customCert);
+        $baseUrl = $this->qualifyURL($apiUrl);
+        
+        // Try to find the item in cached data if available
+        $stats = null;
+        try {
+            $startDate = date('Y-m-d', strtotime("-{$days} days"));
+            $allHistoryResults = $this->fetchAllJellyStatHistory($baseUrl, $token, $startDate, $options);
+            
+            if (!empty($allHistoryResults)) {
+                $processedData = $this->processJellyStatHistory($allHistoryResults);
+                
+                // Look for the item in each content type
+                foreach (['movies', 'shows', 'music'] as $type) {
+                    foreach ($processedData[$type] as $item) {
+                        if ($item['id'] == $key) {
+                            $stats = $item;
+                            break 2; // Exit both loops if found
+                        }
+                    }
+                }
+            }
+        } catch (Exception $e) {
+            $this->writeLog('error', 'Failed to fetch JellyStat metadata: ' . $e->getMessage());
+        }
+        
+        if (!$stats) {
+            // Fallback to basic metadata if item not found
+            $metadata = [
+                'guid' => (string)$key,
+                'summary' => 'This item data is from JellyStat analytics. No additional metadata available.',
+                'rating' => '0',
+                'duration' => '0',
+                'originallyAvailableAt' => '',
+                'year' => '',
+                'tagline' => 'JellyStat Analytics',
+                'genres' => [],
+                'actors' => []
+            ];
+            
+            $item = [
+                'uid' => (string)$key,
+                'title' => 'JellyStat Item',
+                'type' => 'jellystat',
+                'nowPlayingImageURL' => 'plugins/images/homepage/no-np.png',
+                'address' => $this->qualifyURL($this->config['jellyStatURL'] ?? ''),
+                'tabName' => 'jellystat',
+                'openTab' => 'true',
+                'metadata' => $metadata
+            ];
+        } else {
+            // Use actual item data from JellyStat
+            $runtime = '';
+            if (!empty($stats['total_duration'])) {
+                $hours = floor($stats['total_duration'] / 3600);
+                $minutes = floor(($stats['total_duration'] % 3600) / 60);
+                if ($hours > 0) {
+                    $runtime = sprintf('%dh %dm', $hours, $minutes);
+                } else {
+                    $runtime = sprintf('%dm', $minutes);
+                }
+            }
+            
+            $contentType = ucfirst($stats['type'] ?? 'content');
+            $playCount = intval($stats['play_count'] ?? 0);
+            
+            $metadata = [
+                'guid' => (string)$key,
+                'summary' => sprintf(
+                    'This %s has been played %d time%s through your Jellyfin/Emby server. '
+                    . 'Total watch time: %s. Click the JellyStat button to view detailed analytics.',
+                    $stats['type'] === 'show' ? 'TV series' : ($stats['type'] ?? 'content'),
+                    $playCount,
+                    $playCount == 1 ? '' : 's',
+                    $runtime ?: 'Unknown'
+                ),
+                'rating' => '0',
+                'duration' => (string)($stats['total_duration'] ?? '0'),
+                'originallyAvailableAt' => !empty($stats['first_played']) ? date('Y-m-d', strtotime($stats['first_played'])) : '',
+                'year' => (string)($stats['year'] ?? ''),
+                'tagline' => sprintf('Most Watched %s • %d Plays', $contentType, $playCount),
+                'genres' => [],
+                'actors' => []
+            ];
+            
+            $posterUrl = '';
+            if (!empty($stats['poster_path']) && !empty($stats['id']) && !empty($stats['server_id'])) {
+                // Get poster URL if available
+                $posterUrl = $this->getPosterUrl($stats['poster_path'], $stats['id'], $stats['server_id']);
+            }
+            
+            $item = [
+                'uid' => (string)$key,
+                'title' => $stats['title'] ?? 'Unknown Title',
+                'type' => 'jellystat',
+                'nowPlayingImageURL' => $posterUrl ?: 'plugins/images/homepage/no-np.png',
+                'address' => $this->qualifyURL($this->config['jellyStatURL'] ?? ''),
+                'tabName' => 'jellystat',
+                'openTab' => 'true',
+                'metadata' => $metadata
+            ];
+        }
         
         $api['content'][] = $item;
         $this->setAPIResponse('success', null, 200, $api);
@@ -373,6 +465,16 @@ trait JellyStatHomepageItem
             }
         }
         
+        // Helper function to sanitize IDs for use in HTML attributes and CSS selectors
+        function sanitizeId(id) {
+            if (!id) return 'jellystat-unknown';
+            // Convert to string and replace problematic characters
+            return String(id)
+                .replace(/[^a-zA-Z0-9\-_]/g, '_') // Replace special chars with underscores
+                .replace(/^[0-9]/, '_$&') // Prefix with underscore if starts with number
+                .toLowerCase();
+        }
+        
         // Helper function to generate poster URLs from JellyStat/Jellyfin
         function getPosterUrl(posterPath, itemId, serverId) {
             console.log("getPosterUrl called with:", {posterPath, itemId, serverId});
@@ -407,7 +509,7 @@ trait JellyStatHomepageItem
                 // Format: /proxy/Items/Images/Primary?id={itemId}&fillWidth=200&quality=90
                 var baseUrl = jellyStatUrl.replace(/\/+$/, ""); // Remove trailing slashes
                 
-                var apiUrl = baseUrl + "/proxy/Items/Images/Primary?id=" + itemId + "&fillWidth=200&quality=90";
+                var apiUrl = baseUrl + "/proxy/Items/Images/Primary?id=" + encodeURIComponent(itemId) + "&fillWidth=200&quality=90";
                 console.log("Generated JellyStat proxy image URL:", apiUrl);
                 return apiUrl;
             }
@@ -647,8 +749,12 @@ trait JellyStatHomepageItem
                     var year = movie.year && movie.year !== "N/A" ? movie.year : "";
                     var title = movie.title || "Unknown Movie";
                     
+                    // Use sanitized ID for DOM elements but original ID for data attributes
+                    var sanitizedId = sanitizeId(movie.id);
+                    console.log("Using sanitized ID:", sanitizedId, "for original ID:", movie.id);
+                    
                     html += "<div style=\"display: inline-block; margin: 0 10px 0 0; width: 150px; vertical-align: top;\">";
-                    html += "<div class=\"poster-card metadata-get\" style=\"position: relative; width: 150px; height: 225px; transition: transform 0.2s ease; cursor: pointer;\" data-source=\"jellystat\" data-key=\"" + movie.id + "\" data-uid=\"" + movie.id + "\">";
+                    html += "<div class=\"poster-card metadata-get\" style=\"position: relative; width: 150px; height: 225px; transition: transform 0.2s ease; cursor: pointer;\" data-source=\"jellystat\" data-key=\"" + movie.id + "\" data-uid=\"" + sanitizedId + "\">";
                     
                     // Poster image container
                     html += "<div class=\"poster-image\" style=\"position: relative; width: 150px; height: 225px; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.3);\">";
@@ -683,9 +789,9 @@ trait JellyStatHomepageItem
                     
                     html += "</div>";
                     
-                    // Add metadata popup elements (Organizr style)
-                    html += "<div id=\"" + movie.id + "-metadata-div\" class=\"white-popup mfp-with-anim mfp-hide\">";
-                    html += "<div class=\"col-md-8 col-md-offset-2 " + movie.id + "-metadata-info\"></div>";
+                    // Add metadata popup elements (Organizr style) using sanitized ID
+                    html += "<div id=\"" + sanitizedId + "-metadata-div\" class=\"white-popup mfp-with-anim mfp-hide\">";
+                    html += "<div class=\"col-md-8 col-md-offset-2 " + sanitizedId + "-metadata-info\"></div>";
                     html += "</div>";
                     
                     html += "</div>";
@@ -712,8 +818,12 @@ trait JellyStatHomepageItem
                     var year = show.year && show.year !== "N/A" ? show.year : "";
                     var title = show.title || "Unknown Show";
                     
+                    // Use sanitized ID for DOM elements but original ID for data attributes
+                    var sanitizedId = sanitizeId(show.id);
+                    console.log("Using sanitized ID:", sanitizedId, "for original ID:", show.id);
+                    
                     html += "<div style=\"display: inline-block; margin: 0 10px 0 0; width: 150px; vertical-align: top;\">";
-                    html += "<div class=\"poster-card metadata-get\" style=\"position: relative; width: 150px; height: 225px; transition: transform 0.2s ease; cursor: pointer;\" data-source=\"jellystat\" data-key=\"" + show.id + "\" data-uid=\"" + show.id + "\">";
+                    html += "<div class=\"poster-card metadata-get\" style=\"position: relative; width: 150px; height: 225px; transition: transform 0.2s ease; cursor: pointer;\" data-source=\"jellystat\" data-key=\"" + show.id + "\" data-uid=\"" + sanitizedId + "\">";
                     
                     // Poster image container
                     html += "<div class=\"poster-image\" style=\"position: relative; width: 150px; height: 225px; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.3);\">";
@@ -748,9 +858,9 @@ trait JellyStatHomepageItem
                     
                     html += "</div>";
                     
-                    // Add metadata popup elements (Organizr style) 
-                    html += "<div id=\"" + show.id + "-metadata-div\" class=\"white-popup mfp-with-anim mfp-hide\">";
-                    html += "<div class=\"col-md-8 col-md-offset-2 " + show.id + "-metadata-info\"></div>";
+                    // Add metadata popup elements (Organizr style) using sanitized ID
+                    html += "<div id=\"" + sanitizedId + "-metadata-div\" class=\"white-popup mfp-with-anim mfp-hide\">";
+                    html += "<div class=\"col-md-8 col-md-offset-2 " + sanitizedId + "-metadata-info\"></div>";
                     html += "</div>";
                     
                     html += "</div>";
@@ -1034,6 +1144,41 @@ trait JellyStatHomepageItem
         return $allResults;
     }
     
+    /**
+     * Generate poster URL from JellyStat API
+     */
+    private function getPosterUrl($posterPath, $itemId, $serverId)
+    {
+        // Use main URL for poster display (not internal URL)
+        $jellyStatUrl = $this->qualifyURL($this->config['jellyStatURL'] ?? '');
+        
+        if (!$jellyStatUrl) {
+            return null;
+        }
+        
+        // If we have a poster path, process it
+        if ($posterPath) {
+            // If its already an absolute URL, use it directly
+            if (strpos($posterPath, 'http://') === 0 || strpos($posterPath, 'https://') === 0) {
+                return $posterPath;
+            }
+            // If its a relative path starting with /, prepend the JellyStat URL
+            if (strpos($posterPath, '/') === 0) {
+                return rtrim($jellyStatUrl, '/') . $posterPath;
+            }
+        }
+        
+        // If we have itemId, try to generate JellyStat image proxy URL
+        if ($itemId) {
+            // JellyStat uses /proxy/Items/Images/Primary endpoint
+            // Format: /proxy/Items/Images/Primary?id={itemId}&fillWidth=200&quality=90
+            $baseUrl = rtrim($jellyStatUrl, '/');
+            return $baseUrl . '/proxy/Items/Images/Primary?id=' . urlencode($itemId) . '&fillWidth=200&quality=90';
+        }
+        
+        return null;
+    }
+    
     /**
      * Get human-readable label for collection type
      */