Bläddra i källkod

Add support for custom XPath date/time format (#4703)

* Add support for custom XPath date/time format
#fix https://github.com/FreshRSS/FreshRSS/issues/4701
Improvement of https://github.com/FreshRSS/FreshRSS/pull/4220

* Format is not XPath

* Remove TODOs in en-GB
Alexandre Alapetite 3 år sedan
förälder
incheckning
648a876d77

+ 1 - 0
app/Controllers/feedController.php

@@ -211,6 +211,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 				if (Minz_Request::param('xPathItemUri', '') != '') $xPathSettings['itemUri'] = Minz_Request::param('xPathItemUri', '', true);
 				if (Minz_Request::param('xPathItemAuthor', '') != '') $xPathSettings['itemAuthor'] = Minz_Request::param('xPathItemAuthor', '', true);
 				if (Minz_Request::param('xPathItemTimestamp', '') != '') $xPathSettings['itemTimestamp'] = Minz_Request::param('xPathItemTimestamp', '', true);
+				if (Minz_Request::param('xPathItemTimeFormat', '') != '') $xPathSettings['itemTimeFormat'] = Minz_Request::param('xPathItemTimeFormat', '', 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);

+ 1 - 0
app/Controllers/subscriptionController.php

@@ -213,6 +213,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 				if (Minz_Request::param('xPathItemUri', '') != '') $xPathSettings['itemUri'] = Minz_Request::param('xPathItemUri', '', true);
 				if (Minz_Request::param('xPathItemAuthor', '') != '') $xPathSettings['itemAuthor'] = Minz_Request::param('xPathItemAuthor', '', true);
 				if (Minz_Request::param('xPathItemTimestamp', '') != '') $xPathSettings['itemTimestamp'] = Minz_Request::param('xPathItemTimestamp', '', true);
+				if (Minz_Request::param('xPathItemTimeFormat', '') != '') $xPathSettings['itemTimeFormat'] = Minz_Request::param('xPathItemTimeFormat', '', 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);

+ 7 - 0
app/Models/Feed.php

@@ -612,6 +612,7 @@ class FreshRSS_Feed extends Minz_Model {
 		$xPathItemUri = $xPathSettings['itemUri'] ?? '';
 		$xPathItemAuthor = $xPathSettings['itemAuthor'] ?? '';
 		$xPathItemTimestamp = $xPathSettings['itemTimestamp'] ?? '';
+		$xPathItemTimeFormat = $xPathSettings['itemTimeFormat'] ?? '';
 		$xPathItemThumbnail = $xPathSettings['itemThumbnail'] ?? '';
 		$xPathItemCategories = $xPathSettings['itemCategories'] ?? '';
 		$xPathItemUid = $xPathSettings['itemUid'] ?? '';
@@ -652,6 +653,12 @@ class FreshRSS_Feed extends Minz_Model {
 				$item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node);
 				$item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node);
 				$item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node);
