Просмотр исходного кода

Show only one representation per media:group (#9009)

* Show only one representation per media:group

A <media:group> gathers <media:content> elements that are effectively
the same content, yet different representations (e.g. several
resolutions of the same video), but every representation was stored and
rendered as its own enclosure. For instance, articles from PeerTube
feeds showed the same video five times.

When loading entries, keep only one <media:content> per <media:group>:
the one marked with isDefault="true", or the first one otherwise.
https://www.rssboard.org/media-rss#media-group

Fixes #8569

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update SimplePie to include FreshRSS/simplepie#86

Regenerate the vendored SimplePie from FreshRSS/simplepie@freshrss via
`composer update --no-autoloader`, replacing the hand-patched Item.php
with the merged upstream version.

Includes <https://github.com/FreshRSS/simplepie/pull/86>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove redundant test
Already covered by https://github.com/FreshRSS/simplepie/pull/86

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
TowyTowy 1 день назад
Родитель
Сommit
b12ec4fe77
3 измененных файлов с 69 добавлено и 6 удалено
  1. 4 2
      app/Models/Feed.php
  2. 1 1
      lib/composer.json
  3. 64 3
      lib/simplepie/simplepie/src/Item.php

+ 4 - 2
app/Models/Feed.php

@@ -846,8 +846,10 @@ class FreshRSS_Feed extends Minz_Model {
 			}
 
 			$attributeEnclosures = [];
-			if (!empty($item->get_enclosures())) {
-				foreach ($item->get_enclosures() as $enclosure) {
+			// Keep only one representation per `<media:group>` (the `isDefault` one, or the first one)
+			$enclosures = $item->get_enclosures(excludeMediaGroupAlternatives: true);
+			if (!empty($enclosures)) {
+				foreach ($enclosures as $enclosure) {
 					$elink = $enclosure->get_link();
 					if ($elink != '') {
 						$etitle = $enclosure->get_title() ?? '';

+ 1 - 1
lib/composer.json

@@ -18,7 +18,7 @@
 		"marienfressinaud/lib_opml": "dev-main#f0e850b6394af90b898daf0e65fcc7363457b844",
 		"phpgt/cssxpath": "v1.5.0",
 		"phpmailer/phpmailer": "7.1.1",
-		"simplepie/simplepie": "dev-freshrss#5c4cd94907c066f1db5ccd9c6d4610ecba114e39"
+		"simplepie/simplepie": "dev-freshrss#7acfcf7c7195d4c1af626ecf84d1f433564c0211"
 	},
 	"config": {
 		"sort-packages": true,

+ 64 - 3
lib/simplepie/simplepie/src/Item.php

@@ -932,9 +932,11 @@ class Item implements RegistryAware
      * @since Beta 2
      * @todo Add support for end-user defined sorting of enclosures by type/handler (so we can prefer the faster-loading FLV over MP4).
      * @todo If an element exists at a level, but its value is empty, we should fall back to the value from the parent (if it exists).
+     * @param bool $excludeMediaGroupAlternatives When true, only one `<media:content>` is kept per `<media:group>`
+     *   (the one marked `isDefault="true"`, or the first one), skipping the redundant alternative representations.
      * @return \SimplePie\Enclosure[]|null List of \SimplePie\Enclosure items
      */
-    public function get_enclosures()
+    public function get_enclosures(bool $excludeMediaGroupAlternatives = false)
     {
         if (!isset($this->data['enclosures'])) {
             $this->data['enclosures'] = [];
@@ -2294,13 +2296,72 @@ class Item implements RegistryAware
 
             $this->data['enclosures'] = array_values(array_unique($this->data['enclosures']));
         }
-        if (!empty($this->data['enclosures'])) {
-            return $this->data['enclosures'];
+
+        $enclosures = $this->data['enclosures'];
+        if ($excludeMediaGroupAlternatives && $enclosures !== []) {
+            $alternativeUrls = $this->get_media_group_alternative_urls();
+            if ($alternativeUrls !== []) {
+                $enclosures = array_values(array_filter(
+                    $enclosures,
+                    static function (\SimplePie\Enclosure $enclosure) use ($alternativeUrls): bool {
+                        return empty($alternativeUrls[$enclosure->get_link() ?? '']);
+                    }
+                ));
+            }
+        }
+        if (!empty($enclosures)) {
+            return $enclosures;
         }
 
         return null;
     }
 
+    /**
+     * List the URLs of the redundant `<media:content>` alternatives inside `<media:group>` elements.
+     *
+     * A `<media:group>` gathers different representations of the same content (e.g. several video
+     * resolutions), so only one element per group should normally be shown: the one marked with
+     * `isDefault="true"`, or the first one otherwise.
+     *
+     * @link https://www.rssboard.org/media-rss#media-group
+     * @return array<string, true> URLs (as keys) of the media alternatives that should be skipped
+     */
+    private function get_media_group_alternative_urls(): array
+    {
+        $alternativeUrls = [];
+        foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'group') as $group) {
+            $contents = $group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'] ?? null;
+            if (!is_array($contents) || count($contents) < 2) {
+                continue;
+            }
+            $urls = [];
+            $defaultUrl = null;
+            foreach ($contents as $content) {
+                if (!isset($content['attribs']['']['url'])) {
+                    continue;
+                }
+                // Same URL processing as when building the enclosures above, so the URLs can be compared
+                $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($content));
+                if ($url === '') {
+                    continue;
+                }
+                $urls[] = $url;
+                if ($defaultUrl === null && ($content['attribs']['']['isDefault'] ?? '') === 'true') {
+                    $defaultUrl = $url;
+                }
+            }
+            if ($defaultUrl === null) {
+                $defaultUrl = $urls[0] ?? '';
+            }
+            foreach ($urls as $url) {
+                if ($url !== $defaultUrl) {
+                    $alternativeUrls[$url] = true;
+                }
+            }
+        }
+        return $alternativeUrls;
+    }
+
     /**
      * Get the latitude coordinates for the item
      *