Prechádzať zdrojové kódy

RSS: emit <enclosure> for BitTorrent attachments (#9553)

* feat: add the <enclosure/> field to enhance compatibility with BitTorrent downloaders

Closes #6227

Changes proposed in this pull request:

- Emit an additional RSS 2.0 `<enclosure>` element for BitTorrent attachments in the
  generated RSS output (`p/api/query.php?...&f=rss` and the per-feed RSS view).
  The existing `<media:content>` output is unchanged.
- Emit it only for `application/x-bittorrent`, `magnet:` and `.torrent` URLs, and skip
  images / audio / video. Since 1.24 thumbnails are prepended to the enclosure list, a
  naive implementation would duplicate cover images (and podcast media) as attachments.
- Always set `type="application/x-bittorrent"`, and copy `length` when the feed provides it.
- Add unit tests in `tests/app/Views/rssViewTest.php`: torrent, magnet without type,
  image attachment skipped.

* fix(rss): emit <enclosure> only for BitTorrent attachments

The previous patch emitted <enclosure> for every enclosure. Since FreshRSS
1.24, thumbnails are prepended to the enclosures list, so cover images and
podcast audio were also duplicated as RSS attachments. Emit the tag only for
application/x-bittorrent, magnet: or .torrent URLs, and always set
type="application/x-bittorrent" so clients such as qBittorrent can use it.

Add three rssViewTest cases: torrent, magnet without type, image skipped.

* Improve regex

* Minor whitespace

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
CaoMeiYouRen 3 hodín pred
rodič
commit
8e281292f9
2 zmenil súbory, kde vykonal 39 pridanie a 0 odobranie
  1. 9 0
      app/views/index/rss.phtml
  2. 30 0
      tests/app/Views/rssViewTest.php

+ 9 - 0
app/views/index/rss.phtml

@@ -99,6 +99,15 @@ foreach ($this->entries as $item) {
 						. (empty($enclosure['title']) ? '' : '<media:title type="html">' . $enclosure['title'] . '</media:title>')
 						. $mediaCredits
 						. '</media:content>', "\n";
+					// RSS 2.0 <enclosure> for BitTorrent clients that ignore Media RSS (e.g. qBittorrent)
+					// https://www.rssboard.org/rss-specification#ltenclosuregtSubelementOfLtitemgt
+					$enclosureUrl = (string)$enclosure['url'];
+					$enclosureType = strtolower((string)($enclosure['type'] ?? ''));
+					if ($enclosureType === 'application/x-bittorrent' || preg_match('~^magnet:|\.torrent(\?|#|$)~i', $enclosureUrl) === 1) {
+						echo "\t\t\t", '<enclosure url="' . $enclosureUrl . '" type="application/x-bittorrent"'
+							. (empty($enclosure['length']) ? '' : ' length="' . $enclosure['length'] . '"')
+							. '/>', "\n";
+					}
 				}
 			?>
 			<description><![CDATA[<?php

+ 30 - 0
tests/app/Views/rssViewTest.php

@@ -46,4 +46,34 @@ final class rssViewTest extends \PHPUnit\Framework\TestCase {
 		]);
 		self::assertStringContainsString('<media:credit>Alice</media:credit>', $rss);
 	}
+
+	/** BitTorrent enclosures must also be exposed as RSS 2.0 `<enclosure>` for clients that ignore Media RSS */
+	public function test_rss_enclosureForBitTorrent(): void {
+		$rss = self::renderEntryWithEnclosure([
+			'url' => 'https://example.net/file.torrent',
+			'type' => 'application/x-bittorrent',
+			'length' => 12216,
+		]);
+		self::assertStringContainsString(
+			'<enclosure url="https://example.net/file.torrent" type="application/x-bittorrent" length="12216"/>', $rss);
+	}
+
+	/** A magnet link is a torrent even without a MIME type */
+	public function test_rss_enclosureForMagnetWithoutType(): void {
+		$rss = self::renderEntryWithEnclosure([
+			'url' => 'magnet:?xt=urn:btih:0123456789abcdef',
+		]);
+		self::assertStringContainsString(
+			'<enclosure url="magnet:?xt=urn:btih:0123456789abcdef" type="application/x-bittorrent"/>', $rss);
+	}
+
+	/** Images / audio / video must not be duplicated as RSS 2.0 `<enclosure>` */
+	public function test_rss_noEnclosureForImageAttachment(): void {
+		$rss = self::renderEntryWithEnclosure([
+			'url' => 'https://example.net/cover.jpg',
+			'type' => 'image/jpeg',
+			'medium' => 'image',
+		]);
+		self::assertStringNotContainsString('<enclosure', $rss);
+	}
 }