فهرست منبع

XPath ability to define the UID manually (#4507)

* XPath ability to define the UID manually

* Fix error in i18n
Alexandre Alapetite 3 سال پیش
والد
کامیت
1603c10bba

+ 1 - 0
app/Controllers/feedController.php

@@ -198,6 +198,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 				if (Minz_Request::param('xPathItemTimestamp', '') != '') $xPathSettings['itemTimestamp'] = Minz_Request::param('xPathItemTimestamp', '', true);
 				if (Minz_Request::param('xPathItemThumbnail', '') != '') $xPathSettings['itemThumbnail'] = Minz_Request::param('xPathItemThumbnail', '', true);
 				if (Minz_Request::param('xPathItemCategories', '') != '') $xPathSettings['itemCategories'] = Minz_Request::param('xPathItemCategories', '', true);
+				if (Minz_Request::param('xPathItemUid', '') != '') $xPathSettings['itemUid'] = Minz_Request::param('xPathItemUid', '', true);
 				if (!empty($xPathSettings)) {
 					$attributes['xpath'] = $xPathSettings;
 				}

+ 1 - 0
app/Controllers/subscriptionController.php

@@ -204,6 +204,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 				if (Minz_Request::param('xPathItemTimestamp', '') != '') $xPathSettings['itemTimestamp'] = Minz_Request::param('xPathItemTimestamp', '', true);
 				if (Minz_Request::param('xPathItemThumbnail', '') != '') $xPathSettings['itemThumbnail'] = Minz_Request::param('xPathItemThumbnail', '', true);
 				if (Minz_Request::param('xPathItemCategories', '') != '') $xPathSettings['itemCategories'] = Minz_Request::param('xPathItemCategories', '', true);
+				if (Minz_Request::param('xPathItemUid', '') != '') $xPathSettings['itemUid'] = Minz_Request::param('xPathItemUid', '', true);
 				if (!empty($xPathSettings)) {
 					$feed->_attributes('xpath', $xPathSettings);
 				}

+ 8 - 1
app/Models/Feed.php

@@ -611,6 +611,7 @@ class FreshRSS_Feed extends Minz_Model {
 		$xPathItemTimestamp = $xPathSettings['itemTimestamp'] ?? '';
 		$xPathItemThumbnail = $xPathSettings['itemThumbnail'] ?? '';
 		$xPathItemCategories = $xPathSettings['itemCategories'] ?? '';
+		$xPathItemUid = $xPathSettings['itemUid'] ?? '';
 		if ($xPathItem == '') {
 			return null;
 		}
@@ -657,8 +658,14 @@ class FreshRSS_Feed extends Minz_Model {
 						}
 					}
 				}
-				if ($item['title'] . $item['content'] . $item['link'] != '') {
+				if ($xPathItemUid != '') {
+					$item['guid'] = @$xpath->evaluate('normalize-space(' . $xPathItemUid . ')', $node);
+				}
+				if (empty($item['guid'])) {
 					$item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']);
+				}
+
+				if ($item['title'] . $item['content'] . $item['link'] != '') {
 					$item = Minz_Helper::htmlspecialchars_utf8($item);
 					$view->entries[] = FreshRSS_Entry::fromArray($item);
 				}

+ 1 - 0
app/Services/ImportService.php

@@ -179,6 +179,7 @@ class FreshRSS_Import_Service {
 						case 'xPathItemTimestamp': $xPathSettings['itemTimestamp'] = $value['value']; break;
 						case 'xPathItemThumbnail': $xPathSettings['itemThumbnail'] = $value['value']; break;
 						case 'xPathItemCategories': $xPathSettings['itemCategories'] = $value['value']; break;
+						case 'xPathItemUid': $xPathSettings['itemUid'] = $value['value']; break;
 					}
 				}
 			}

+ 5 - 1
app/i18n/cz/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/de/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/en-us/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// IGNORE
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// IGNORE
 				),
-				'item_categories' => 'items tags',	// IGNORE
+				'item_categories' => 'item tags',	// IGNORE
 				'item_content' => array(
 					'_' => 'item content',	// IGNORE
 					'help' => 'Example to take the full item: <code>.</code>',	// IGNORE
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// IGNORE
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// IGNORE
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// IGNORE
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// IGNORE
 					'help' => 'Example: <code>descendant::a/@href</code>',	// IGNORE

+ 5 - 1
app/i18n/en/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',
 				),