+				if ($xPathItemTimeFormat != '') {
+					$dateTime = DateTime::createFromFormat($xPathItemTimeFormat, $item['timestamp'] ?? '');
+					if ($dateTime != false) {
+						$item['timestamp'] = $dateTime->format(DateTime::ATOM);
+					}
+				}
 				$item['thumbnail'] = $xPathItemThumbnail == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemThumbnail . ')', $node);
 				if ($xPathItemCategories != '') {
 					$itemCategories = @$xpath->query($xPathItemCategories, $node);

+ 1 - 0
app/Services/ImportService.php

@@ -174,6 +174,7 @@ class FreshRSS_Import_Service {
 						case 'xPathItemUri': $xPathSettings['itemUri'] = $value['value']; break;
 						case 'xPathItemAuthor': $xPathSettings['itemAuthor'] = $value['value']; break;
 						case 'xPathItemTimestamp': $xPathSettings['itemTimestamp'] = $value['value']; break;
+						case 'xPathItemTimeFormat': $xPathSettings['itemTimeFormat'] = $value['value']; break;
 						case 'xPathItemThumbnail': $xPathSettings['itemThumbnail'] = $value['value']; break;
 						case 'xPathItemCategories': $xPathSettings['itemCategories'] = $value['value']; break;
 						case 'xPathItemUid': $xPathSettings['itemUid'] = $value['value']; break;

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'náhled položky',
 					'help' => 'Příklad: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'datum položky',
 					'help' => 'Výsledek bude zpracován pomocí <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'Artikel-Vorschaubild',
 					'help' => 'Beispiel: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'Artikel-Datum',
 					'help' => 'Das Ergebnis wird durch <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a> geparst',

+ 4 - 0
app/i18n/en-us/sub.php

@@ -98,6 +98,10 @@ return array(
 					'_' => 'item thumbnail',	// IGNORE
 					'help' => 'Example: <code>descendant::img/@src</code>',	// IGNORE
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// IGNORE
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// IGNORE
+				),
 				'item_timestamp' => array(
 					'_' => 'item date',	// IGNORE
 					'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',	// IGNORE

+ 22 - 22
app/i18n/en/conf.php

@@ -68,9 +68,9 @@ return array(
 	),
 	'logs' => array(
 		'loglist' => array(
-			'level' => 'Log Level',	// TODO
-			'message' => 'Log Message',	// TODO
-			'timestamp' => 'Timestamp',	// TODO
+			'level' => 'Log Level',
+			'message' => 'Log Message',
+			'timestamp' => 'Timestamp',
 		),
 		'pagination' => array(
 			'first' => 'First',
@@ -139,28 +139,28 @@ return array(
 		'always_show_favorites' => 'Show all articles in favourites by default',
 		'article' => array(
 			'authors_date' => array(
-				'_' => 'Authors and date',	// TODO
-				'both' => 'In header and footer',	// TODO
-				'footer' => 'In footer',	// TODO
-				'header' => 'In header',	// TODO
-				'none' => 'None',	// TODO
+				'_' => 'Authors and date',
+				'both' => 'In header and footer',
+				'footer' => 'In footer',
+				'header' => 'In header',
+				'none' => 'None',
 			),
 			'feed_name' => array(
-				'above_title' => 'Above title/tags',	// TODO
-				'none' => 'None',	// TODO
-				'with_authors' => 'In authors and date row',	// TODO
+				'above_title' => 'Above title/tags',
+				'none' => 'None',
+				'with_authors' => 'In authors and date row',
 			),
-			'feed_title' => 'Feed title',	// TODO
+			'feed_title' => 'Feed title',
 			'tags' => array(
-				'_' => 'Tags',	// TODO
-				'both' => 'In header and footer',	// TODO
-				'footer' => 'In footer',	// TODO
-				'header' => 'In header',	// TODO
-				'none' => 'None',	// TODO
+				'_' => 'Tags',
+				'both' => 'In header and footer',
+				'footer' => 'In footer',
+				'header' => 'In header',
+				'none' => 'None',
 			),
 			'tags_max' => array(
-				'_' => 'Max number of tags shown',	// TODO
-				'help' => '0 means: show all tags and do not collapse them',	// TODO
+				'_' => 'Max number of tags shown',
+				'help' => '0 means: show all tags and do not collapse them',
 			),
 		),
 		'articles_per_page' => 'Number of articles per page',
@@ -171,7 +171,7 @@ return array(
 		'display_categories_unfolded' => 'Categories to unfold',
 		'headline' => array(
 			'articles' => 'Articles: Open/Close',
-			'articles_header_footer' => 'Articles: header/footer',	// TODO
+			'articles_header_footer' => 'Articles: header/footer',
 			'categories' => 'Left navigation: Categories',
 			'mark_as_read' => 'Mark article as read',
 			'misc' => 'Miscellaneous',
@@ -187,7 +187,7 @@ return array(
 			'article_viewed' => 'when the article is viewed',
 			'keep_max_n_unread' => 'Max number of articles to keep unread',
 			'scroll' => 'while scrolling',
-			'upon_gone' => 'when it is no longer in the upstream news feed',	// TODO
+			'upon_gone' => 'when it is no longer in the upstream news feed',
 			'upon_reception' => 'upon receiving the article',
 			'when' => 'Mark an article as read…',
 			'when_same_title' => 'if an identical title already exists in the top <i>n</i> newest articles',
@@ -222,7 +222,7 @@ return array(
 		'_' => 'Sharing',
 		'add' => 'Add a sharing method',
 		'blogotext' => 'Blogotext',
-		'deprecated' => 'This service is deprecated and will be removed from FreshRSS in a <a href="https://freshrss.github.io/FreshRSS/en/users/08_sharing_services.html" title="Open documentation for more information" target="_blank">future release</a>.',	// TODO
+		'deprecated' => 'This service is deprecated and will be removed from FreshRSS in a <a href="https://freshrss.github.io/FreshRSS/en/users/08_sharing_services.html" title="Open documentation for more information" target="_blank">future release</a>.',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'facebook' => 'Facebook',

+ 3 - 3
app/i18n/en/gen.php

@@ -18,7 +18,7 @@ return array(
 		'back_to_rss_feeds' => '← Go back to your RSS feeds',
 		'cancel' => 'Cancel',
 		'create' => 'Create',
-		'delete_muted_feeds' => 'Delete muted feeds',	// TODO
+		'delete_muted_feeds' => 'Delete muted feeds',
 		'demote' => 'Demote',
 		'disable' => 'Disable',
 		'empty' => 'Empty',
@@ -32,7 +32,7 @@ return array(
 		'open_url' => 'Open URL',
 		'promote' => 'Promote',
 		'purge' => 'Purge',
-		'refresh_opml' => 'Refresh OPML',	// TODO
+		'refresh_opml' => 'Refresh OPML',
 		'remove' => 'Remove',
 		'rename' => 'Rename',
 		'see_website' => 'See website',
@@ -130,7 +130,7 @@ return array(
 		'confirm_action_feed_cat' => 'Are you sure you want to perform this action? You will lose related favourites and user queries. It cannot be cancelled!',
 		'feedback' => array(
 			'body_new_articles' => 'There are %%d new articles to read on FreshRSS.',
-			'body_unread_articles' => '(unread: %%d)',	// TODO
+			'body_unread_articles' => '(unread: %%d)',
 			'request_failed' => 'A request has failed, it may have been caused by internet connection problems.',
 			'title_new_articles' => 'FreshRSS: new articles!',
 		),

+ 11 - 7
app/i18n/en/sub.php

@@ -25,12 +25,12 @@ return array(
 		'add' => 'Add a category',
 		'archiving' => 'Archiving',
 		'dynamic_opml' => array(
-			'_' => 'Dynamic OPML',	// TODO
-			'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds',	// TODO
+			'_' => 'Dynamic OPML',
+			'help' => 'Provide the URL to an <a href=http://opml.org/ target=_blank>OPML file</a> to dynamically populate this category with feeds',
 		),
 		'empty' => 'Empty category',
 		'information' => 'Information',
-		'opml_url' => 'OPML URL',	// TODO
+		'opml_url' => 'OPML URL',
 		'position' => 'Display position',
 		'position_help' => 'To control category sort order',
 		'title' => 'Title',
@@ -61,7 +61,7 @@ return array(
 		'css_path' => 'Article CSS selector on original website',
 		'css_path_filter' => array(
 			'_' => 'CSS selector of the elements to remove',
-			'help' => 'A CSS selector may be a list such as: <kbd>.footer, .aside</kbd>',	// TODO
+			'help' => 'A CSS selector may be a list such as: <kbd>.footer, .aside</kbd>',
 		),
 		'description' => 'Description',
 		'empty' => 'This feed is empty. Please verify that it is still maintained.',
@@ -98,6 +98,10 @@ return array(
 					'_' => 'item thumbnail',
 					'help' => 'Example: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',
+				),
 				'item_timestamp' => array(
 					'_' => 'item date',
 					'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',
@@ -107,8 +111,8 @@ return array(
 					'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 unique ID',
+					'help' => 'Optional. Example: <code>descendant::div/@data-uri</code>',
 				),
 				'item_uri' => array(
 					'_' => 'item link (URL)',
@@ -198,7 +202,7 @@ return array(
 		'_' => 'Subscription management',
 		'add' => 'Add a feed or category',
 		'add_category' => 'Add a category',
-		'add_dynamic_opml' => 'Add dynamic OPML',	// TODO
+		'add_dynamic_opml' => 'Add dynamic OPML',
 		'add_feed' => 'Add a feed',
 		'add_label' => 'Add a label',
 		'delete_label' => 'Delete a label',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'item thumbnail',	// TODO
 					'help' => 'Example: <code>descendant::img/@src</code>',	// TODO
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'item date',	// TODO
 					'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',	// TODO

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'miniature de l’article',
 					'help' => 'Exemple : <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Format personnalisé pour interpréter la date',
+					'help' => 'Optionnel. Un format supporté par <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> comme <code>d-m-Y H:i:s</code>',
+				),
 				'item_timestamp' => array(
 					'_' => 'date de l’article',
 					'help' => 'Le résultat sera passé à la fonction <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'item thumbnail',	// TODO
 					'help' => 'Example: <code>descendant::img/@src</code>',	// TODO
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'item date',	// TODO
 					'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',	// TODO

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'oggetto miniatura',
 					'help' => 'Esempio: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'oggetto data',
 					'help' => 'Il risultato verrà analizzato da <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => '項目のサムネイル',
 					'help' => '例: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => '項目の日付',
 					'help' => '結果は<a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>によってパースされます',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => '기사 섬네일',
 					'help' => '예제: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => '기사 날짜',
 					'help' => '결과 값은 <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>에서 파싱한 값을 이용합니다.',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'miniatuur van bericht',
 					'help' => 'Voorbeeld: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'datum van bericht',
 					'help' => 'Het resultaat zal worden geparset door <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'item vinheta',
 					'help' => 'Exemple : <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'item data',
 					'help' => 'Lo resultats serà formatat per la foncion <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'miniaturki',
 					'help' => 'Przykład: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'daty',
 					'help' => 'Wynik zostanie przetworzony za pomocą funkcji <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

+ 4 - 0
app/i18n/pt-br/sub.php

@@ -98,6 +98,10 @@ return array(
 					'_' => 'Miniatura do item',
 					'help' => 'Exemplo: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'Data do Item',
 					'help' => 'O resultado será parecido com: <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'эскиза элемента',
 					'help' => 'Пример: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'даты элемента',
 					'help' => 'Результат будет распарсен с <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'položka miniatúra',
 					'help' => 'Príklad: <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'položka dátum',
 					'help' => 'Výsledok spracuje <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',	// DIRTY

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

@@ -98,6 +98,10 @@ return array(
 					'_' => 'item thumbnail',	// TODO
 					'help' => 'Example: <code>descendant::img/@src</code>',	// TODO
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => 'item date',	// TODO
 					'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>',	// TODO

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

@@ -98,6 +98,10 @@ return array(
 					'_' => '文章缩略图',
 					'help' => '例如 <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => '文章日期:',
 					'help' => '结果将被 <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a> 解析',

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

@@ -98,6 +98,10 @@ return array(
 					'_' => '文章縮圖',
 					'help' => '例如 <code>descendant::img/@src</code>',
 				),
+				'item_timeFormat' => array(
+					'_' => 'Custom date/time format',	// TODO
+					'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>',	// TODO
+				),
 				'item_timestamp' => array(
 					'_' => '文章日期:',
 					'help' => '結果將被 <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a> 解析',

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

@@ -26,6 +26,7 @@ function feedsToOutlines($feeds, $excludeMutedFeeds = false): array {
 			$outline['frss:xPathItemUri'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemUri'] ?? null];
 			$outline['frss:xPathItemAuthor'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemAuthor'] ?? null];
 			$outline['frss:xPathItemTimestamp'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimestamp'] ?? null];
+			$outline['frss:xPathItemTimeformat'] = ['namespace' => FreshRSS_Export_Service::FRSS_NAMESPACE, 'value' => $xPathSettings['itemTimeformat'] ?? 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];

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

@@ -465,6 +465,15 @@
 					<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_timestamp.help') ?></p>
 				</div>
 			</div>
+			<div class="form-group">
+				<label class="group-name" for="xPathItemTimeFormat"><small>
+					<?= _t('sub.feed.kind.html_xpath.item_timeFormat') ?></label>
+				<div class="group-controls">
+					<textarea class="w100" name="xPathItemTimeFormat" id="xPathItemTimeFormat" rows="2" cols="64" spellcheck="false"
+						data-leave-validation="<?= $xpath['itemTimeFormat'] ?? '' ?>"><?= $xpath['itemTimeFormat'] ?? '' ?></textarea>
+					<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_timeFormat.help') ?></p>
+				</div>
+			</div>
 			<div class="form-group">
 				<label class="group-name" for="xPathItemCategories"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
 					<?= _t('sub.feed.kind.html_xpath.item_categories') ?></label>

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

@@ -140,6 +140,14 @@
 						<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_timestamp.help') ?></p>
 					</div>
 				</div>
+				<div class="form-group">
+					<label class="group-name" for="xPathItemTimeFormat">
+						<?= _t('sub.feed.kind.html_xpath.item_timeFormat') ?></label>
+					<div class="group-controls">
+						<textarea name="xPathItemTimeFormat" id="xPathItemTimeFormat" rows="2" cols="64" spellcheck="false"></textarea>
+						<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.html_xpath.item_timeFormat.help') ?></p>
+					</div>
+				</div>
 				<div class="form-group">
 					<label class="group-name" for="xPathItemCategories"><small><?= _t('sub.feed.kind.html_xpath.relative') ?></small><br />
 						<?= _t('sub.feed.kind.html_xpath.item_categories') ?></label>

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

@@ -36,6 +36,7 @@ The following attributes are using similar naming conventions than [RSS-Bridge](
 * `frss:xPathItemAuthor`: XPath expression for extracting an item author from the item context.
 	* Example: `"Anonymous"`
 * `frss:xPathItemTimestamp`: XPath expression for extracting an item timestamp from the item context. The result will be parsed by [`strtotime()`](https://php.net/strtotime).
+* `frss:xPathItemTimeFormat`: Date/Time format to parse the timestamp, according to [`DateTime::createFromFormat()`](https://php.net/datetime.createfromformat).
 * `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.