-				'item_categories' => 'items tags',
+				'item_categories' => 'item tags',
 				'item_content' => array(
 					'_' => 'item content',
 					'help' => 'Example to take the full item: <code>.</code>',
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',
 					'help' => 'Example: <code>descendant::a/@href</code>',

+ 5 - 1
app/i18n/es/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 4 - 0
app/i18n/fr/sub.php

@@ -104,6 +104,10 @@ return array(
 					'_' => 'titre de l’article',
 					'help' => 'Utiliser en particulier l’<a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">axe XPath</a> <code>descendant::</code> comme <code>descendant::h2</code>',
 				),
+				'item_uid' => array(
+					'_' => 'identifiant unique de l’article',
+					'help' => 'Optionnel. Exemple : <code>descendant::div/@data-uri</code>',
+				),
 				'item_uri' => array(
 					'_' => 'lien (URL) de l’article',
 					'help' => 'Exemple : <code>descendant::a/@href</code>',

+ 5 - 1
app/i18n/he/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/it/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/ja/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/ko/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/nl/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/oc/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/pl/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/pt-br/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 4 - 0
app/i18n/ru/sub.php

@@ -104,6 +104,10 @@ return array(
 					'_' => 'заголовка элемента',
 					'help' => 'Используйте, в частности, <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">ось XPath</a> <code>descendant::</code>, наподобие <code>descendant::h2</code>',
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'ссылки элемента (URL)',
 					'help' => 'Пример: <code>descendant::a/@href</code>',

+ 5 - 1
app/i18n/sk/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 5 - 1
app/i18n/tr/sub.php

@@ -87,7 +87,7 @@ return array(
 					'_' => 'item author',	// TODO
 					'help' => 'Can also be a static string. Example: <code>"Anonymous"</code>',	// TODO
 				),
-				'item_categories' => 'items tags',	// TODO
+				'item_categories' => 'item tags',	// TODO
 				'item_content' => array(
 					'_' => 'item content',	// TODO
 					'help' => 'Example to take the full item: <code>.</code>',	// TODO
@@ -104,6 +104,10 @@ return array(
 					'_' => 'item title',	// TODO
 					'help' => 'Use in particular the <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> like <code>descendant::h2</code>',	// TODO
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',	// TODO
 					'help' => 'Example: <code>descendant::a/@href</code>',	// TODO

+ 4 - 0
app/i18n/zh-cn/sub.php

@@ -104,6 +104,10 @@ return array(
 					'_' => '文章标题',
 					'help' => '特别是用 <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath 轴</a> <code>descendant::</code> 像 <code>descendant::h2</code>',
 				),
+				'item_uid' => array(
+					'_' => 'item unique ID',	// TODO
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',	// TODO
+				),
 				'item_uri' => array(
 					'_' => '文章链接 (URL)',
 					'help' => '例: <code>descendant::a/@href</code>',

+ 1 - 0
app/views/helpers/export/opml.phtml

@@ -28,6 +28,7 @@ function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
 			$outline['frss:xPathItemTimestamp'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimestamp'] ?? null];
 			$outline['frss:xPathItemThumbnail'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemThumbnail'] ?? null];
 			$outline['frss:xPathItemCategories'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemCategories'] ?? null];
+			$outline['frss:xPathItemUid'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemUid'] ?? null];
 		}
 		if (!empty($feed->filtersAction('read'))) {
 			$filters = '';

+ 8 - 0
app/views/helpers/feed/update.phtml

@@ -475,6 +475,14 @@
 						data-leave-validation="<?= $xpath['itemCategories'] ?? '' ?>"><?= $xpath['itemCategories'] ?? '' ?></textarea>
 				</div>
 			</div>
+			<div class="form-group">
+				<label class="group-name" for="xPathItemUid"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
+					<?= _t('sub.feed.kind.html_xpath.item_uid') ?></label>
+				<div class="group-controls">
+					<textarea class="valid-xpath w100" name="xPathItemUid" id="xPathItemUid" rows="2" cols="64" spellcheck="false"
+						data-leave-validation="<?= $xpath['itemUid'] ?? '' ?>"><?= $xpath['itemUid'] ?? '' ?></textarea>
+				</div>
+			</div>
 		</fieldset>
 		<div class="form-group form-actions">
 			<div class="group-controls">

+ 7 - 0
app/views/subscription/add.phtml

@@ -147,6 +147,13 @@
 						<textarea class="valid-xpath" name="xPathItemCategories" id="xPathItemCategories" rows="2" cols="64" spellcheck="false"></textarea>
 					</div>
 				</div>
+				<div class="form-group">
+					<label class="group-name" for="xPathItemUid"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
+						<?= _t('sub.feed.kind.html_xpath.item_uid') ?></label>
+					<div class="group-controls">
+						<textarea class="valid-xpath" name="xPathItemUid" id="xPathItemUid" rows="2" cols="64" spellcheck="false"></textarea>
+					</div>
+				</div>
 			</fieldset>
 		</details>
 

+ 1 - 0
docs/en/developers/OPML.md

@@ -39,6 +39,7 @@ The following attributes are using similar naming conventions than [RSS-Bridge](
 * `frss:xPathItemThumbnail`: XPath expression for extracting an item’s thumbnail (image) URL from the item context.
 	* Example: `descendant::img/@src`
 * `frss:xPathItemCategories`: XPath expression for extracting a list of categories (tags) from the item context.
+* `frss:xPathItemUid`: XPath expression for extracting an item’s unique ID from the item context. If left empty, a hash is computed automatically.
 
 ### Miscellaneous